Contents

Java ReentrantLock with examples

Written by: David Vlijmincx

Introduction

In this post, we'll take a quick look at how to use locks in Java, so you can start using them in your code. We'll cover the Lock interface, an excellent synchronization mechanism that will help you make your code more reliable. Plus, we'll explore what happens if you try to acquire a lock you already own, so you can avoid some common pitfalls. Let's get started!

Lock in Java

Using a lock in Java is very straightforward. To implement a lock, you usually need to perform these steps:

  • Create a Lock
  • Give each thread access to the same lock
  • Acquire the lock
  • Release the lock when done

In the following example, we implement the first two steps. At Line 5, we create a lock that the Car class uses to synchronize the threads. Each thread must use the same lock because you can only synchronize threads that have a reference to the same Lock object. Because both threads use the same car object, they also use the same lock.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class UsingLocks {
    public static void main(String[] args) {

        // Creating a lock for the car class to use
        Lock lock = new ReentrantLock();

        // Pass the lock to the car class
        // This way, every thread uses the same lock
        Car car = new Car(lock);

        // Implementing the functional interface inside the
        Thread t1 = new Thread(() -> {
            car.lockedMethod();
        });

        // using a method reference
        Thread t2 = new Thread(car::lockedMethod);

        // Start both threads
        t1.start();
        t2.start();
    }
}

The last two steps are to acquire the lock using the lock reference and to release it when we are done. Acquiring and releasing the lock is done inside the lockedMethod.

As you can see, the lock is acquired inside a try-finally. We use the try-finally because if an exception happens while we have acquired the lock, it will always be released. So, for example, if your code throws an exception on line 12, the finally will make sure that the lock is released again so other threads can acquire the lock.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
class Car {
    Lock lock;

    public Car(Lock lock) {
        this.lock = lock;
    }

    public void lockedMethod() {

        try {
            lock.lock();
            // Code that will run only on the thread
            // with that holds the lock
        } finally {
            lock.unlock();
        }
    }
}

Acquiring the same Lock multiple times

One thing to keep in mind when using a lock is that you need to release it as many times as you have acquired it. In the following example, we acquire the lock three times, but we also release it three times. If you only release the lock twice, another thread won't be able to acquire the lock.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public void lockedMethod() {

        try {
            lock.lock();
            try {
                lock.lock();
                try {
                    lock.lock();
                    // Code behind three locks
                } finally {
                    lock.unlock();
                }
            } finally {
                lock.unlock();
            }
        } finally {
            lock.unlock();
        }
    }

Conclusion

Locks are an essential tool for Java programmers who want to ensure the smooth and efficient execution of their code. By using the Lock interface, you can synchronize threads and prevent conflicts that could cause bugs or other issues. In this post, we looked at implementing a lock using Java in four steps. We also saw what happens when you acquire a lock multiple times.

Further reading

More about locks and multithreading in Java: