Java programming language logo

Reentrant Lock

The simplest way to achieve thread synchronization in Java is making the code synchronized. This solution is simple, but it has many limitations. The java.util.concurrent.locks package provides tools for a more flexible synchronization by Lock objects.

Behind the scenes, synchronized code uses reentrant lock. When a thread needs to invoke a synchronized method of an object, it has to own the lock of that object instance during the invocation, and only one thread can own the lock at a same time. This is done by Java automatically. Lock objects use the same logic. Only one thread can own the Lock object at a same time.

The main benefits of Lock over synchronization:
- Synchronization don’t deal with the order of the waiting threads. Maybe some threads can execute the synchronized code frequently with minimal blocking, while other threads are blocked for long periods or maybe for forever, which leads to the starvation problem. Lock objects give solution to this. They can observe the order of the threads for the locking requests.
- Synchronization blocks the thread until it can enter to the synchronized block, which maybe makes the thread waiting forever. Lock objects can handle this situation and provide a way for the thread to proceed if it cannot get access to the protected code.
- The synchronized keyword can only be used within a single method. The lock and unlock methods of the Lock objects can be called from different methods.

The ReentrantLock is an implementation of the Lock interface, and it proves the same behavior as the implicit lock used by synchronized methods and statements, but with extended capabilities. Let’s see some examples for the ReentrantLock object.

The lock Method

In the below example, the Printer class provides a print method for printing out a message. It needs one second to get this done, and only one thread can execute it at a time because of the lock.

The lock method locks the Lock instance if possible. If the Lock instance is already locked by another thread, the thread calling lock is blocked at this line until the Lock gets unlocked.

Recommended to call the unlock method from a finally block, which guarantees, the lock will always be unlocked.


package com.programcodex.concurrency.lock.simple;

import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Printer {

    private final Lock lock = new ReentrantLock();

    public void print(String message) {
        lock.lock();
        try {
            doPrint(message);
        } finally {
            lock.unlock();
        }
    }

    private void doPrint(String message) {
        DateTimeFormatter dtf = DateTimeFormatter.ofPattern("HH:mm:ss");
        String time = dtf.format(LocalTime.now());
        System.out.println(time + " Successfully printed: " + message);

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

In the main method of the TestLock class, a Printer is instantiated and passed to three Runnable objects. Each Runnable prints out a message. Three Thread instances are started, but each thread needs one second to get done. As you can see from the output, the total run-time of the threads is three seconds because they can only run one after another.


package com.programcodex.concurrency.lock.simple;

public class TestLock {

    public static void main(String[] args) {
        Printer printer = new Printer();

        Runnable r1 = newRunnable(printer, "1st message.");
        Runnable r2 = newRunnable(printer, "2nd message.");
        Runnable r3 = newRunnable(printer, "3rd message.");

        new Thread(r1).start();
        new Thread(r2).start();
        new Thread(r3).start();
    }

    private static Runnable newRunnable(Printer printer, String msg) {
        return () -> {
            printer.print(msg);
        };
    }
}

The output of the above class:

ReentrantLock lock method

The tryLock Method

In the previous Printer class, the threads are blocked until they cannot get the lock. This can lead to problems like deadlock or starvation. To avoid these problems, Lock interface provides a tryLock method, which tries to get lock only if it is free. It returns with true if the locking succeeds. If the lock is already locked by another thread, it returns with false. Depending on the return value, the thread has options how to proceed. This method never blocks.

Let’s alter the print method of the Printer class. Now the locking is called in an if statement. If the tryLock returns with true, it can print out the message. If it returns with false, it only prints out an error message.


package com.programcodex.concurrency.lock.trylock;

import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Printer {

    private final Lock lock = new ReentrantLock();

    public void print(String message) {
        if (lock.tryLock()) {
            try {
                doPrint(message);
            } finally {
                lock.unlock();
            }
        } else {
            System.out.println("ERROR: This message can't be printed out: "
                    + message);
        }
    }

    private void doPrint(String message) {
        DateTimeFormatter dtf = DateTimeFormatter.ofPattern("HH:mm:ss");
        String time = dtf.format(LocalTime.now());
        System.out.println(time + " Successfully printed: " + message);

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

If we replace the Printer in the previous TestLock class with this new Printer, the output modifies. Only one thread can get the lock, the other two not blocked, they skip the printing.

ReentrantLock tryLock method

The tryLock Method with Timeout

There is an overloaded version of tryLock method, that takes two parameters. First is the maximum time to wait for the lock, the second is the time unit of the first parameter. This method works like the tryLock method, except if the lock is not free, it waits for it for a while before giving up the lock acquisition. Moreover, this method can throw InterruptedException when the thread is interrupted while acquiring the lock.

The below print method of the Printer class uses this kind of tryLock method. It waits maximum 1500 milliseconds for the lock. If the current thread manages to get the lock before the timeout, the lockFlag will be true, and the message gets printed out. Otherwise, the lockFlag will be false, and only an error message gets printed out. In the finally block, the unlock method is called only if the lock is locked.


package com.programcodex.concurrency.lock.trylocktimeout;

import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Printer {

    private final Lock lock = new ReentrantLock();

    public void print(String message) {
        boolean lockFlag = false;

        try {
            lockFlag = lock.tryLock(1500, TimeUnit.MILLISECONDS);
            if (lockFlag) {
                doPrint(message);
            } else {
                System.out.println("ERROR: This message can't be printed out: "
                        + message);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            if (lockFlag) {
                lock.unlock();
            }
        }
    }

    private void doPrint(String message) {
        DateTimeFormatter dtf = DateTimeFormatter.ofPattern("HH:mm:ss");
        String time = dtf.format(LocalTime.now());
        System.out.println(time + " Successfully printed: " + message);

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

Let’s use the TestLock class with this Printer. Three threads are started at the same time. One of them can get the lock and prints out the message in the first second. The other two have to wait for the lock. After one second, the first thread is done, and a second one can get the lock and prints out the next message. When the second thread finishes, two seconds have elapsed, but the timeout was one and a half second. So the third thread had given up the waiting before the second thread finished.

The output of the TestLock class:

ReentrantLock tryLock method with timeout

The static Lock

Next Printer version is actually the same as the first version, except the lock field is declared to static. In this case, if there are more than one Printer instance, and different threads call printer methods of different Printer instances, only one print method can get the lock at a same time.


package com.programcodex.concurrency.lock.staticc;

import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Printer {

    private static final Lock lock = new ReentrantLock();

    public void print(String message) {
        lock.lock();
        try {
            doPrint(message);
        } finally {
            lock.unlock();
        }
    }

    private void doPrint(String message) {
        DateTimeFormatter dtf = DateTimeFormatter.ofPattern("HH:mm:ss");
        String time = dtf.format(LocalTime.now());
        System.out.println(time + " Successfully printed: " + message);

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

The TestStaticLock class creates three Printer instances and uses them in three different threads. As you can see from the output, the messages are printed out one after another, regardless of there are three different Printer instances.


package com.programcodex.concurrency.lock.staticc;

public class TestStaticLock {

    public static void main(String[] args) {
        Printer pr1 = new Printer();
        Printer pr2 = new Printer();
        Printer pr3 = new Printer();

        Runnable r1 = newRunnable(pr1, "1st message.");
        Runnable r2 = newRunnable(pr2, "2nd message.");
        Runnable r3 = newRunnable(pr3, "3rd message.");

        new Thread(r1).start();
        new Thread(r2).start();
        new Thread(r3).start();
    }

    private static Runnable newRunnable(Printer printer, String msg) {
        return () -> {
            printer.print(msg);
        };
    }
}

The output of the above class:

static ReentrantLock