Locks in Java
Learn advanced locking mechanisms for better control over thread synchronization
Think of locks like different types of keys for doors! The basic synchronized keyword is like having one master key for a room - simple but limited. Java's Lock interface gives you special keys with superpowers: keys that can try the door and walk away if it's locked, keys that can wait for a specific time, or even keys that let multiple people read a book together but only one person write in it at a time!
Why We Need Locks Beyond synchronized
The synchronized keyword is great, but it has limitations! You can't try to acquire a lock and give up if it's busy, you can't interrupt a waiting thread, and you can't have multiple readers at once. Let's see why we need more advanced locks:
// Limitations of synchronized keywordclass BankAccount { private int balance = 1000; // Problem 1: Can't try and give up if locked public synchronized void withdraw(int amount) { System.out.println(Thread.currentThread().getName() + " trying to withdraw..."); // If another thread has the lock, we MUST wait // No way to say "never mind, I'll come back later" try { Thread.sleep(1000); // Simulate long operation } catch (InterruptedException e) { e.printStackTrace(); } if (balance >= amount) { balance -= amount; System.out.println("Withdrew: " + amount); } } // Problem 2: Can't interrupt a waiting thread public synchronized void longOperation() { System.out.println("Starting long operation..."); try { // Even if we want to cancel, waiting threads can't be interrupted Thread.sleep(10000); } catch (InterruptedException e) { System.out.println("Operation interrupted!"); } } public int getBalance() { return balance; }}public class SynchronizedLimitations { public static void main(String[] args) { BankAccount account = new BankAccount(); // Thread 1 holds lock for long time Thread t1 = new Thread(() -> { account.longOperation(); }, "Thread-1"); // Thread 2 stuck waiting - can't give up or be interrupted easily Thread t2 = new Thread(() -> { account.withdraw(100); }, "Thread-2"); t1.start(); try { Thread.sleep(100); // Ensure t1 gets lock first } catch (InterruptedException e) { e.printStackTrace(); } t2.start(); System.out.println("Thread-2 is stuck waiting... no way to cancel!"); }}/* Output:Starting long operation...Thread-2 trying to withdraw...(Thread-2 waits for 10 seconds - no way to cancel or timeout!)PROBLEM: We need more flexible locking mechanisms!*/ReentrantLock - Basic Lock Usage
ReentrantLock is like an upgraded key that gives you more control! You can try the lock and give up if it's busy (tryLock), wait with a timeout, or even check if it's locked before trying. Plus, you must manually unlock it - which gives you flexibility but requires responsibility!
// Using ReentrantLock for better controlimport java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;class SmartBankAccount { private int balance = 1000; private Lock lock = new ReentrantLock(); // Method 1: Basic lock/unlock pattern public void deposit(int amount) { lock.lock(); // Acquire the lock try { System.out.println(Thread.currentThread().getName() + " depositing: " + amount); balance += amount; System.out.println("New balance: " + balance); } finally { lock.unlock(); // ALWAYS unlock in finally block! } } // Method 2: Try lock - don't wait if busy public boolean tryWithdraw(int amount) { if (lock.tryLock()) { // Try to get lock, return immediately try { System.out.println(Thread.currentThread().getName() + " got the lock!"); if (balance >= amount) { Thread.sleep(1000); // Simulate processing balance -= amount; System.out.println("Withdrew: " + amount); return true; } return false; } catch (InterruptedException e) { e.printStackTrace(); return false; } finally { lock.unlock(); } } else { System.out.println(Thread.currentThread().getName() + " - Lock busy, giving up!"); return false; } } public int getBalance() { lock.lock(); try { return balance; } finally { lock.unlock(); } }}public class ReentrantLockExample { public static void main(String[] args) { SmartBankAccount account = new SmartBankAccount(); // Thread 1 withdraws (takes time) Thread t1 = new Thread(() -> { account.tryWithdraw(500); }, "Customer-1"); // Thread 2 tries to withdraw but gives up if busy Thread t2 = new Thread(() -> { try { Thread.sleep(100); // Start slightly after t1 } catch (InterruptedException e) { e.printStackTrace(); } account.tryWithdraw(300); }, "Customer-2"); t1.start(); t2.start(); try { t1.join(); t2.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("\nFinal balance: " + account.getBalance()); }}/* Output:Customer-1 got the lock!Customer-2 - Lock busy, giving up!Withdrew: 500Final balance: 500SUCCESS: Customer-2 didn't wait - just gave up and left!This is impossible with synchronized keyword.*/Lock with Timeout - TryLock with Time
Sometimes you want to wait for a lock, but not forever! The tryLock() method can accept a timeout - it's like saying 'I'll wait at the door for 5 seconds, but if you don't open it by then, I'm leaving!' This is perfect for avoiding deadlocks and keeping your program responsive.
// Using tryLock with timeoutimport java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;import java.util.concurrent.TimeUnit;class TicketBooking { private int availableSeats = 10; private Lock lock = new ReentrantLock(); public boolean bookSeats(int seats) { String customer = Thread.currentThread().getName(); System.out.println(customer + " trying to book " + seats + " seats..."); try { // Wait up to 2 seconds for the lock if (lock.tryLock(2, TimeUnit.SECONDS)) { try { System.out.println(customer + " acquired lock!"); // Check availability if (availableSeats >= seats) { System.out.println(customer + " - Seats available, processing..."); // Simulate booking process Thread.sleep(1500); availableSeats -= seats; System.out.println(customer + " - Booking confirmed! " + "Remaining seats: " + availableSeats); return true; } else { System.out.println(customer + " - Not enough seats!"); return false; } } finally { lock.unlock(); } } else { System.out.println(customer + " - Timeout! Waited 2 seconds, giving up."); return false; } } catch (InterruptedException e) { System.out.println(customer + " - Interrupted while waiting!"); return false; } }}public class TryLockTimeoutExample { public static void main(String[] args) { TicketBooking booking = new TicketBooking(); // Customer 1 books first (takes 1.5 seconds) Thread customer1 = new Thread(() -> { booking.bookSeats(3); }, "Customer-1"); // Customer 2 waits up to 2 seconds (will succeed) Thread customer2 = new Thread(() -> { try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } booking.bookSeats(4); }, "Customer-2"); // Customer 3 waits up to 2 seconds (might timeout) Thread customer3 = new Thread(() -> { try { Thread.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); } booking.bookSeats(2); }, "Customer-3"); customer1.start(); customer2.start(); customer3.start(); try { customer1.join(); customer2.join(); customer3.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("\nBooking session complete!"); }}/* Output:Customer-1 trying to book 3 seats...Customer-1 acquired lock!Customer-2 trying to book 4 seats...Customer-3 trying to book 2 seats...Customer-1 - Seats available, processing...Customer-1 - Booking confirmed! Remaining seats: 7Customer-2 acquired lock!Customer-2 - Seats available, processing...Customer-2 - Booking confirmed! Remaining seats: 3Customer-3 acquired lock!Customer-3 - Seats available, processing...Customer-3 - Booking confirmed! Remaining seats: 1Booking session complete!*/ReadWriteLock - Multiple Readers, Single Writer
ReadWriteLock is super smart! It's like having a library where many people can read books at the same time, but only one person can write/edit a book at a time, and when someone is writing, nobody can read. This dramatically improves performance when you have many readers and few writers!
// Using ReadWriteLock for better concurrencyimport java.util.concurrent.locks.ReadWriteLock;import java.util.concurrent.locks.ReentrantReadWriteLock;class PriceList { private int applePrice = 10; private int bananaPrice = 5; private ReadWriteLock rwLock = new ReentrantReadWriteLock(); // Multiple threads can read simultaneously public void checkPrices() { rwLock.readLock().lock(); // Acquire read lock try { String reader = Thread.currentThread().getName(); System.out.println(reader + " reading prices..."); System.out.println(reader + " - Apple: $" + applePrice + ", Banana: $" + bananaPrice); Thread.sleep(1000); // Simulate reading time System.out.println(reader + " finished reading"); } catch (InterruptedException e) { e.printStackTrace(); } finally { rwLock.readLock().unlock(); } } // Only one thread can write at a time public void updatePrices(int newApple, int newBanana) { rwLock.writeLock().lock(); // Acquire write lock try { String writer = Thread.currentThread().getName(); System.out.println("\n" + writer + " UPDATING PRICES..."); applePrice = newApple; bananaPrice = newBanana; Thread.sleep(1500); // Simulate update time System.out.println(writer + " - Update complete!\n"); } catch (InterruptedException e) { e.printStackTrace(); } finally { rwLock.writeLock().unlock(); } }}public class ReadWriteLockExample { public static void main(String[] args) { PriceList priceList = new PriceList(); // Create 3 readers Runnable readTask = () -> { priceList.checkPrices(); }; // Create 1 writer Runnable writeTask = () -> { try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } priceList.updatePrices(12, 7); }; Thread reader1 = new Thread(readTask, "Reader-1"); Thread reader2 = new Thread(readTask, "Reader-2"); Thread reader3 = new Thread(readTask, "Reader-3"); Thread writer = new Thread(writeTask, "Writer"); // Start all threads reader1.start(); reader2.start(); writer.start(); try { Thread.sleep(1600); // Wait for writer to finish } catch (InterruptedException e) { e.printStackTrace(); } reader3.start(); // This reader sees updated prices try { reader1.join(); reader2.join(); reader3.join(); writer.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("All operations complete!"); }}/* Output:Reader-1 reading prices...Reader-1 - Apple: $10, Banana: $5Reader-2 reading prices...Reader-2 - Apple: $10, Banana: $5Reader-1 finished readingReader-2 finished readingWriter UPDATING PRICES...Writer - Update complete!Reader-3 reading prices...Reader-3 - Apple: $12, Banana: $7Reader-3 finished readingAll operations complete!NOTICE: Reader-1 and Reader-2 read simultaneously!Writer had exclusive access. Reader-3 saw new prices.*/Lock Fairness and Reentrancy
ReentrantLock can be 'fair' - meaning threads get the lock in the order they requested it, like a proper queue! Also, 'reentrant' means the same thread can acquire the lock multiple times (useful when one locked method calls another locked method). Let's see both features!
// Demonstrating fairness and reentrancyimport java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;class FairQueue { // Fair lock - threads get access in order they requested private Lock fairLock = new ReentrantLock(true); // true = fair private int counter = 0; public void accessResource() { String thread = Thread.currentThread().getName(); System.out.println(thread + " requesting lock..."); fairLock.lock(); try { System.out.println(thread + " GOT LOCK! Counter: " + (++counter)); Thread.sleep(100); // Hold lock briefly } catch (InterruptedException e) { e.printStackTrace(); } finally { fairLock.unlock(); System.out.println(thread + " released lock"); } }}class ReentrantExample { private Lock lock = new ReentrantLock(); // Outer method acquires lock public void outerMethod() { lock.lock(); try { System.out.println(Thread.currentThread().getName() + " - Outer method has lock"); // Calling inner method that ALSO needs the lock innerMethod(); System.out.println(Thread.currentThread().getName() + " - Back to outer method"); } finally { lock.unlock(); } } // Inner method needs same lock - this works because it's reentrant! public void innerMethod() { lock.lock(); try { System.out.println(Thread.currentThread().getName() + " - Inner method ALSO has lock (reentrant!)"); // Check how many times current thread holds this lock System.out.println("Hold count: " + ((ReentrantLock) lock).getHoldCount()); } finally { lock.unlock(); } }}public class FairnessReentrancyExample { public static void main(String[] args) { System.out.println("=== Testing Fairness ==="); FairQueue queue = new FairQueue(); // Create 5 threads - with fair lock, they'll get access in order for (int i = 1; i <= 5; i++) { new Thread(() -> { queue.accessResource(); }, "Thread-" + i).start(); } try { Thread.sleep(1000); // Wait for fairness demo } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("\n=== Testing Reentrancy ==="); ReentrantExample reentrant = new ReentrantExample(); Thread t = new Thread(() -> { reentrant.outerMethod(); }, "Worker"); t.start(); try { t.join(); } catch (InterruptedException e) { e.printStackTrace(); } }}/* Output:=== Testing Fairness ===Thread-1 requesting lock...Thread-2 requesting lock...Thread-3 requesting lock...Thread-4 requesting lock...Thread-5 requesting lock...Thread-1 GOT LOCK! Counter: 1Thread-1 released lockThread-2 GOT LOCK! Counter: 2Thread-2 released lockThread-3 GOT LOCK! Counter: 3Thread-3 released lockThread-4 GOT LOCK! Counter: 4Thread-4 released lockThread-5 GOT LOCK! Counter: 5Thread-5 released lock=== Testing Reentrancy ===Worker - Outer method has lockWorker - Inner method ALSO has lock (reentrant!)Hold count: 2Worker - Back to outer methodNOTICE: Fair locks grant access in FIFO order!Reentrant locks allow same thread to acquire multiple times.*/Real-World Example: Thread-Safe Cache with Lock
Let's build a practical cache system that multiple threads can use safely! This cache uses ReadWriteLock for maximum performance - many threads can read cached data simultaneously, but only one can update the cache at a time. Perfect for real applications!
// Real-world thread-safe cache implementationimport java.util.HashMap;import java.util.Map;import java.util.concurrent.locks.ReadWriteLock;import java.util.concurrent.locks.ReentrantReadWriteLock;class DataCache<K, V> { private Map<K, V> cache = new HashMap<>(); private ReadWriteLock lock = new ReentrantReadWriteLock(); private int hits = 0; private int misses = 0; // Read from cache (multiple threads can do this simultaneously) public V get(K key) { lock.readLock().lock(); try { String thread = Thread.currentThread().getName(); V value = cache.get(key); if (value != null) { hits++; System.out.println(thread + " - CACHE HIT for key: " + key); } else { misses++; System.out.println(thread + " - CACHE MISS for key: " + key); } return value; } finally { lock.readLock().unlock(); } } // Write to cache (exclusive access needed) public void put(K key, V value) { lock.writeLock().lock(); try { String thread = Thread.currentThread().getName(); System.out.println(thread + " - CACHING: " + key + " = " + value); // Simulate database/API call delay Thread.sleep(200); cache.put(key, value); System.out.println(thread + " - Cache updated!"); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.writeLock().unlock(); } } // Clear cache (exclusive access needed) public void clear() { lock.writeLock().lock(); try { cache.clear(); System.out.println("Cache cleared!"); } finally { lock.writeLock().unlock(); } } // Get statistics (read access) public void printStats() { lock.readLock().lock(); try { System.out.println("\n=== Cache Statistics ==="); System.out.println("Size: " + cache.size()); System.out.println("Hits: " + hits); System.out.println("Misses: " + misses); double hitRate = hits + misses > 0 ? (hits * 100.0) / (hits + misses) : 0; System.out.println("Hit Rate: " + String.format("%.2f", hitRate) + "%"); System.out.println("======================\n"); } finally { lock.readLock().unlock(); } }}public class CacheExample { public static void main(String[] args) { DataCache<String, String> cache = new DataCache<>(); // Thread 1: Add data to cache Thread writer1 = new Thread(() -> { cache.put("user:1", "Alice"); cache.put("user:2", "Bob"); }, "Writer-1"); // Thread 2: Read from cache (will miss initially) Thread reader1 = new Thread(() -> { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } cache.get("user:1"); cache.get("user:3"); // This will miss }, "Reader-1"); // Thread 3: Add more data Thread writer2 = new Thread(() -> { try { Thread.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); } cache.put("user:3", "Charlie"); }, "Writer-2"); // Thread 4: Read again (should hit this time) Thread reader2 = new Thread(() -> { try { Thread.sleep(600); } catch (InterruptedException e) { e.printStackTrace(); } cache.get("user:1"); // Hit cache.get("user:2"); // Hit cache.get("user:3"); // Hit }, "Reader-2"); // Start all threads writer1.start(); reader1.start(); writer2.start(); reader2.start(); try { writer1.join(); reader1.join(); writer2.join(); reader2.join(); } catch (InterruptedException e) { e.printStackTrace(); } // Print final statistics cache.printStats(); }}/* Output:Writer-1 - CACHING: user:1 = AliceWriter-1 - Cache updated!Writer-1 - CACHING: user:2 = BobWriter-1 - Cache updated!Reader-1 - CACHE HIT for key: user:1Reader-1 - CACHE MISS for key: user:3Writer-2 - CACHING: user:3 = CharlieWriter-2 - Cache updated!Reader-2 - CACHE HIT for key: user:1Reader-2 - CACHE HIT for key: user:2Reader-2 - CACHE HIT for key: user:3=== Cache Statistics ===Size: 3Hits: 4Misses: 1Hit Rate: 80.00%======================Multiple readers accessed cache simultaneously!Writers had exclusive access for updates.*/Key Concepts
Lock vs synchronized
Locks provide more flexibility than synchronized: tryLock, timed waits, interruptible lock acquisition, and separate read/write locks. But you must manually unlock (always in finally block!), whereas synchronized auto-releases.
ReentrantLock
Allows the same thread to acquire the lock multiple times (reentrant). Keeps a hold count - must unlock the same number of times as locked. Supports fair and non-fair modes.
ReadWriteLock
Maintains two locks: read lock (shared - multiple threads) and write lock (exclusive - one thread). Perfect for scenarios with many readers and few writers, dramatically improving concurrency.
Lock Fairness
Fair locks grant access in FIFO order (threads waiting longest get priority). Non-fair locks (default) are faster but threads might 'starve'. Choose based on your priority: fairness vs throughput.
Best Practices
- ✓Always unlock in a finally block to ensure the lock is released even if exceptions occur
- ✓Use tryLock with timeout instead of lock() to avoid deadlocks and improve responsiveness
- ✓Prefer ReadWriteLock when you have many readers and few writers - it dramatically improves performance
- ✓Keep the locked section as small as possible - don't do slow I/O operations while holding a lock
- ✓Consider fair locks when thread starvation is a concern, but remember they're slower
- ✓Document which locks protect which data and the locking order to prevent deadlocks
Common Mistakes to Avoid
- ✗Forgetting to unlock in finally block - if an exception occurs, the lock stays locked forever!
- ✗Using lock() without tryLock - you lose the flexibility that makes Lock better than synchronized
- ✗Acquiring write lock when read lock is sufficient - hurts performance unnecessarily
- ✗Locking for too long - performing slow operations while holding the lock
- ✗Not checking tryLock return value - assuming you got the lock when you didn't
- ✗Mismatched lock/unlock calls - locking N times but unlocking M times leads to deadlock or IllegalMonitorStateException
Interview Tips
- 💡Know the advantages of Lock over synchronized: tryLock, timed waits, interruptibility, ReadWriteLock
- 💡Always mention unlocking in finally block when discussing Lock - this is critical!
- 💡Understand ReentrantLock's fairness parameter and its trade-offs (fairness vs performance)
- 💡Be able to explain ReadWriteLock and when it's beneficial (read-heavy workloads)
- 💡Know that ReentrantLock allows checking if locked, getting hold count, and checking if current thread owns the lock
- 💡Understand the difference between lock(), tryLock(), and tryLock(time, unit) - when to use each