We have covered the basics of the synchronized keyword. The main idea is simple, but as often happens, the devil is in the details. There are some more complex cases that are important to understand. Let's look at them through examples.
A synchronized counter
Here is an example. It's a synchronized counter with two synchronized instance methods: increment and getValue.
class SynchronizedCounter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getValue() {
return count;
}
}When multiple threads invoke increment on the same instance, no issues arise because the synchronized keyword protects the shared field. Only one thread can change the field at a time. Other threads wait until the current thread releases the monitor. All changes to the variable count are visible.
The Java Language Specification guarantees that changes made by a thread are visible to other threads if they synchronize on the same monitor. Specifically, if a thread changes shared data (such as a variable) inside a synchronized block or method and releases the monitor, other threads can see all changes after acquiring the same monitor.
The method getValue doesn't modify the field; it only reads the current value. We synchronize this method so that the reading thread always gets the actual value. Without synchronization, there's no guarantee that the reading thread will see the updated value of count.
Here is a class called Worker that extends Thread. The class takes an instance of SynchronizedCounter and calls the method increment 10,000,000 times.
class Worker extends Thread {
private final SynchronizedCounter counter;
public Worker(SynchronizedCounter counter) {
this.counter = counter;
}
@Override
public void run() {
for (int i = 0; i < 10_000_000; i++) {
counter.increment();
}
}
}The following code creates an instance of SynchronizedCounter, starts threads, and prints the result.
SynchronizedCounter counter = new SynchronizedCounter();
Worker worker1 = new Worker(counter);
Worker worker2 = new Worker(counter);
worker1.start();
worker2.start();
worker1.join();
worker2.join();
System.out.println(counter.getValue()); // the result is 20_000_000Sometimes, you don't need to synchronize methods that only read shared data (including getters) in these cases:
When you're sure that the reading thread successfully returns from
joinon all writing threads before reading a field. This applies to the code above, so we can remove the synchronized keyword fromgetValue.
When a shared field uses the
volatilekeyword. In this case, you'll always see the actual value of this field.
Use caution when deciding not to synchronize read methods.
One monitor and multiple synchronized methods and blocks
Important: an object or a class that has only one monitor and only one thread can execute synchronized code on the same monitor.
It means that if a class has several synchronized instance methods and a thread invokes one of them, other threads cannot execute either of these methods on the same instance until the first thread releases the monitor of the instance.
Here is an example: a class with three instance methods. Two methods are synchronized and the third one has an internal synchronized block. Both methods and the block are synchronized on the monitor of this instance.
class SomeClass {
public synchronized void method1() {
// do something useful
}
public synchronized void method2() {
// do something useful
}
public void method3() {
synchronized (this) {
// do something useful
}
}
}If a thread invokes method1 and executes statements inside the method, no other thread can execute statements inside method2 or in the synchronized block in method3 because this monitor is already acquired. The threads will wait for the release of the monitor.
The same behavior is correct when a class monitor is used.
Reentrant synchronization
A thread cannot acquire a lock owned by another thread. But a thread can acquire a lock that it already owns. This behavior is called reentrant synchronization.
Take a look at the following example:
class SomeClass {
public static synchronized void method1() {
method2(); // legal invocation because a thread has acquired monitor of SomeClass
}
public static synchronized void method2() {
// do something useful
}
}The code above is correct. When a thread is inside method1 it can invoke method2 because both methods are synchronized on the same object (SomeClass.class).
Fine-grained synchronization
Sometimes a class has several fields that are never used together. It's possible to protect these fields by using the same monitor, but in this case, only one thread will be able to access one of these fields, despite their independence. To improve the concurrency rate it's possible to use an idiom with additional objects as monitors.
Here is an example: a class with two methods. The class stores the number of calls to each method in a special field.
class SomeClass {
private int numberOfCallingMethod1 = 0;
private int numberOfCallingMethod2 = 0;
private final Object lock1 = new Object(); // an object for locking
private final Object lock2 = new Object(); // another object for locking
public void method1() {
System.out.println("method1...");
synchronized (lock1) {
numberOfCallingMethod1++;
}
}
public void method2() {
System.out.println("method2...");
synchronized (lock2) {
numberOfCallingMethod2++;
}
}
}As you can see, the class has two additional fields that are the locks for separating monitors for each critical section.
If we have an instance of the class, one thread may work inside the synchronized block of the first method and, at the same time, another thread may work inside the synchronized block of the second method.
Synchronization and performance of programs
Remember, the code protected by the synchronization mechanism can be executed only by one thread at the same time. It reduces the parallelism and responsiveness of the program.
Do not synchronize all your code. Try to use synchronization only when it is really necessary. Determine small parts of the code to be synchronized. Sometimes it's better to use a synchronization block instead of synchronizing a whole method (if the method is complex)