Java Lock

Jakob Jenkov
Last update: 2021-01-04

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

Java Lock Tutorial Video

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 to lock() and unlock() 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.

Jakob Jenkov

Featured Videos

Java Generics

Java ForkJoinPool

P2P Networks Introduction



















Close TOC
All Tutorial Trails
All Trails
Table of contents (TOC) for this tutorial trail
Trail TOC
Table of contents (TOC) for this tutorial
Page TOC
Previous tutorial in this tutorial trail
Previous
Next tutorial in this tutorial trail
Next