How to use synchronized and reentrant lock in Java

In this post, we will discuss the uses of synchronized and reentrant lock in Java. Below are the points we are gonna cover.

The problem

Before jumping into the use of a synchronized or reentrant lock, first, let see what kind of problem it’s gonna solve. Here is a simple requirement we have, we have to write a method which increments the value of an int ‘a’. Refer the below code snippet.

static int a = 0;
static void add() {
   for (int i = 0; i < 10000; i++)
     a++;
}

But when we call the same method using two threads we are not getting the expected value of ‘a’. Here is the complete java source.

public class SynchronizedAndReentrantLockExample {
    static int a = 0;

    static void add() {
        for (int i = 0; i < 10000; i++)
            a++;
    }

    public static void main(String[] args) throws InterruptedException {

        Thread t1 = new Thread(() ->{
            add();
        });

        Thread t2 = new Thread(() ->{
            add();
        });

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println("Final value of a is "+a);
    }
}

The expected value of a is 20000 but after executing this it didn’t return that.

So the problem here is that at the same time when we call the add() method using two threads, one thread overwrites the value incremented by the other thread, and the reason behind that is the method add() can be executed by two threads at the same time.

Using synchronized to fix

We can fix the above problem using the synchronized keyword. Let’s add the synchronized keyword to the add() method. As soon as we make the add () method synchronized it starts working as expected.

static synchronized void add() {
  for (int i = 0; i < 10000; i++)
     a++;
}

So, once we make this method synchronized only one thread can execute this method at a time, hence it solves the problem we have. But is this is the only way can use synchronized? The answer is a big NO.

Instead of making the entire method synchronized, we can use a synchronized block. Refer to the updated version of the add() method.

static void add() {
   synchronized (SynchronizedAndReentrantLockExample.class) {
       for (int i = 0; i < 10000; i++)
	 a++;
   }
}

The difference between synchronized block and method is when we use the synchronized method the entire method is blocked, but by using a synchronized keyword if the method has code that is not inside the synchronized block it can be executed by other threads. That could be useful in some cases.

By using these two ways we can solve the problem, but it has some limitations as well. The major one is in both cases the lock will be acquired on the class, which means if two threads are running at the same time and the first thread that acquires the lock will block the second thread even if it is calling another synchronized method.

public class SynchronizedAndReentrantLockExample {
    static int a = 0;

    static synchronized void add() {
        System.out.println("incrementing values");
        for (int i = 0; i < 10000; i++)
            a++;
        System.out.println("method add done, going to sleep now ");
        try {
            Thread.sleep(4000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    static synchronized void print() {
        System.out.println("the value of a is "+a);
    }


    public static void main(String[] args) throws InterruptedException {

        Thread t1 = new Thread(() ->{
            add();
        });

        Thread t2 = new Thread(() ->{
            print();
            //add();
        });

        t1.start();
        t2.start();
    }
}
incrementing values
method add done, going to sleep now 
the value of a is 10000

Problem with the synchronized method and block

Here you can see the thread t1 call the add () method which increments the values and then sleeps for 4000 ms. But the thread t2 calls another synchronized method print () which has nothing to do with the add () method. But as thread t1 acquires the lock on the class it blocks thread t2 which is entirely independent of t1. This is the problem of using a class lock.

Remember: Every problem has a solution. So let’s fix the class lock problem. It can be fixed in a couple of ways. Let’s see how we can do that.

1. Using object lock.
2. By Using ReentrantLock.

Let’s modify the add () method and introduce an object lock.

final static Object lock = new Object();

static void add() {
 synchronized (lock) {
  for (int i = 0; i < 10000; i++)
    a++;
  }
}

By using lock objects, in the case of the class lock, the lock object should be static whereas in the case of object lock it will be non-static. The benefit of using a lock object is, it allows the other threads to execute the code which is independent of this method as opposed to the class lock which does acquire the lock in the entire class and block any other thread from executing even another synchronized method.

Using ReentrantLock

The same implementation as using object lock can be achieved using ReentrantLock, the inbuilt class in java.util.concurrency package which implements Lock and serializable interface.

public class ReentrantLock implements Lock, Serializable

Here how we can rewrite the add () method using ReentrantLock. We will create a Lock object by initiating ReentrantLock.

static Lock lock = new ReentrantLock();
static void add() {
  lock.lock();
    for (int i = 0; i < 10000; i++)
      a++;
  lock.unlock();
}

The benefit of using ReentrantLock is that we don’t have to handle the low-level problem of synchronization and all.

The important point while using a reentrant lock, the lock acquisitions should be wrapped in a try/finally block, to prevent the deadlock in case of an exception.

Difference between the synchronized and reentrant lock

A reentrant mutual exclusion Lock with the same basic behavior and semantics as the implicit monitor lock accessed using synchronized methods and statements, but with extended capabilities.

The ability of ReentrantLock to make the lock fair. Synchronized blocks are unfair. We can create the lock object by passing the value of fair as true/false in the constructor.

Lock lock = new ReentrantLock(true);

The fair locks favor granting access to the longest-waiting thread.

Recommended Read

An introduction to Multithreading

Happy Learning !!

Leave a Comment