Home/Java/Locks in Java

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:

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
// Limitations of synchronized keyword
class 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!

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
// Using ReentrantLock for better control
import 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: 500
Final balance: 500
SUCCESS: 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.

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
// Using tryLock with timeout
import 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: 7
Customer-2 acquired lock!
Customer-2 - Seats available, processing...
Customer-2 - Booking confirmed! Remaining seats: 3
Customer-3 acquired lock!
Customer-3 - Seats available, processing...
Customer-3 - Booking confirmed! Remaining seats: 1
Booking 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!

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
// Using ReadWriteLock for better concurrency
import 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: $5
Reader-2 reading prices...
Reader-2 - Apple: $10, Banana: $5
Reader-1 finished reading
Reader-2 finished reading
Writer UPDATING PRICES...
Writer - Update complete!
Reader-3 reading prices...
Reader-3 - Apple: $12, Banana: $7
Reader-3 finished reading
All 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!

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
// Demonstrating fairness and reentrancy
import 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: 1
Thread-1 released lock
Thread-2 GOT LOCK! Counter: 2
Thread-2 released lock
Thread-3 GOT LOCK! Counter: 3
Thread-3 released lock
Thread-4 GOT LOCK! Counter: 4
Thread-4 released lock
Thread-5 GOT LOCK! Counter: 5
Thread-5 released lock
=== Testing Reentrancy ===
Worker - Outer method has lock
Worker - Inner method ALSO has lock (reentrant!)
Hold count: 2
Worker - Back to outer method
NOTICE: 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!

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
// Real-world thread-safe cache implementation
import 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 = Alice
Writer-1 - Cache updated!
Writer-1 - CACHING: user:2 = Bob
Writer-1 - Cache updated!
Reader-1 - CACHE HIT for key: user:1
Reader-1 - CACHE MISS for key: user:3
Writer-2 - CACHING: user:3 = Charlie
Writer-2 - Cache updated!
Reader-2 - CACHE HIT for key: user:1
Reader-2 - CACHE HIT for key: user:2
Reader-2 - CACHE HIT for key: user:3
=== Cache Statistics ===
Size: 3
Hits: 4
Misses: 1
Hit 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