JAVA / THREADS AND CONCURRENCY
synchronized blocks and choosing the lock object
Use synchronized blocks to guard exactly the statements that touch shared state, and pick a lock object that maps to the fields you are protecting.
What you will learn
- Guard state with a private final Object lock instead of this or a public field
- Keep I/O and pure computation outside the block; guard only the shared mutation
- Give independent fields independent locks so unrelated calls stop queueing up
- Never lock on a reassignable field, a String literal, or a boxed Integer
Understanding synchronized blocks and choosing the lock object
The block form takes an argument: synchronized (expr) { ... } evaluates expr to a reference and holds that object's monitor until the block exits. You are therefore making two separate decisions, which statements go inside and which object is the lock, and only the second one decides who excludes whom. Two threads block each other exactly when they acquire the same object's monitor; blocks on different objects run in parallel no matter which fields they touch, and blocks on the same object serialize even when they sit in unrelated classes.
Because exclusion follows object identity, a lock is really a name for a set of fields, and that mapping lives only in your head and your comments: neither the compiler nor the JVM checks that a block touches the fields its lock is supposed to guard. The default choice is a private final Object created in its declaration, private so no outside code can acquire it and final so the reference cannot be swapped while threads are inside. Locking on this compiles and appears in plenty of library code, but it publishes your monitor to every caller holding a reference to the instance, so foreign code can hold it for a long time or take part in a deadlock you cannot see by reading your own class.
Inside the block, keep only the reads and writes that must be atomic together; parsing, formatting, logging and anything that waits on I/O belong above or below it, because a thread holding the monitor while blocked on a socket makes every other caller wait for the socket too. The opposite error is splitting one invariant across two blocks: if two fields must agree, releasing the lock between the two writes lets another thread observe the half-updated pair. Fields that are genuinely unrelated, such as a hit counter and an audit log, deserve two locks, and when one method truly needs both, fix a single acquisition order and use it everywhere so two threads can never each hold what the other wants.
Reads need the same lock as writes, not because reading corrupts anything, but because a thread that never acquires the monitor has no guarantee it sees the last writer's value, and a getter that returns the live collection lets callers iterate it while another thread mutates it.
public class TwoLocks {
private final Object hitsLock = new Object(); // guards hits
private final Object missesLock = new Object(); // guards misses
private long hits;
private long misses;
void hit() {
synchronized (hitsLock) {
hits++;
}
}
void miss() {
synchronized (missesLock) {
misses++;
}
}
long hits() {
synchronized (hitsLock) {
return hits;
}
}
long misses() {
synchronized (missesLock) {
return misses;
}
}
public static void main(String[] args) throws InterruptedException {
TwoLocks stats = new TwoLocks();
Thread a = new Thread(() -> { for (int i = 0; i < 100_000; i++) stats.hit(); });
Thread b = new Thread(() -> { for (int i = 0; i < 100_000; i++) stats.hit(); });
Thread c = new Thread(() -> { for (int i = 0; i < 50_000; i++) stats.miss(); });
a.start();
b.start();
c.start();
a.join();
b.join();
c.join();
System.out.println("hits=" + stats.hits());
System.out.println("misses=" + stats.misses());
}
}A synchronized block protects fields only against threads that lock the very same object, so choosing the lock object is the real design decision.
Worked examples
Two classes, one accidental monitor
Shows how locking on a String literal makes unrelated classes contend on the same monitor.
public class SharedLiteralLock {
static class Downloader {
void run() throws InterruptedException {
synchronized ("LOCK") { // bug: literals are interned JVM-wide
System.out.println("downloader entered");
Thread.sleep(300);
System.out.println("downloader leaving");
}
}
}
static class Reporter {
void run() {
synchronized ("LOCK") { // same instance, not just an equal string
System.out.println("reporter entered");
System.out.println("reporter leaving");
}
}
}
public static void main(String[] args) throws InterruptedException {
Downloader downloader = new Downloader();
Reporter reporter = new Reporter();
Thread t1 = new Thread(() -> {
try {
downloader.run();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
Thread t2 = new Thread(reporter::run);
t1.start();
Thread.sleep(100); // reporter starts while downloader is inside
t2.start();
t1.join();
t2.join();
System.out.println("done");
}
}Example explained
Line 1Both classes write synchronized ("LOCK"), and equal string literals are interned to a single instance, so both acquire one monitor.
Line 2Thread.sleep(100) only staggers the start; the reporter is then blocked for the remaining 200 ms even though it shares no data with the downloader.
Line 3Neither class references the other, so this contention is invisible in either file; Integer.valueOf(1), Boolean.TRUE and other cached boxes have the same problem.
Line 4Giving each class its own private final Object lock makes the two blocks independent again.
A lock field that moves
Proves that a non-final lock field can be reassigned mid-block, letting two threads run the same guarded region at once.
public class MovingLock {
private static Object lock = new Object(); // bug: not final
public static void main(String[] args) throws InterruptedException {
Object old = lock;
Thread t;
synchronized (old) { // main holds the original monitor
lock = new Object(); // later blocks target a new one
t = new Thread(() -> {
synchronized (lock) { // does not wait for main
System.out.println("second thread inside the block");
}
});
t.start();
t.join();
System.out.println("main still holds the old monitor");
}
System.out.println("mutual exclusion was lost");
}
}Example explained
Line 1Object old = lock captures the monitor main is about to hold, because the field is about to point elsewhere.
Line 2lock = new Object() compiles only because the field is not final; every subsequent synchronized (lock) now names a different monitor.
Line 3t.join() returns while main is still inside its block, which is the proof that both threads were in the guarded region simultaneously.
Line 4Declaring private static final Object lock = new Object() turns the reassignment into a compile error, which is exactly why lock fields are final.
Only the mutation goes inside
Narrows the block to the map update and hands out a copy taken under the lock.
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;
public class NarrowBlock {
private final Object lock = new Object(); // guards counts
private final Map<String, Integer> counts = new HashMap<>();
void record(String raw) {
String key = raw.trim().toLowerCase(); // no shared state: stay outside
synchronized (lock) {
counts.merge(key, 1, Integer::sum); // read-modify-write: must be inside
}
}
Map<String, Integer> snapshot() {
synchronized (lock) {
return new TreeMap<>(counts); // copy while holding the lock
}
}
public static void main(String[] args) throws InterruptedException {
NarrowBlock tags = new NarrowBlock();
Runnable job = () -> {
for (int i = 0; i < 1000; i++) {
tags.record(" Java ");
tags.record("SQL");
}
};
Thread t1 = new Thread(job);
Thread t2 = new Thread(job);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(tags.snapshot());
}
}Example explained
Line 1trim() and toLowerCase() allocate and copy characters but touch nothing shared, so doing them before the block keeps the critical section to a single statement.
Line 2counts.merge reads the current count, adds one and stores it back; that sequence must be atomic, so it stays inside the block.
Line 3snapshot() acquires the same lock and returns a TreeMap copy, so a caller cannot iterate the live HashMap while another thread mutates it.
Line 4Returning counts directly instead of the copy would hand out unguarded state and reintroduce the race the lock was added to prevent.
Important notes
The monitor is released on every exit from the block, including a return or a thrown exception, so no finally is needed; but an exception can leave the guarded fields half-updated for the next thread to see.
synchronized (expr) throws NullPointerException when expr evaluates to null, so initialize lock fields in their declaration rather than lazily.
Common mistakes
Locking on a fresh object per call, such as synchronized (new Object()) or a lock declared inside the method: each thread takes its own monitor, the code compiles and looks synchronized, and updates are still lost.
Locking on the field being replaced, as in synchronized (items) { items = new ArrayList<>(); }: after the swap, later callers acquire a different monitor and two threads run the block at the same time.
Locking on a String literal or a boxed value like Integer.valueOf(count): literals are interned and small Integers are cached, so unrelated classes silently share one monitor, and a boxed counter changes identity as its value changes, ending exclusion altogether.
Try it yourself
Change, predict, then run
Take a class with two independent counters guarded by one synchronized method, give each counter its own private final Object lock and the narrowest possible block, and move the argument formatting outside both blocks. Run three threads that hammer both counters and confirm after joining that each total is exact.
Open the Java workspaceCheck your understanding
A class has a private int count. increment() does synchronized (this) { count++; } and decrement() does synchronized (countLock) { count--; }, where countLock is a private final Object. What happens when two threads call them concurrently?
- Both threads can be inside their block at once, so count++ and count-- interleave and updates are lost
- It is safe, because every access to count happens inside some synchronized block
- decrement() throws IllegalMonitorStateException because countLock is not the receiver
- It does not compile, because a field may be guarded by only one lock
Show answer
Mutual exclusion holds only between blocks that acquire the same monitor, and this and countLock are two distinct objects, so neither thread ever waits and the read-modify-write on count can interleave. The second option is the tempting one, but synchronized is not a property of the code: nothing in the compiler or the JVM checks which fields a block touches, so being 'inside some block' guarantees nothing. IllegalMonitorStateException comes from calling wait or notify without owning that object's monitor, not from locking on something other than this.