Vấn đề
Bạn đang xây dựng mô hình producer-consumer hoặc một blocking queue tùy chỉnh — thứ gì đó liên quan đến các thread chờ lẫn nhau. Khi chạy, một trong số chúng bị crash với:
Exception in thread "Thread-0" java.lang.IllegalMonitorStateException: current thread is not owner
at java.base/java.lang.Object.wait(Native Method)
at com.example.SharedQueue.waitForItem(SharedQueue.java:14)
at com.example.ConsumerThread.run(ConsumerThread.java:22)
Stack trace trỏ đến dòng gọi wait(), notify(), hoặc notifyAll(). Thread đó đã chết. Tùy vào thiết kế của bạn, các thread còn lại sẽ rơi vào deadlock hoặc quay vòng mãi mãi chờ một tín hiệu không bao giờ đến.
Nguyên nhân gốc rễ
Java có một quy tắc cứng về ba phương thức này: bạn chỉ có thể gọi wait(), notify(), hoặc notifyAll() trên một object nếu thread hiện tại đang giữ monitor lock của object đó. Đây không phải hướng dẫn tùy ý — nếu lock chưa được giữ, JVM sẽ ném IllegalMonitorStateException ngay lập tức, không có cơ hội thứ hai.
Đây là pattern lỗi gây ra vấn đề:
// SAI: gọi wait() và notify() mà không có synchronized block
public class SharedQueue {
private final List<String> queue = new ArrayList<>();
public void waitForItem() throws InterruptedException {
while (queue.isEmpty()) {
queue.wait(); // <-- ném IllegalMonitorStateException
}
}
public void addItem(String item) {
queue.add(item);
queue.notify(); // <-- ném IllegalMonitorStateException
}
}
Thread gọi queue.wait() chưa bao giờ chiếm được lock trên queue. Không có lock, không thể wait.
Debug: Tìm sự không khớp chính xác
Stack trace cho bạn biết chính xác dòng nào. Sau đó hỏi hai câu hỏi:
- Lời gọi
wait()hoặcnotify()có nằm trong mộtsynchronizedblock không? synchronizedblock đó có đang khóa cùng một object mà bạn đang gọi phương thức trên đó không?
Hai trường hợp không khớp trông ổn khi nhìn qua nhưng sẽ nổ tung khi chạy:
// Lỗi 1: synchronized trên 'this' nhưng gọi wait() trên một field
public synchronized void bad() throws InterruptedException {
queue.wait(); // lock trên 'this', gọi wait() trên 'queue' — KHÔNG KHỚP
}
// Lỗi 2: synchronized block trên object sai
synchronized (this) {
queue.notify(); // giữ lock trên 'this', không phải 'queue' — KHÔNG KHỚP
}
Object nào bạn synchronized thì chỉ object đó bạn mới có thể gọi wait() hoặc notify(). Cùng một object, cả hai vai trò — không có ngoại lệ.
Cách sửa
Cách 1: Synchronized method (lock trên this)
Khai báo method là synchronized — điều đó biến this thành monitor — rồi gọi this.wait() và this.notify():
public class SharedQueue {
private final List<String> queue = new ArrayList<>();
public synchronized void waitForItem() throws InterruptedException {
while (queue.isEmpty()) {
this.wait(); // thread hiện tại giữ monitor 'this' — OK
}
}
public synchronized void addItem(String item) {
queue.add(item);
this.notify(); // thread hiện tại giữ monitor 'this' — OK
}
}
Cách 2: Synchronized block trên object đích
Để kiểm soát chi tiết hơn, hãy synchronize tường minh trên đúng object bạn muốn wait:
public class SharedQueue {
private final List<String> queue = new ArrayList<>();
public void waitForItem() throws InterruptedException {
synchronized (queue) { // chiếm lock trên 'queue'
while (queue.isEmpty()) {
queue.wait(); // cùng object — OK
}
}
}
public void addItem(String item) {
synchronized (queue) { // cùng lock
queue.add(item);
queue.notify(); // cùng object — OK
}
}
}
Luôn dùng while, không dùng if
Thêm một điểm nữa — tách biệt với vấn đề lock không khớp, nhưng quan trọng không kém. Hãy bọc wait() trong vòng lặp while, không phải if. JVM có thể đánh thức một thread đang chờ mà không có lý do thực sự, hiện tượng này gọi là spurious wakeup. Với if, thread tiếp tục mà không kiểm tra lại xem điều kiện có thực sự đúng không:
// SAI: spurious wakeup bỏ qua việc kiểm tra điều kiện
synchronized (lock) {
if (!ready) lock.wait();
// 'ready' có thể vẫn là false ở đây
}
// ĐÚNG: kiểm tra lại mỗi lần
synchronized (lock) {
while (!ready) lock.wait();
// 'ready' được đảm bảo là true ở đây
}
Cách 3: Dùng java.util.concurrent thay thế
Object.wait() thô vẫn hoạt động, nhưng dễ sai. Hầu hết code Java production đều dùng ReentrantLock với Condition — API làm cho quyền sở hữu lock trở nên tường minh, nên kiểu không khớp ở trên khó xảy ra vô tình hơn nhiều:
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class SharedQueue {
private final List<String> queue = new ArrayList<>();
private final ReentrantLock lock = new ReentrantLock();
private final Condition notEmpty = lock.newCondition();
public void waitForItem() throws InterruptedException {
lock.lock();
try {
while (queue.isEmpty()) {
notEmpty.await(); // tương đương wait()
}
} finally {
lock.unlock();
}
}
public void addItem(String item) {
lock.lock();
try {
queue.add(item);
notEmpty.signal(); // tương đương notify()
} finally {
lock.unlock();
}
}
}
Hoặc bỏ qua hoàn toàn việc quản lý lock thủ công. LinkedBlockingQueue xử lý toàn bộ đồng bộ hóa nội bộ — không cần wait(), không cần notify(), không thể xảy ra sự không khớp:
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
BlockingQueue<String> queue = new LinkedBlockingQueue<>();
// Thread producer
queue.put("item"); // block nếu đầy, không cần đồng bộ thủ công
// Thread consumer
String item = queue.take(); // block cho đến khi có item, không cần đồng bộ thủ công
Kiểm tra sau khi sửa
Chạy lại đoạn code đang bị crash. IllegalMonitorStateException phải biến mất. Một test tối giản để xác nhận:
public static void main(String[] args) throws InterruptedException {
SharedQueue sq = new SharedQueue();
Thread consumer = new Thread(() -> {
try {
sq.waitForItem();
System.out.println("Consumer: got item");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
Thread producer = new Thread(() -> {
try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
sq.addItem("hello");
System.out.println("Producer: sent item");
});
consumer.start();
producer.start();
consumer.join();
producer.join();
}
// Kết quả mong đợi (thứ tự có thể thay đổi):
// Producer: sent item
// Consumer: got item
Cả hai dòng đều in ra, không có exception — bạn đã xong.
Bài học rút ra
- wait(), notify(), và notifyAll() yêu cầu phải có lock. Cụ thể, lock trên đúng object bạn đang gọi chúng. JVM kiểm tra điều này khi chạy mỗi lần gọi — không có thời gian ân hạn.
- Object lock và đích của wait/notify phải giống hệt nhau. Synchronize trên
thistrong khi gọifield.wait()compile không báo lỗi và crash khi chạy. Không có cảnh báo, không có lỗi biên dịch. - Luôn dùng vòng lặp với wait(). Spurious wakeup là hành vi JVM được ghi chép rõ ràng, không phải trường hợp lý thuyết hiếm gặp. Kiểm tra bằng
ifkhông bảo vệ được gì; vòng lặpwhilethì có. - Với code mới, hãy dùng java.util.concurrent.
BlockingQueue,ReentrantLock, vàSemaphorelàm cho giao thức locking hiện ra rõ ràng trong chính code. Lớp bug gây ra exception này trở nên khó viết vô tình hơn nhiều.

