The synchronization brings together the mechanisms that coordinate the access of multiple threads to shared state.
In the previous article, we learned how to create threads to perform several tasks concurrently.
If those threads are independent, they don’t share this problem. When two threads access the same state and at least one writes, we need to coordinate that access.
Welcome to the world of Race Conditions.
The problem: race conditions
Imagine a simple counter. We want two threads to add 1,000 visits each to a website. The final result should be 2,000.
class Counter {
private int count = 0;
public void increment() {
count++; // The danger is here
}
public int getCount() { return count; }
}
public class Main {
public static void main(String[] args) throws InterruptedException {
Counter c = new Counter();
// Thread 1: Adds 1000 times
Thread t1 = new Thread(() -> {
for(int i=0; i<1000; i++) c.increment();
});
// Thread 2: Adds 1000 times
Thread t2 = new Thread(() -> {
for(int i=0; i<1000; i++) c.increment();
});
t1.start();
t2.start();
t1.join(); // Wait for them to finish
t2.join();
System.out.println("Total visits: " + c.getCount());
}
}If you run this, you would expect to see 2000.
You might see 2000 or a lower value, like 1950, 1890, or 1999. The result is not guaranteed and can change between executions.
Why does it fail? (atomicity)
The problem is that the count++ operation is NOT atomic. For the CPU, adding 1 involves three distinct steps:
- Read the current value from memory (e.g., 10).
- Add 1 to the value (10 + 1 = 11).
- Write the new value to memory (11).
If two threads enter at the same time, disaster occurs:
- Thread A reads 10.
- Thread B reads 10 (before A writes).
- Thread A adds and writes 11.
- Thread B adds (based on its 10) and writes 11.
Result: They performed two additions, but the counter only increased by 1. We “lost” one visit.
The solution: synchronized
To fix it, we turn increment() into a critical section. Only one thread will be able to modify the count with the same lock at any one time, and the changes will have the visibility defined by the memory model.
In Java, this is achieved with the keyword synchronized.
class SafeCounter {
private int count = 0;
// Only one thread can execute this method on the same instance at any one time
public synchronized void increment() {
count++;
}
public int getCount() { return count; }
}By adding synchronized, Java creates a Monitor Lock associated with the object.
synchronized blocks (fine-grained synchronization)
Synchronizing an entire method is easy, but sometimes it’s inefficient. If your method takes 5 seconds to execute, you paralyze all other threads for 5 seconds.
Sometimes you only want to protect a specific line. You can use a Synchronized Block.
public void complexMethod() {
System.out.println("Preparing data..."); // This can be done in parallel
// We only block access to the shared variable
// 'this' is the object we use as a key
synchronized(this) {
count++;
}
System.out.println("Saving logs..."); // This can also be done in parallel
}This improves performance because the “lock” lasts the minimum amount of time.
The cost of synchronization
You might think: “Well, I’ll put synchronized on all my methods and be safe.”
Bad idea.
- Performance: Acquiring and releasing locks consumes CPU. Moreover, you convert your multithreaded program into a sequential one (if everyone queues up, no one works in parallel).
- Deadlocks: If Thread A holds the key to resource 1 and waits for resource 2, and Thread B holds the key to resource 2 and waits for resource 1… both freeze forever. The program hangs.
Modern alternatives: lock-free atomicity
For simple cases like counters, Java includes classes in java.util.concurrent.atomic that offer atomic operations without using a monitor. Their performance depends on contention; they are not universally faster than synchronized.
import java.util.concurrent.atomic.AtomicInteger;
class AtomicCounter {
// Special thread-safe variable
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
// Equivalent to count++ but it's atomic and safe
count.incrementAndGet();
}
}