Java ReentrantLock Example
In this example, we shall be demonstrating how to use ReentrantLock in Java.
Rigidness of Intrinsic Locking :
The traditional way of providing synchronization in multi-threaded environment was to use the synchronized
keyword. However, the synchronized
keyword is considered rather rigid under certain circumstances.For example, if a thread is already executing inside a synchronized block
and another thread tries to enter the block, it has to wait until the currently running thread exits the block. As long as the thread does not enter the synchronized block it remains blocked and cannot be interrupted.
Any thread can acquire the lock once the synchronized thread is exited by a currently running thread. This may lead to starvation of some(unlucky?) thread.
Synchronized blocks don’t offer any mechanism to query the status of “waiting queue” of threads for a particular resource.
The synchronized block have to be present within the same method. A synchronized block cannot start in one method and end in another.
ReentrantLock to the Rescue!
While every application does not need the flexibility discussed above, it is certainly worth looking into how we can design a more flexible application using the ReentrantLock
Class.
The ReentrantLock
was provided in the java.util.concurrent.locks
package since JDK 1.5, which provides extrinsic locking mechanism.
ReentrantLock Class provides a constructor that optionally takes a boolean
parameter fair. When more than one thread are competing for the same lock, the lock favors granting access to the longest waiting thread.(The reader should not misunderstand this as a means to guarantee fairness in Thread scheduling.)
ReentrantLock Class provides a number of ways to acquire locks. The tryLock()
and the tryLock(long timeout, TimeUnit unit)
methods provide means for the threads to time-out while waiting for a lock instead of indefinitely waiting till lock is acquired.
try{ lock.tryLock(300,TimeUnit.SECONDS); } catch(InterruptedException ie) { ... }
It also provides us methods which provide information about list of Threads
waiting for acquiring the lock and are queued.
Also, the lockInterruptibly()
method provides the mechanism to interrupt a thread while it has acquired the lock. This method however, throws InterruptedException
if the thread is still waiting for the lock.
Let’s have a look at a sample program using ReentrantLock
.
ThreadSafeArrayList.java:
package com.jcg.examples; import java.util.ArrayList; import java.util.List; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class ThreadSafeArrayList<E> { private final Lock lock = new ReentrantLock(); private final List<E> list = new ArrayList<E>(); private static int i = 0; public void set(E o) { lock.lock(); try { i++; list.add(o); System.out.println("Adding element by thread"+Thread.currentThread().getName()); } finally { lock.unlock(); } } public static void main(String[] args) { final ThreadSafeArrayList<String> lockExample = new ThreadSafeArrayList<String>(); Runnable syncThread = new Runnable() { @Override public void run() { while (i < 6) { lockExample.set(String.valueOf(i)); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } }; Runnable lockingThread = new Runnable() { @Override public void run() { while (i < 6) { lockExample.set(String.valueOf(i)); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } }; Thread t1 = new Thread(syncThread, "syncThread"); Thread t2 = new Thread(lockingThread, "lockingThread"); t1.start(); t2.start(); } }
Output :
Adding element by threadsyncThread Adding element by threadlockingThread Adding element by threadlockingThread Adding element by threadsyncThread Adding element by threadlockingThread Adding element by threadsyncThread
Down-sides with ReentrantLock
While ReentrantLock
Class does provide a lot of flexibility and control it does carry some baggage.
- A careless developer can easily forget to release a lock acquired and that will introduce subtle bugs into the software which are a bit difficult to debug.
- The code using normal
synchronized
block is far more readable than a corresponding code using ReentrantLock. - The fairness parameter used to construct the
ReentrantLock
object can reduce the throughput of the program
Conclusion :
While the ReentrantLock requires a little more expertise and carefulness on the part of the programmer, the flexibility and control offered by it, is certainly worth the effort.