Quick Refresh : Lock in Java : ReentrantLock, ReentrantReadWriteLock, StampedLock and Conditions
1. Lock
a lock is a more flexible and sophisticated thread synchronization mechanism than the standard synchronized block.
The Lock interface has been around since Java 1.5. It’s defined inside the java.util.concurrent. locks package and it provides extensive operations for locking.
2. Differences between Lock and Synchronized block
There are few differences between the use of synchronized block and using Lock API’s:
- A synchronized block is fully contained within a method – we can have Lock API’s lock() and unl
ock() operation in separate methods - A synchronized block doesn’t support the fairness, any thread can acquire the lock once released, no preference can be specified. We can achieve fairness within the Lock APIs by specifying the fairness property. It makes sure that longest waiting thread is given access to the lock
- A thread gets blocked if it can’t get an access to the synchronized block. The Lock
API provides tryLock() method. The thread acquires lock only if it’s available and not held by any other thread. This reduces blocking time of thread waiting for the lock - A thread which is in “waiting” state to acquire the access to synchronized block, can’t be interrupted. The Lock API provides a method lockInterruptibly() whi
ch can be used to interrupt the thread when it’s waiting for the lock
3. Lock API
Let’s take a look at the methods in the Lock interface:
- void lock() – acquire the lock if it’s available; if the lock isn’t available a thread gets blocked until the lock is released
- void lockInterruptibly() – this is similar to the lock(), but it allows the blocked thread to be interrupted and resume the execution through a thrown java.lang.
InterruptedException - boolean tryLock() – this is a non-blocking version of lock() method; it attempts to acquire the lock immediately, return true if locking succeeds
- boolean tryLock(long timeout, TimeUnit timeUnit) – this is similar to tryLock(), except it waits up the given timeout before giving up trying to acquire the Lock
- void unlock() – unlocks the Lock instance
A locked instance should always be unlocked to avoid deadlock condition. A recommended code block to use the lock should contain a try/catch and finally block:
1
2
3
4
5
6
7
| Lock lock = ...; lock.lock(); try { // access to the shared resource } finally { lock.unlock(); } |
In addition to the Lock interface, we have a ReadWriteLock interface which maintains a pair of locks, one for read-only operations, and one for the write operation. The read lock may be simultaneously held by multiple threads as long as there is no write.
ReadWriteLock declares methods to acquire read or write locks:
- Lock readLock() – returns the lock that’s used for reading
- Lock writeLock() – returns the lock that’s used for writing
4. Lock implementations
4.1. ReentrantLock
The ReentrantLock class implements the Lock interface and provides synchronization to methods while accessing shared resources. The code which manipulates the shared resource is surrounded by calls to lock and unlock method. This gives a lock to the current working thread and blocks all other threads which are trying to take a lock on the shared resource.
As the name says, ReentrantLock allow threads to enter into lock on a resource more than once. When the thread first enters into lock, a hold count is set to one. Before unlocking the thread can re-enter into lock again and every time hold count is incremented by one. For every unlock request, hold count is decremented by one and when hold count is 0, the resource is unlocked.
Reentrant Locks also offer a fairness parameter, by which the lock would abide by the order of the lock request i.e. after a thread unlocks the resource, the lock would go to the thread which has been waiting for the longest time. This fairness mode is set up by passing true to the constructor of the lock. The fairness parameter used to construct the lock object decreases the throughput of the program.
Let’s see, how we can use ReenrtantLock for synchronization:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| public class SharedObject { //... ReentrantLock lock = new ReentrantLock(); int counter = 0 ; public void perform() { lock.lock(); // hold count 1 lock.lock(); // hold count 2
try { // Critical section here count++; } finally { lock.unlock(); // hold count1 lock.unlock(); //hold count 0 } } //... } |
We need to make sure that we are wrapping the lock() and the unlock() calls in the try-finally block to avoid the deadlock situations.
Let’s see how the tryLock() works:
1
2
3
4
5
6
7
8
9
10
11
12
13
| public void performTryLock(){ //... boolean isLockAcquired = lock.tryLock( 1 , TimeUnit.SECONDS); if (isLockAcquired) { try { //Critical section here } finally { lock.unlock(); } } //... } |
In this case, the thread calling tryLock(), will wait for one second and will give up waiting if the lock isn’t available.
4.2. ReentrantReadWriteLock
ReentrantReadWriteLock class implements the ReadWriteLock interface.
Let’s see rules for acquiring the ReadLock or WriteLock by a thread:
- Read Lock – if no thread acquired the write lock or requested for it then multiple threads can acquire the read lock
- Write Lock – if no threads are reading or writing then only one thread can acquire the write lock
Let’s see how to make use of the ReadWriteLock:
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
| public class SynchronizedHashMapWithReadWri Map<String,String> syncHashMap = new HashMap<>(); ReadWriteLock lock = new ReentrantReadWriteLock(); // ... Lock writeLock = lock.writeLock(); public void put(String key, String value) { try { writeLock.lock(); syncHashMap.put( } finally { writeLock.unlock() } } ... public String remove(String key){ try { writeLock.lock(); return syncHashMap.remove(key); } finally { writeLock.unlock() } } //... } |
For both the write methods, we need to surround the critical section with the write lock, only one thread can get access to it:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| Lock readLock = lock.readLock(); //... public String get(String key){ try { readLock.lock(); return syncHashMap.get(key); } finally { readLock.unlock(); } } public boolean containsKey(String key) { try { readLock.lock(); return syncHashMap.containsKey(key); } finally { readLock.unlock(); } } |
For both read methods, we need to surround the critical section with the read lock. Multiple threads can get access to this section if no write operation is in progress.
Note: ReentrantReadWriteLock provides an explicit locking mechanism. In ReadWrite Locking policy, it allows the read lock to be held simultaneously by multiple reader threads, as long as there are no writers, and the write lock is exclusive.
But it was later discovered that ReentrantReadWriteLock has some severe issues with starvation if not handled properly (using fairness may help, but it may be an overhead and compromise throughput). For example, a number of reads but very few writes can cause the writer thread to fall into starvation. Make sure to analyze your setup properly to know how many reads/writes are present before choosing ReadWriteLock.
4.3. StampedLock
StampedLock is introduced in Java 8. StampedLock is made of a stamp and mode, where your lock acquisition method returns a stamp, which is a long value used for unlocking within the finally block. If the stamp is ever zero, that means there's been a failure to acquire access. StampedLock is all about giving us a possibility to perform optimistic reads.
Keep one thing in mind: StampedLock is not reentrant, so each call to acquire the lock always returns a new stamp and blocks if there's no lock available, even if the same thread already holds a lock, which may lead to deadlock.
Another point to note is that ReadWriteLock has two modes for controlling the read/write access while StampedLock has three modes of access:
Reading: Method 'public long readLock()' actually acquires a non-exclusive lock, and it blocks, if necessary, until available. It returns a stamp that can be used to unlock or convert the mode.
Writing: Method 'public long writeLock()' acquires an exclusive lock, and it blocks, if necessary, until available. It returns a stamp that can be used to unlock or convert the mode.
Optimistic reading: Method 'public long tryOptimisticRead()' acquires a non-exclusive lock without blocking only when it returns a stamp that can be later validated. Otherwise the value is zero if it doesn't acquire a lock. This is to allow read operations. After calling the tryOptimisticRead() method, always check if the stamp is valid using the 'lock.validate(stamp)' method, as the optimistic read lock doesn't prevent another thread from getting a write lock, which will make the optimistic read lock stamp's invalid.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| public class StampedLockDemo { Map<String,String> map = new HashMap<>(); private StampedLock lock = new StampedLock(); public void put(String key, String value){ long stamp = lock.writeLock(); try { map.put(key, value); } finally { lock.unlockWrite( } } public String get(String key) throws InterruptedException { long stamp = lock.readLock(); try { return map.get(key); } finally { lock.unlockRead( } } } |
Another feature provided by StampedLock is optimistic locking. Most of the time read operations doesn’t need to wait for write operation completion and as a result of this, the full-fledged read lock isn’t required.
Instead, we can upgrade to read lock:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| public String readWithOptimisticLock(String key) { long stamp = lock.tryOptimisticRead(); String value = map.get(key); if (!lock.validate(stamp)) { stamp = lock.readLock(); try { return map.get(key); } finally { lock.unlock(stamp) } } return value; }
You might be wondering how to convert the mode and when. Well:
Note: One thing to note is that the tryConvertToReadLock and tryConvertToWriteLock methods will not block and may return the stamp as zero, which means these methods' calls were not successful.
|
5. Working with Conditions
The Condition class provides the ability for a thread to wait for some condition to occur while executing the critical section.
This can occur when a thread acquires the access to the critical section but doesn’t have the necessary condition to perform its operation. For example, a reader thread can get access to the lock of a shared queue, which still doesn’t have any data to consume.
Traditionally Java provides wait(), notify() and notifyAll() methods for thread intercommunication. Conditions have similar mechanisms, but in addition, we can specify multiple conditions:
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
| public class ReentrantLockWithCondition { Stack<String> stack = new Stack<>(); int CAPACITY = 5 ; ReentrantLock lock = new ReentrantLock(); Condition stackEmptyCondition = lock.newCondition(); Condition stackFullCondition = lock.newCondition(); public void pushToStack(String item){ try { lock.lock(); while (stack.size() == CAPACITY) { stackFullCondi } stack.push(item); stackEmptyConditio } finally { lock.unlock(); } } public String popFromStack() { try { lock.lock(); while (stack.size() == 0 ) { stackEmptyCond } return stack.pop(); } finally { stackFullCondition lock.unlock(); } } } |
Comments
Post a Comment