Lesson 7 | Thread Synchronization |
Objective | Understand how synchronization ensures correctness in concurrent code, when to use it, and modern alternatives that reduce contention. |
synchronized
actually doesImportant: Synchronization doesn’t make code “uninterruptible.” An interrupt does not force a monitor release.
Threads blocked trying to enter a monitor are not made to throw InterruptedException
, but once they acquire it, normal rules apply.
// Method-level: monitor is 'this' (or the Class object if static)
public synchronized void increment() { count++; }
// Block-level: pick a specific lock to narrow scope and reduce contention
private final Object lock = new Object();
public void incrementSafely() {
synchronized (lock) {
count++;
}
}
Prefer block-level synchronization to keep critical sections small, and synchronize on a private final lock to avoid accidental external locking.
AtomicInteger
, ReentrantLock
, ReentrantReadWriteLock
,
ConcurrentHashMap
, and immutability.
// UNSAFE: no synchronization
class Counter {
int value = 0;
void increment() { value++; } // data race: read-modify-write is not atomic
}
// FIX 1: synchronized block
class SyncCounter {
private int value = 0;
private final Object lock = new Object();
void increment() { synchronized (lock) { value++; } }
int get() { synchronized (lock) { return value; } }
}
// FIX 2: lock-free atomic
import java.util.concurrent.atomic.AtomicInteger;
class AtomicCounter {
private final AtomicInteger value = new AtomicInteger();
void increment() { value.incrementAndGet(); }
int get() { return value.get(); }
}
The original example started two threads with the same name but didn’t show they were distinct.
Print the id and name, and join()
so the main thread waits for both to finish.
class SimpleTask implements Runnable {
private final String label;
SimpleTask(String label) { this.label = label; }
@Override public void run() {
for (int i = 0; i < 10; i++) {
Thread t = Thread.currentThread();
System.out.println(i + " | id=" + t.getId() + " | name=" + t.getName() + " | " + label);
try {
Thread.sleep((long)(Math.random() * 300));
} catch (InterruptedException ignored) {}
}
System.out.println("DONE | id=" + Thread.currentThread().getId() + " | " + label);
}
}
public class TwoThreadsDemo {
public static void main(String[] args) throws InterruptedException {
Thread a = new Thread(new SimpleTask("worker A"), "Java Programmer #1");
Thread b = new Thread(new SimpleTask("worker B"), "Java Programmer #2");
a.start();
b.start();
a.join();
b.join();
System.out.println("Both threads finished.");
}
}
What you’ll see: interleaved lines containing two different ids and names, then two DONE
lines, and finally
“Both threads finished.” This demonstrates two separate threads executing.
synchronized
is clear, correct, and fast enough.ReentrantReadWriteLock
or StampedLock
.AtomicInteger
, AtomicReference
, with CAS operations.ConcurrentHashMap
, LinkedBlockingQueue
).ExecutorService
, CompletableFuture
. With virtual threads (Java 21+),
you can block more naturally, but still design to minimize shared mutable state.Question: What happens when you compile and run the updated example that starts two threads with distinct names?
Runnable
must have a no-args constructor.run()
executes once.RuntimeException
due to duplicate names.Answer: c
Thread
instances execute the Runnable
independently; no overwriting occurs.join()
ensures the main thread waits for both to complete, so you see two DONE
lines.