Java Lock
Jakob Jenkov |
The Java Lock interface, java.util.concurrent.locks.Lock
,
represents a concurrent lock which can be used to guard against race conditions inside critical sections.
Thus, the Java Lock interface provides a more flexible alternative to a Java synchronized block.
In this Java Lock tutorial I will explain how the Lock interface works, and how to use it.
If you are not familiar with Java synchronized blocks, race conditions and critical sections, you can
read more about it in my tutorials:
By the way, in my Java Concurrency tutorial I have described how to implement your own locks, in case you are interested (or need it). See my text on Locks for more details.
Java Lock Tutorial Video
If you prefer video, I have a video version of this tutorial here: Java Lock Tutorial
Main Differences Between a Lock and a Synchronized Block
The main differences between a Lock
and a synchronized block are:
- A synchronized block makes no guarantees about the sequence in which threads waiting to entering it are granted access.
- You cannot pass any parameters to the entry of a synchronized block. Thus, having a timeout trying to get access to a synchronized block is not possible.
- The synchronized block must be fully contained within a single method.
A
Lock
can have it's calls tolock()
andunlock()
in separate methods.
Java Lock Implementations
Since Java Lock is an interface, you cannot create an instance of Lock directly. You must create an instance of
a class that implements the Lock interface. The java.util.concurrent.locks
package has the following
implementations of the Lock
interface:
- java.util.concurrent.locks.ReentrantLock
In the following sections I will explain how to use the ReentrantLock class as a Lock.
Create Reentrant Lock
To create an instance of the ReentrantLock class you simply use the new
operator, like this:
Lock lock = new ReentrantLock();
Now you have a Java Lock instance - a ReentrantLock instance actually.
Locking and Unlocking a Java Lock
Since Lock
is an interface, you need to use one of its implementations to use a Lock
in your applications. In the following example I create an instance of ReentrantLock. To lock the Lock instance
you must call its lock()
method. To unlock the Lock instance you must call its unlock()
method. Here is an example of locking and unlocking a Java lock instance:
Lock lock = new ReentrantLock(); lock.lock(); //critical section lock.unlock();
First a Lock
is created. Then it's lock()
method is called. Now the Lock
instance is locked. Any other thread calling lock()
will be blocked until the thread that locked
the lock calls unlock()
. Finally unlock()
is called, and the Lock
is
now unlocked so other threads can lock it.
Obviously all threads must share the same Lock instance. If each thread creates its own Lock instance, then they will be locking on different locks, and thus not be blocking each other from access. I will show you later in this Java Lock tutorial an example of how a shared Lock instance looks.
Fail-safe Lock and Unlock
If you look at the example in the previous section, imagine what happens if an exception is thrown between
the call to lock.lock()
and lock.unlock()
. The exception would interrupt the program
flow, and the call to lock.unlock()
would never be executed. The Lock would thus remain locked
forever.
To avoid exceptions locking a Lock forever, you should lock and unlock it from within a try-finally block, like this:
Lock lock = new ReentrantLock(); try{ lock.lock(); //critical section } finally { lock.unlock(); }
This way the Lock is unlocked even if an exception is thrown from inside the try-block.
Example Lock Protected Counter
To better understand how using a Lock looks different from using a synchronized block,
I have created two simple concurrent Counter classes which protects their internal count
in different ways. The first class uses a synchronized block, and the second class uses a Java Lock:
public class CounterSynchronized { private long count = 0; public synchronized void inc() { this.count++; } public synchronized long getCount() { return this.count; } }
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class CounterLock { private long count = 0; private Lock lock = new ReentrantLock(); public void inc() { try { lock.lock(); this.count++; } finally { lock.unlock(); } } public long getCount() { try { lock.lock(); return this.count; } finally { lock.unlock(); } } }
Notice that the CounterLock class is longer than the CounterSynchronized class. However, using a Java Lock to
guard the count
variable internally may provide higher degrees of flexibility, should you need it.
These simple examples don't really need it though - but more advanced counters might.
Lock Reentrance
A lock is called reentrant if the thread that holds the lock can lock it again. A non-reentrant lock is a lock which cannot be locked again if locked, not even by the thread that holds the lock. Non-reentrant locks may result in reentrance lockout which is a situation similar to a deadlock.
The ReentrantLock class is a reentrant lock. That means, that even if a thread holds the hold it can lock it again. Consequently the thread must unlock it as many times as it has locked it, in order to fully unlock the Reentrant lock for other threads.
A reentrant lock is useful in certain concurrent designs. Below is a concurrent implementation of a calculator. The calculator can hold the current result internally, and offers a set of methods that can perform calculations on that result.
public class Calculator { public static class Calculation { public static final int UNSPECIFIED = -1; public static final int ADDITION = 0; public static final int SUBTRACTION = 1; int type = UNSPECIFIED; public double value; public Calculation(int type, double value){ this.type = type; this.value = value; } } private double result = 0.0D; Lock lock = new ReentrantLock(); public void add(double value) { try { lock.lock(); this.result += value; } finally { lock.unlock(); } } public void subtract(double value) { try { lock.lock(); this.result -= value; } finally { lock.unlock(); } } public void calculate(Calculation ... calculations) { try { lock.lock(); for(Calculation calculation : calculations) { switch(calculation.type) { case Calculation.ADDITION : add (calculation.value); break; case Calculation.SUBTRACTION: subtract(calculation.value); break; } } } finally { lock.unlock(); } } }
Notice how the calculate()
method both locks the Calculator instance's Lock before performing
any calculations, and also call the add()
and subtract()
methods which also
locks the lock. Because the ReentrantLock is reentrant, this does not cause any problems. The thread calling
calculate
Lock Fairness
An unfair lock does not guarantee the order in which threads waiting to lock the lock will be given access to lock it. That means, that a waiting thread could risk waiting forever, if other threads keep trying to lock the lock, and are given priority over the waiting thread. This situation can lead to starvation. I cover starvation and fairness in more detail in my Starvation and Fairness Tutorial.
The ReentrantLock behaviour is unfair by default. However, you can tell it to operate in fair mode via
its constructor. The ReentrantLock class has a constructor that takes a boolean
parameter
specifying whether the ReentrantLock should provide fairness or not to waiting threads.
Here is an example of creating a ReentrantLock instance using fair mode:
ReentrantLock lock = new ReentrantLock(true);
Please note, that the method tryLock()
(covered later in this Java Lock tutorial) with no parameters
does not respect the fairness mode of the ReentrantLock. To get fairness you must use the
tryLock(long timeout, TimeUnit unit)
method instead, like this:
lock.tryLock(0, TimeUnit.SECONDS);
Lock and ReentrantLock Methods
The Java Lock interface contains the following primary methods:
- lock()
- lockInterruptibly()
- tryLock()
- tryLock(long timeout, TimeUnit timeUnit)
- unlock()
The Java ReentrantLock also has a few interesting public methods:
- getHoldCount()
- getQueueLength()
- hasQueuedThread(Thread)
- hasQueuedThreads()
- isFair()
- isHeldByCurrentThread()
- isLocked()
I will cover each of these methods in more detail in the following sections.
lock()
The lock()
method locks the Lock
instance if possible. If the Lock
instance is
already locked, the thread calling lock()
is blocked until the Lock
is unlocked.
lockInterruptibly()
The lockInterruptibly()
method locks the Lock
unless the thread calling the method has been interrupted.
Additionally, if a thread is blocked waiting to lock the Lock
via this method, and it is interrupted, it exits
this method calls.
tryLock()
The tryLock()
method attempts to lock the Lock
instance immediately. It returns true
if the locking succeeds, false if Lock
is already locked. This method never blocks.
trylock(long timeout, TimeUnit timeUnit)
The tryLock(long timeout, TimeUnit timeUnit)
works like the tryLock()
method, except
it waits up the given timeout before giving up trying to lock the Lock
.
unlock()
The unlock()
method unlocks the Lock
instance. Typically, a Lock
implementation
will only allow the thread that has locked the Lock
to call this method. Other threads calling this method
may result in an unchecked exception (RuntimeException
).
getHoldCount()
The Java ReentrantLock getHoldCount()
method returns the number of times a given thread has locked this Lock
instance. A thread can lock a Lock more than once due to Lock reentrance.
getQueueLength()
The ReentrantLock getQueueLength()
method returns the number of threads waiting to lock the Lock.
hasQueuedThread()
The ReentrantLock hasQueuedThread(Thread thread)
method takes a Thread as parameter and return true
if that Thread is queued up waiting to lock the Lock, and false
if not.
hasQueuedThreads()
The ReentrantLock hasQueuedThreads()
method returns true
if any threads are queued up waiting
to lock this Lock, and false
if not.
isFair()
The ReentrantLock isFair()
method returns true
if this Lock guarantees fairness among threads
waiting to lock it, and false
if not. See Lock Fairness for more information
about Lock fairness.
isHeldByCurrentThread()
The ReentrantLock isHeldByCurrentThread()
method returns true
if the Lock is held (locked) by
the thread calling isHeldByCurrentThread()
, and false
if not.
isLocked()
The ReentrantLock isLocked()
method returns true
if the Lock is currently locked, and
false
if not.
Tweet | |
Jakob Jenkov |