Last Updated on July 9, 2023 by KnownSense
The lock interface is one of the most used interfaces in Java. Lock interface is available in the Java.util.concurrent.locks package which we use as a thread synchronization mechanism, i.e., similar to synchronized blocks. It is more flexible and provides more options in comparison to the synchronized block. It allows us to gain the benefit of fine-grained locking without having to use the synchronized keyword.
To use them, you first instantiate a lock and follow the bellow pattern.
Lock lock = new ReentrantLock();
lock.lock();
try {
<<use shared resource>>
} finally {
lock.unlock();
}
Benefits
- Abstraction:
In above example The concrete class is ReentrantLock. Our variable is of type Lock. This gives us a nice abstraction with the following methods
• lock() — acquire the lock and block if needed
• lockInterruptibly() — acquire the lock, block if needed, allow interruption
• newCondition() —bind a Condition to this lock
• tryLock() and tryLock(…) —acquire the lock without blocking or with a time out
• unlock() — release the lock
Currently java.util.concurrent.locks provide us with two types of locks- ReentrantLock and ReentrantReadWriteLock
The interface Lock allows us to write code that is not dependent on the concrete classes and could change if better options are added in the future
The Lock interface gives us four different ways of acquiring a lock: blocking, non-blocking, with a time out, with the option of being interrupted - Thread Interruption: With synchronized keyword, there’s no way to do it. if a thread becomes blocked then it it will be stuck until you kill the JVM.
With the Lock interface, you simply acquire it with the lockInterruptibly() method and then Thread.interrupt() behaves as you would expect. - Timeouts: With the Lock interface, you have the option of calling tryLock() and specify a timeout.
if (lock.tryLock(50L, TimeUnit.SECONDS)) {
Lock acquired
} else {
Timeout occurred; act accordingly
} - Condition Variables: Condition Variables are used when your thread’s logic involves waiting for an event to occur (or a condition to become true) before it does its work If the event has not happened or the condition is false, we want the thread to block and not consume the scheduler’s time
ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();
lock.lock()
try {
while (!«condition is true») { condition.await(); }
«use shared resources»
} finally {
lock.unlock();
}
Producer consumer problem – Using Lock interface
Below sample program shows how does two condition variables are used for inter thread communication- once a thread(lets say producer) finds queue is not empty then this thread wait till one space is vacant in queue. Similarly, another thread(consumer) finds queue is empty, it waits till queue is filled and it communicates to waiting producer thread via signal()/signalAll().
public class ProducerConsumerUsingLockAndCondition {
//Shared resources used by all threads in the system
static class SharedResource {
private static Queue<Integer> queue;
private static int MAX_QUEUE_SIZE;
private static Random random;
public SharedResource() {
queue = new LinkedList<Integer>();
random = new Random();
MAX_QUEUE_SIZE = 10;
}
}
static class ProducerConsumerImplementation {
// create lock instance followed by condition variable
private final Lock lock = new ReentrantLock();
private final Condition bufferFull = lock.newCondition();
private final Condition bufferEmpty = lock.newCondition();
public void put() {
try {
lock.lock(); // Acquire lock and block other threads
while (SharedResource.queue.size() == SharedResource.MAX_QUEUE_SIZE) {
System.out.println("Size of buffer is "
+ SharedResource.queue.size());
bufferFull.await(); // wait till buffer is full, no place to
// store
}// while close
int nextval = (Integer) SharedResource.random.nextInt();
boolean status = (Boolean) SharedResource.queue.offer(nextval);
if (status) {
System.out.println("Thread "
+ Thread.currentThread().getName()
+ " added value " + nextval + "in queue ");
bufferEmpty.signalAll();// similar to notifyAll -
// communicate waiting thread
// that queue is not empty now
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();// Release lock
}
}
public void get() {
Integer returnVal = Integer.MIN_VALUE;
try {
lock.lock();// aquire lock
while (SharedResource.queue.size() == 0) {
System.out
.println("No element in Buffer, wait at least one element is available");
bufferEmpty.await();
}
System.out.println("Size of buffer is "
+ SharedResource.queue.size());
returnVal = (Integer) SharedResource.queue.poll();
if (returnVal != null) {
System.out.println("Thread "
+ Thread.currentThread().getName()
+ " consumed value " + returnVal + " in queue ");
bufferFull.signalAll(); // communicate waiting thread that queue has space
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();// release lock
}
}
}
static class Producer implements Runnable {
SharedResource sharedObj;
ProducerConsumerImplementation pci;
public Producer(SharedResource sharedObj,
ProducerConsumerImplementation pci) {
this.sharedObj = sharedObj;
this.pci = pci;
}
public void run() {
int i = 0;
while (true) {
pci.put();
i++;
}
}
}
static class Consumer implements Runnable {
SharedResource sharedObj;
ProducerConsumerImplementation pci;
public Consumer(SharedResource sharedObj,
ProducerConsumerImplementation pci) {
this.sharedObj = sharedObj;
this.pci = pci;
}
public void run() {
int i = 0;
while (true) {
pci.get();
i++;
}
}
}
public static void main(String[] args) {
SharedResource sharedResource = new SharedResource();
ProducerConsumerImplementation producerConsumerImplementation = new ProducerConsumerImplementation();
Thread tp1 = new Thread(new Producer(sharedResource, producerConsumerImplementation), "producer1");
Thread tc1 = new Thread(new Consumer(sharedResource, producerConsumerImplementation), "consumer1");
tc1.start();
tp1.start();
}
}