Understanding Java synchronized: Preventing Thread‑Unsafe Dirty Reads
This article explains how unsynchronized access to instance variables can cause dirty reads in multithreaded Java programs, demonstrates the issue with sample code, shows how adding the synchronized keyword resolves it, and explores the behavior when multiple objects each hold their own lock.
Non‑thread‑safe scenario and dirty reads
When multiple threads concurrently read and write the same instance variable without proper synchronization, a dirty read can occur: a thread may see a value that has been partially updated by another thread.
Example without synchronization
public class HasSelfPrivateNum {
private int num = 0; // member variable
public void add(String username) {
try {
if (username.equals("a")) {
num = 100;
System.out.println("a setover");
Thread.sleep(2000);
} else {
num = 200;
System.out.println("b setover");
}
System.out.println("username=" + username + ", num=" + num);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}Running two threads that invoke add("a") and add("b") on the same object produces interleaved output such as:
a set over
b set over
username=b, num=200
username=a, num=200The second thread reads the value written by the first thread, demonstrating a dirty read.
Adding synchronized to make the method thread‑safe
public class HasSelfPrivateNum {
private int num = 0;
synchronized public void add(String username) { // synchronized method
try {
if (username.equals("a")) {
num = 100;
System.out.println("a setover");
Thread.sleep(2000);
} else {
num = 200;
System.out.println("b setover");
}
System.out.println("username=" + username + ", num=" + num);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}With the synchronized keyword, the method acquires the object's lock, ensuring exclusive access. The output now shows ordered execution:
a set over
username=a, num=100
b set over
username=b, num=200Multiple objects, each with its own lock
public class ThreadA extends Thread {
private HasSelfPrivateNum numRef;
public ThreadA(HasSelfPrivateNum numRef) {
super();
this.numRef = numRef;
}
@Override
public void run() {
super.run();
numRef.add("a");
}
}
public class ThreadB extends Thread {
private HasSelfPrivateNum numRef;
public ThreadB(HasSelfPrivateNum numRef) {
super();
this.numRef = numRef;
}
@Override
public void run() {
super.run();
numRef.add("b");
}
}
public class Run {
public static void main(String[] args) {
HasSelfPrivateNum numRfA = new HasSelfPrivateNum();
HasSelfPrivateNum numRfB = new HasSelfPrivateNum();
ThreadA threadA = new ThreadA(numRfA);
threadA.start();
ThreadB threadB = new ThreadB(numRfB);
threadB.start();
}
}Because each thread works on a different instance, each synchronized method locks a different object. The execution interleaves, producing output such as:
a set over
b set over
username=b, num=200
username=a, num=100This demonstrates that synchronized acquires an object lock, not a global lock; only threads accessing the same object are serialized.
Key takeaway
The synchronized keyword provides mutual exclusion on a per‑object basis. To avoid dirty reads, ensure that all threads share the same object when protecting critical sections, or use other concurrency utilities for finer‑grained control.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
