Java Lock example
Introduction
We have seen before in Java explicit lock example how to implement a lock using only the synchronized statement together with the wait/notify idiom.
Since Java 5, several facilities became bundled within the JDK itself - more precisely the java.util.concurrent package - that provide out-of-the-box synchronization and locking mechanisms.
In this tutorial we will see how to use a specific Lock interface implementation - more precisely the ReentrantLock - in order to properly synchronize access to shared resources.
The Lock
A lock usage scenario may look like the following:
package com.byteslounge.concurrency; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class ExclusiveDatabaseOperations { Lock lock = new ReentrantLock(); public void doWorkWithTheDatabase() { lock.lock(); try { // do work with the database } finally { lock.unlock(); } } }
We have defined a class - ExclusiveDatabaseOperations - that represents illustrative database operations that should be exclusive.
The class has a property of type ReentrantLock (one of the Lock interface implementations provided by the JDK) that will be used to synchronize access to the database.
As soon as a given thread enters the doWorkWithTheDatabase() method it will try to acquire the lock by executing lock() against our lock object. If no other thread currently holds the lock, our thread will successfully acquire the lock and proceed with the execution.
As soon as other threads also enter the doWorkWithTheDatabase() method and try to acquire the lock, they will hold execution in lock.lock() until the current lock holding thread (our thread) releases the lock.
Lock release is done by executing unlock() method against the lock object. Note that in our case we wrapped the synchronized code section inside a try/finally block. This is to make sure that whatever exception may happen inside the synchronized - and exclusive section - we guarantee that we will always release the lock.
The lock implementation we have chosen is the ReentrantLock. A reentrant lock is a kind of lock that may be acquired multiple times by the same thread while already holding the lock (ie. calling lock() on a given lock object while already holding that same lock). As a consequence, in order to release the lock the thread must call unlock() the same number of times it has previously acquired it.
Lock acquiring timeout
Just by calling lock() on a lock object it will make the thread to wait indefinitely for the lock to be available. Sometimes it may be useful to determine a timeout for lock acquisition, ie. the maximum amount of time a thread should wait to acquire the lock and give up otherwise.
Let's change the exclusive database access method we define before to the following:
public void doWorkWithTheDatabase(int timeout) throws InterruptedException { if (lock.tryLock(timeout, TimeUnit.SECONDS)) { try { // update database } finally { lock.unlock(); } } else { throw new RuntimeException("Could not acquire database lock"); } }
This time we are using tryLock() method instead. This method will try to acquire the lock for the specified amount of time and return true if the lock was successfully acquired meanwhile. If the lock is not successfully acquired the method returns false.
As we did before, in case the lock is successfully acquired we should always wrap the following synchronized code section in a try/finally block. This enforces the lock release in case of an eventual exception inside the synchronized section.
The tryLock() method may also be used without parameters. In this case the thread will try to acquire the lock and give up if the lock is not immediately available:
if (lock.tryLock()){ // ... }
Happens-before relationship
An happens before relationship will be established between two distinct threads that acquire the same lock object in sequence. If thread A acquires a lock, later releases the lock and thread B acquires that same lock, an happens-before relationship will be established between thread A and thread B.
It is the same happens-before relationship that is established when we use the synchronized statement or the volatile modifier, as we described in the following articles:
Lock fairness
The ReentrantLock class provided an additional constructor that takes a boolean parameter that represents fairness:
Lock lock = new ReentrantLock(true);
By default a ReentrantLock is not fair, ie. there is no guarantee on the order that eventually lock waiting threads will acquire the lock as soon as it is released.
On the other hand, if we specify the lock to be fair the waiting threads will acquire the released lock in a first-in-first-out basis.
A concurrent integer array
Based on the previous sections we may implement a concurrent integer array as another example of Lock usage:
package com.byteslounge.concurrency; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class ConcurrentIntegerArray { private final int[] array; private final int size; private final Lock lock = new ReentrantLock(); public ConcurrentIntegerArray(int size) { array = new int[size]; this.size = size; } public void set(int index, int value) { lock.lock(); try { array[index] = value; } finally { lock.unlock(); } } public int get(int index) { lock.lock(); try { return array[index]; } finally { lock.unlock(); } } public int getSize() { return size; } }
The above ConcurrentIntegerArray implementation is used here just to demonstrate an example of Lock usage in Java. The Java Collections Framework already provides concurrent and optimized data structures that serve the same purpose of this illustrative implementation and should be used instead.