Hôm nay tôi có cho lớp JC3 tại ActiveStudy làm một bài tập như sau:
Implements mô hình Producer – Consumer sử dụng synchonized, wait và notify
Và đây là một bài làm điển hình:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import java.util.LinkedList; | |
import java.util.Queue; | |
/** | |
* | |
* @author 404NotFound | |
*/ | |
public class MyQueue<E> { | |
private final Object sync = new Object(); | |
private final int capacity; | |
private Queue<E> queue = new LinkedList<>(); | |
public MyQueue() { | |
this.capacity = 1024; | |
} | |
public MyQueue(int capacity) { | |
this.capacity = capacity; | |
} | |
public void put(E e) { | |
synchronized (sync) { | |
try { | |
while (queue.size() == capacity) { | |
sync.wait(); | |
} | |
} catch (InterruptedException ignore) { | |
} | |
queue.offer(e); | |
sync.notify(); | |
} | |
} | |
public E take() { | |
synchronized(sync){ | |
try { | |
while (queue.size() == 0) { | |
sync.wait(); | |
} | |
} catch (InterruptedException ignore) { | |
} | |
E e = queue.poll(); | |
sync.notify(); | |
return e; | |
} | |
} | |
public int size(){ | |
return queue.size(); | |
} | |
} |
Class MyQueue trên có thể chạy tốt trong rất nhiều trường hợp. Tuy nhiên, đôi khi chương trình lại treo một cách bất thường…
Để dễ hiểu, tôi tạo một class Main trong đó khởi tạo MyQueue<String> có capacity bằng 1, chương trình chạy với 1 producer P và 2 consumer C1, C2. Lúc đầu tôi nghĩ rằng chương trình treo do deadlock, nhưng khi dump thread thì mọi chuyện lại khác.
“P” #11 prio=5 os_prio=0 tid=0x000000001741f800 nid=0x2cac in Object.wait() [0x0000000017d1f000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
– waiting on <0x00000000e102e270> (a java.lang.Object)
at java.lang.Object.wait(Object.java:502)
at Processor.produce(Processor.java:19)
– locked <0x00000000e102e270> (a java.lang.Object)
at App$1.run(App.java:14)
at java.lang.Thread.run(Thread.java:745)Locked ownable synchronizers:
– None“C2” #13 prio=5 os_prio=0 tid=0x000000001741f000 nid=0x2a2c in Object.wait() [0x0000000017c1f000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
– waiting on <0x00000000e102e270> (a java.lang.Object)
at java.lang.Object.wait(Object.java:502)
at Processor.consume(Processor.java:37)
– locked <0x00000000e102e270> (a java.lang.Object)
at App$3.run(App.java:50)
at java.lang.Thread.run(Thread.java:745)Locked ownable synchronizers:
– None“C1” #12 prio=5 os_prio=0 tid=0x0000000017429000 nid=0x1728 in Object.wait() [0x0000000017b1f000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
– waiting on <0x00000000e102e270> (a java.lang.Object)
at java.lang.Object.wait(Object.java:502)
at Processor.consume(Processor.java:37)
– locked <0x00000000e102e270> (a java.lang.Object)
at App$2.run(App.java:36)
at java.lang.Thread.run(Thread.java:745)Locked ownable synchronizers:
– None
Như vậy, cả 3 thread cùng wait. sau khi debug để xem thời gian ở mức nanosecond thì chuyện là thế này (1 trong rất nhiều case):
- P1: RUNNABLE, C1: RUNNABLE, C2: RUNNABLE
- C1 và C2 lần lượt lấy lock và WAITING
- P1 put String vào queue, notify rồi ngay lập tức quay về BLOCKED
- Giả sử P1 notify cho C1. Chú ý rằng khi một thread được notify thì nó sẽ chuyển về trạng thái BLOCKED và vẫn phải tranh chấp, cùng với đó nó phải được thread scheduler lập lịch để lấy lock trước khi chuyển về trạng thái RUNNABLE. Do đó lúc này cơ hội để P1 và C1 lấy lock là như nhau.
- P1 lấy được lock, chuyển về RUNNABLE. Do queue full, P1 nhả lock và wait.
- C1 giữ lock, lấy object và in ra màn hình. Sau đó notify cho C2 (Chú ý rằng method notify() sẽ gọi một thread bất kỳ trong số những thread đang wait.).
- C2 check điều kiện. Hiển nhiên queue trống. C2 tiếp tục wait.
- C1 đang trong trạng thái BLOCKED, lấy được lock từ C2, check queue –> queue trống –> C1 wait.
–> 3 Thread cùng wait, không có thread nào notify! Mặc dù C2 đã nhả lock, nhưng cả 3 thread đều đang trong trạng thái WAITING, mà lại không có thread nào notify cả.
Đây chắc chắn không phải deadlock, cũng không phải livelock hay starvation. Đây là hiện tượng missing signal, khi một thread bị miss tín hiệu notify. Cuối cùng, producer đã không được nhận signal notify mà nó đáng ra nên được nhận.
Để giải quyết vấn đề này, tôi dùng notifyAll(). Tương tự như trên, nếu dùng notifyAll(), mỗi thread đang wait sẽ được phép bỏ qua synchonized block và execute tiếp sau lệnh wait() một lần (theo thứ tự nhất định chứ không phải đồng thời). Như vậy, sau khi C1 giữ lock và in ra màn hình, cả P và C2 đều có cơ hội được giữ lock. Do khi giữ lock C2 wait() ngay nên P tiếp tục được produce, do đó chương trình chạy ngon lành cành đào.
Chốt: Chỉ dùng notify khi các thread đang wait có cùng công việc, bản chất như nhau, hay nói cách khác thằng nào wakeup không quan trọng. Còn lại thì gọi tất notifyAll.