Synchronized Keyword in Java

Introduction

This is the second article in the series of articles on Concurrency in Java. In the previous article, we learnt about the Executor pool and various categories of Executors in Java.

In this article, we will learn what the synchronized keyword is and how we can use that in a multi-threading environment.

What is Synchronization?

In a multi-threaded environment, it is possible that more than one thread may try to access the same resource. For example, two threads trying to write in to the same text file. In the absence of any synchronization between them, it is possible that the data written to the file will be corrupt when two or more threads have write access to the same file.

Also, in the JVM, each thread stores a local copy of variables on its stack. The actual value of these variables may be changed by some other thread. But that value may not be refreshed in another thread's local copy. This may cause incorrect execution of programs and non-deterministic behavior.

To avoid such issues, Java provides us with the synchronized keyword, which acts like a lock to a particular resource. This helps achieve communication between threads such that only one thread accesses the synchronized resource and other threads wait for the resource to become free.

The synchronized keyword can be used in a few different ways, like a synchronized block:

synchronized (someObject) {
    // Thread-safe code here
}

It can also be used with a method like this:

public synchronized void somemMethod() {
    // Thread-safe code here
}

How Synchronization Works in the JVM

When a thread tries to enter the synchronized block or method, it has to acquire a lock on the object being synchronized. One and only one thread can acquire that lock at a time and execute code in that block.

If another thread tries to access a synchronized block before the current thread completes its execution of the block, it has to wait. When the current thread exits the block, the lock is automatically released and any waiting thread can acquire that lock and enter the synchronized block:

  • For a synchronized block, the lock is acquired on the object specified in the parentheses after the synchronized keyword
  • For a synchronized static method, the lock is acquired on the .class object
  • For a synchronized instance method, the lock is acquired on the current instance of that class i.e. this instance

Synchronized Methods

Defining synchronized methods is as easy as simply including the keyword before the return type. Let's define a method that prints out the numbers between 1 and 5 in a sequential manner.

Two threads will try to access this method, so let's first see how this'll end up without synchronizing them, and then we'll lock the shared object and see what happens:

public class NonSynchronizedMethod {

    public void printNumbers() {
        System.out.println("Starting to print Numbers for " + Thread.currentThread().getName());

        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
        }

        System.out.println("Completed printing Numbers for " + Thread.currentThread().getName());
    }
}

Now, let's implement two custom threads that access this object and wish to run the printNumbers() method:

class ThreadOne extends Thread {

    NonSynchronizedMethod nonSynchronizedMethod;

    public ThreadOne(NonSynchronizedMethod nonSynchronizedMethod) {
        this.nonSynchronizedMethod = nonSynchronizedMethod;
    }

    @Override
    public void run() {
        nonSynchronizedMethod.printNumbers();
    }
}

class ThreadTwo extends Thread {

    NonSynchronizedMethod nonSynchronizedMethod;

    public ThreadTwo(NonSynchronizedMethod nonSynchronizedMethod) {
        this.nonSynchronizedMethod = nonSynchronizedMethod;
    }

    @Override
    public void run() {
        nonSynchronizedMethod.printNumbers();
    }
}

These threads share a common object NonSynchronizedMethod and they will simultaneously try to call the non-synchronized method printNumbers() on this object.

To test this behavior, let's write a main class:

public class TestSynchronization {
    public static void main(String[] args) {

        NonSynchronizedMethod nonSynchronizedMethod = new NonSynchronizedMethod();

        ThreadOne threadOne = new ThreadOne(nonSynchronizedMethod);
        threadOne.setName("ThreadOne");

        ThreadTwo threadTwo = new ThreadTwo(nonSynchronizedMethod);
        threadTwo.setName("ThreadTwo");

        threadOne.start();
        threadTwo.start();

    }
}

Running the code will give us something along the lines of:

Starting to print Numbers for ThreadOne
Starting to print Numbers for ThreadTwo
ThreadTwo 0
ThreadTwo 1
ThreadTwo 2
ThreadTwo 3
ThreadTwo 4
Completed printing Numbers for ThreadTwo
ThreadOne 0
ThreadOne 1
ThreadOne 2
ThreadOne 3
ThreadOne 4
Completed printing Numbers for ThreadOne

ThreadOne started first, though ThreadTwo completed first.

And running it again greets us with another undesired output:

Starting to print Numbers for ThreadOne
Starting to print Numbers for ThreadTwo
ThreadOne 0
ThreadTwo 0
ThreadOne 1
ThreadTwo 1
ThreadOne 2
ThreadTwo 2
ThreadOne 3
ThreadOne 4
ThreadTwo 3
Completed printing Numbers for ThreadOne
ThreadTwo 4
Completed printing Numbers for ThreadTwo

These outputs are given completely to chance, and are completely unpredictable. Each run will give us a different output. Factor this in with the fact that there can be many more threads, and we could have a problem. In real-world scenarios this is especially important to consider when accessing some type of shared resource, like a file or other type of IO, as opposed to just printing to the console.

Now, let's adequately synchronize our method:

public synchronized void printNumbers() {
    System.out.println("Starting to print Numbers for " + Thread.currentThread().getName());

    for (int i = 0; i < 5; i++) {
        System.out.println(Thread.currentThread().getName() + " " + i);
    }

    System.out.println("Completed printing Numbers for " + Thread.currentThread().getName());
}

Absolutely nothing has changed, besides including the synchronized keyword. Now, when we run the code:

Starting to print Numbers for ThreadOne
ThreadOne 0
ThreadOne 1
ThreadOne 2
ThreadOne 3
ThreadOne 4
Completed printing Numbers for ThreadOne
Starting to print Numbers for ThreadTwo
ThreadTwo 0
ThreadTwo 1
ThreadTwo 2
ThreadTwo 3
ThreadTwo 4
Completed printing Numbers for ThreadTwo

This looks about right.

Here, we see that even though the two threads run simultaneously, only one of the threads enters the synchronized method at a time which in this case is ThreadOne.

Once it completes execution, ThreadTwo can starts with the execution of the printNumbers() method.

Synchronized Blocks

The main aim of multi-threading is to execute as many tasks in parallel as is possible. However, synchronization throttles the parallelism for threads which have to execute synchronized method or block.

This reduces the throughput and parallel execution capacity of the application. This downside cannot be entirely avoided due to shared resources.

However, we can try to reduc the amount of code to be executed in a synchronized fashion by keeping the least amount of code as possible in the scope of synchronized. There could be many scenarios where instead of synchronizing on the whole method, it is okay to just synchronize a few lines of code in the method instead.

We can use the synchronized block to enclose only that portion of code instead of the whole method.

Since there is less amount of code to be executed inside the synchronized block, the lock is released by each of the threads more quickly. As a result, the other threads spend less time waiting for the lock and code throughput increases greatly.

Let's modify the earlier example to synchronize only the for loop printing the sequence of numbers, as realistically, it's the only portion of code that should be synchronized in our example:

public class SynchronizedBlockExample {

    public void printNumbers() {

        System.out.println("Starting to print Numbers for " + Thread.currentThread().getName());

        synchronized (this) {
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName() + " " + i);
            }
        }

        System.out.println("Completed printing Numbers for " + Thread.currentThread().getName());
    }
}

Let's check out the output now:

Starting to print Numbers for ThreadOne
Starting to print Numbers for ThreadTwo
ThreadOne 0
ThreadOne 1
ThreadOne 2
ThreadOne 3
ThreadOne 4
Completed printing Numbers for ThreadOne
ThreadTwo 0
ThreadTwo 1
ThreadTwo 2
ThreadTwo 3
ThreadTwo 4
Completed printing Numbers for ThreadTwo

Although it may seem alarming that ThreadTwo has "started" printing numbers before ThreadOne completed its task, this is only because we allowed the thread to reach past the System.out.println(Starting to print Numbers for ThreadTwo) statement before stopping ThreadTwo with the lock.

That's fine because we just wanted to synchronize the sequence of the numbers in each thread. We can clearly see that the two threads are printing numbers in the correct sequence by just synchronizing the for loop.

Conclusion

In this example we saw how we can use synchronized keyword in Java to achieve synchronization between multiple threads. We also learnt when we can use synchronized method and blocks with examples.

As always, you can find the code used in this example here.

Author image
Mumbai Website
Chandan is a passionate software engineer with extensive experience in designing and developing Java applications. In free time, he likes to read fiction and write about his experiences.