Why Your Java Producer‑Consumer Code Deadlocks and How to Fix It
This article explains the root causes of thread‑safety problems in Java producer‑consumer scenarios, demonstrates how race conditions and deadlocks arise, and provides step‑by‑step solutions using synchronized blocks, wait/notify, notifyAll, and explicit Lock with Condition objects.
Introduction
Thread safety in Java requires careful coordination when multiple threads share mutable objects. The classic producer‑consumer pattern illustrates the pitfalls of unsynchronised access and the mechanisms ( synchronized, wait / notify, Lock / Condition) that can be used to guarantee correct behaviour.
1. Producer‑Consumer Race Conditions
1.1 Problem illustration
A shared Resource object holds id, name and wheelNumber. Two threads continuously write and read this object:
public class Resource {
private int id;
private String name;
private int wheelNumber;
private boolean flag = false; // used for coordination
// getters, setters, toString omitted
}
public class Input implements Runnable {
private final Resource r;
public Input(Resource r) { this.r = r; }
public void run() {
for (int i = 0; ; i++) {
if (i % 2 == 0) {
r.setId(i);
r.setName("小汽车");
r.setWheelNumber(4);
} else {
r.setId(i);
r.setName("电动车");
r.setWheelNumber(2);
}
}
}
}
public class Output implements Runnable {
private final Resource r;
public Output(Resource r) { this.r = r; }
public void run() {
for (;;) {
System.out.println(r.toString());
}
}
}Without coordination the producer may be pre‑empted after setting some fields, allowing the consumer to read a partially updated object. This leads to missing wheels or duplicated consumption.
1.2 Solutions
Synchronized block – guard the whole production and consumption steps with a lock on the shared object:
synchronized (r) {
// set all fields atomically
}wait/notify – introduce a flag and make the producer wait while the flag is true, the consumer wait while the flag is false. Use a while loop to protect against spurious wake‑ups:
if (r.isFlag()) {
r.wait();
}
// produce
r.setFlag(true);
r.notify();notifyAll – replace notify() with notifyAll() when multiple producers or consumers exist, ensuring that any waiting thread can be awakened.
2. Deadlock and Explicit Locks
2.1 Classic deadlock example
Two threads acquire locks in opposite order, causing a circular wait:
class Deadlock1 implements Runnable {
private final Object lock1;
private final Object lock2;
public Deadlock1(Object o1, Object o2) { lock1 = o1; lock2 = o2; }
public void run() {
while (true) {
synchronized (lock1) {
System.out.println("Deadlock1----lock1");
synchronized (lock2) {
System.out.println("Deadlock1----lock2");
}
}
}
}
}
class Deadlock2 implements Runnable {
private final Object lock1;
private final Object lock2;
public Deadlock2(Object o1, Object o2) { lock1 = o1; lock2 = o2; }
public void run() {
while (true) {
synchronized (lock2) {
System.out.println("Deadlock2----lock2");
synchronized (lock1) {
System.out.println("Deadlock2----lock1");
}
}
}
}
}Both threads block after printing the first two lines, demonstrating a deadlock.
2.2 Avoiding deadlock in multi‑producer‑consumer
When several producers and consumers share the same monitor, using if to test the flag can wake a thread of the same type, leaving the opposite side waiting. Replacing the test with a while loop and using notifyAll() guarantees that a waiting thread of the correct role is awakened.
2.3 Using java.util.concurrent.locks.Lock
Java 1.5 introduced explicit locks with separate Condition objects, allowing distinct wait‑sets for producers and consumers.
Lock lock = new ReentrantLock();
Condition inCond = lock.newCondition(); // producer monitor
Condition outCond = lock.newCondition(); // consumer monitorProducer logic:
lock.lock();
while (r.isFlag()) {
inCond.await();
}
// produce the resource
r.setId(i);
... // set other fields
r.setFlag(true);
outCond.signal();
lock.unlock();Consumer logic:
lock.lock();
while (!r.isFlag()) {
outCond.await();
}
System.out.println(r);
r.setFlag(false);
inCond.signal();
lock.unlock();Full example:
public static void main(String[] args) {
Resource r = new Resource();
Lock lock = new ReentrantLock();
Condition inCond = lock.newCondition();
Condition outCond = lock.newCondition();
Input producer = new Input(r, lock, inCond, outCond);
Output consumer = new Output(r, lock, inCond, outCond);
new Thread(producer).start();
new Thread(producer).start();
new Thread(consumer).start();
new Thread(consumer).start();
}This design eliminates the race conditions of the naïve version and prevents deadlock by ensuring that only the appropriate side is signalled.
Conclusion
Multithreading improves CPU utilisation but introduces safety challenges when mutable state is shared. synchronized combined with wait / notify (or notifyAll) can coordinate a single producer‑consumer pair.
Explicit Lock and Condition provide finer‑grained control, separate wait‑sets, and avoid the limitations of intrinsic locks in more complex scenarios.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
IT Architects Alliance
Discussion and exchange on system, internet, large‑scale distributed, high‑availability, and high‑performance architectures, as well as big data, machine learning, AI, and architecture adjustments with internet technologies. Includes real‑world large‑scale architecture case studies. Open to architects who have ideas and enjoy sharing.
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.
