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 thesynchronized
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:
Check out our hands-on, practical guide to learning Git, with best-practices, industry-accepted standards, and included cheat sheet. Stop Googling Git commands and actually learn it!
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.