Keywords: Java Multithreading | wait and notify | IllegalMonitorStateException | Synchronization Mechanism | Matrix Multiplication
Abstract: This article provides an in-depth exploration of the correct usage of wait and notify methods in Java multithreading programming. Through a matrix multiplication case study, it analyzes the causes of IllegalMonitorStateException and presents comprehensive solutions. Starting from synchronization mechanism principles, the article explains object monitor lock acquisition and release mechanisms, offers complete code refactoring examples, and discusses strategies for choosing between notify and notifyAll. Combined with system design practices, it emphasizes the importance of thread coordination in complex computational scenarios.
Problem Background and Exception Analysis
In the multithreaded matrix multiplication scenario, developers aim to implement ordered printing of computation results. In the original code, multiplication threads call the notify() method after completing cell calculations, while the printing thread waits for notifications via the wait() method. However, this implementation frequently throws IllegalMonitorStateException.
The root cause of this exception is that the thread calling notify() must hold the monitor lock of the target object. In the original code, the multiplication thread's notify() call lacks necessary synchronization block protection, while the printing thread uses synchronized (this) but both threads actually operate on different object instances.
Core Principles of Synchronization Mechanism
Java's wait() and notify() methods implement inter-thread communication based on object intrinsic lock mechanisms. Each Java object is associated with a monitor lock, and a thread must acquire this lock to call the object's wait(), notify(), or notifyAll() methods.
The correct usage pattern requires:
- Waiting and notifying threads must synchronize on the same object
- The thread calling
wait()releases the held lock and enters waiting state - The thread calling
notify()must hold the same object lock - The awakened thread needs to reacquire the lock to continue execution
Code Refactoring and Implementation Solution
Based on the above principles, we refactor the thread coordination logic for matrix multiplication. First, define shared synchronization objects:
// Shared synchronization object
private final Object lock = new Object();
// Counters for tracking printing order
private int currentRow = 0;
private int currentCol = 0;
Improved implementation of multiplication thread:
public void run() {
int countNumOfActions = 0;
int maxActions = randomize();
for (int i = 0; i < size; i++) {
result[rowNum][colNum] = result[rowNum][colNum] + row[i] * col[i];
countNumOfActions++;
if (countNumOfActions >= maxActions) {
countNumOfActions = 0;
maxActions = randomize();
Thread.yield();
}
}
isFinished[rowNum][colNum] = true;
// Call notify within synchronization block
synchronized (lock) {
lock.notifyAll();
}
}
Improved implementation of printing thread:
public void run() {
System.out.println("The result matrix of the multiplication is:");
while (currentRow < creator.getmThreads().length) {
synchronized (lock) {
try {
// Use loop to check condition, avoiding spurious wakeups
while (!creator.getmThreads()[currentRow][currentCol].getIsFinished()[currentRow][currentCol]) {
lock.wait();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
// Print current cell
System.out.print(creator.getResult()[currentRow][currentCol] + " ");
// Update printing position
if (currentCol < creator.getmThreads()[currentRow].length - 1) {
currentCol++;
} else {
System.out.println();
currentCol = 0;
currentRow++;
}
}
}
Selection Strategy Between notify and notifyAll
In the original problem, using notify() may cause thread starvation issues. Since multiple multiplication threads may complete calculations simultaneously but there's only one printing thread, using notify() can only wake up one waiting thread. If the awakened thread is not the printing thread but another multiplication thread, the printing thread might never be awakened.
Therefore, we choose to use notifyAll(), which wakes up all threads waiting on that object. Although this incurs some performance overhead, it ensures system correctness. After being awakened, the printing thread checks whether the condition is met, and continues waiting if not.
System Design Considerations
In complex multithreaded systems, the design of inter-thread coordination mechanisms is crucial. The matrix multiplication case demonstrates a variant of the producer-consumer pattern, where multiplication threads act as producers and the printing thread acts as a consumer.
From a system design perspective:
- Clearly define access protocols for shared resources
- Consider thread-safe data structures for managing computation states
- Design reasonable timeout and error handling mechanisms
- Make trade-offs between performance and correctness
Through practice with over 120 system design problems, developers can deeply understand multiple solutions to such coordination problems, including using advanced synchronization tools from the java.util.concurrent package, such as CountDownLatch, CyclicBarrier, or Phaser, which provide richer and safer thread coordination mechanisms.
Summary and Best Practices
Proper usage of wait() and notify() methods requires following several key principles: always call these methods within synchronization blocks, use the same synchronization object, check waiting conditions in loops to avoid spurious wakeups, and carefully choose between notify() and notifyAll().
For modern Java development, it's recommended to prioritize advanced concurrency tools provided by the java.util.concurrent package, which offer significant advantages in usability and safety. However, understanding the underlying wait() and notify() mechanisms remains crucial for deeply mastering Java concurrent programming.