- 利用共享對象實現(xiàn)通信
- 忙等(busy waiting)
- wait(), notify() and notifyAll()
- 信號丟失(Missed Signals)
- 虛假喚醒(Spurious Wakeups)
- 多個線程等待相同的信號
- 不要對String對象或者全局對象調(diào)用wait方法
線程通信的目的就是讓線程間具有互相發(fā)送信號通信的能力。
而且,線程通信可以實現(xiàn),一個線程可以等待來自其他線程的信號。舉個例子,一個線程B可能正在等待來自線程A的信號,這個信號告訴線程B數(shù)據(jù)已經(jīng)處理好了。
利用共享對象實現(xiàn)通信
一個實現(xiàn)線程通信的簡單的方式就是通過在某些共享的對象變量中設(shè)置一個信號值。舉個例子,線程A在一個synchronize的語句塊中設(shè)置一個boolean的成員變量hasDataToProcess為true,線程B在一個synchronize語句塊中讀取hasDataToProcess,如果為true就執(zhí)行代碼,否則就等待。這樣就實現(xiàn)了線程A對線程B的通知。看下面的代碼實現(xiàn):
public class MySignal{
protected boolean hasDataToProcess = false;
public synchronized boolean hasDataToProcess(){
return this.hasDataToProcess;
}
public synchronized void setHasDataToProcess(boolean hasData){
this.hasDataToProcess = hasData;
}
}
線程A和B都必須擁有同一個MySignal類的對象實例的引用。如果線程擁有的是不同的實例,那么他們就無法獲取到對方的信號。
忙等(busy waiting)
線程B執(zhí)行的條件是,等待線程A發(fā)出通知,也就是等到線程A將hasDataToProcess()設(shè)置為true,所以線程b一直在等待信號,在一個循環(huán)的檢測條件中。這時候線程B就處于一個忙等的狀態(tài)。,因為線程b在等待的過程中是忙碌的,因為線程B在不斷的循環(huán)檢測條件是否成功。
protected MySignal sharedSignal = ...
...
while(!sharedSignal.hasDataToProcess()){
//do nothing... busy waiting
}
wait(), notify() and notifyAll()
忙等對于cpu的利用不是一個有效率的選擇,除非忙等的時間是非常短的。不然,與其讓線程處于忙等的狀態(tài),不如直接讓線程直接sleep,直到它收到信號再重新激活它。
Java有一個內(nèi)置的方法,可以讓線程在等待信號的變?yōu)閕nactive狀態(tài)。所有類的超類 java.lang.Object 定義了三個方法, wait(), notify(), and notifyAll()
一個線程可以對任何一個對象調(diào)用wait方法,這樣這個線程就會變成wait狀態(tài),inactive,等待其他線程在同一個對象上調(diào)用notify方法,來喚醒這個線程。值得注意的是,在調(diào)用wait和notify方法之前,必須要先獲得這個對象的鎖。換句話說,線程必須在synchronize的語句塊中調(diào)用wait或者notify方法。看下面的代碼實例:
public class MonitorObject{
}
public class MyWaitNotify{
MonitorObject myMonitorObject = new MonitorObject();
public void doWait(){
synchronized(myMonitorObject){
try{
myMonitorObject.wait();
} catch(InterruptedException e){...}
}
}
public void doNotify(){
synchronized(myMonitorObject){
myMonitorObject.notify();
}
}
}
等待的線程可以調(diào)用dowait方法,notify線程可以調(diào)用donotify方法。當一個線程在一個對象上調(diào)用notify方法的時候,這個對象的等待線程隊列中的一個線程會被喚醒,獲得執(zhí)行的權(quán)利。notifyAll方法則是會將給定對象的等待隊列中的所有線程都喚醒。
我們可以看到我們調(diào)用wait或者notify方法的時候,都是在synchronize語句塊中調(diào)用的。這是一個必要條件。一個線程如果沒有取得相關(guān)對象的鎖則無法調(diào)用wait和notify方法,會拋出IllegalMonitorStateException異常。
一旦一個線程調(diào)用wait方法,他就會釋放鎖,這就允許其他線程去繼續(xù)調(diào)用wait方法或者notify方法,所以這些方法都必須出現(xiàn)在synchronize語句塊中。
一個線程如果被喚醒了,不會立即離開wait方法,因為還沒獲得鎖,要等到那個調(diào)用notify的線程離開他的synchronize的語句塊,也就是等待他釋放鎖,才可以獲得鎖,離開wait。換句話說,換句話,線程要離開wait方法,必須重新獲得鎖相應(yīng)對象的鎖。如果多個線程被notifyall方法喚醒,那么在某一個時刻,只有一個被喚醒的線程可以離開wait方法,因為每個都必須重新獲得鎖才可以離開wait方法。
信號丟失(Missed Signals)
如果在調(diào)用notify或者notifyAll的時候,線程等待隊列中,沒有線程在等待,那么這個喚醒的信號并不會被保存。而是會丟失。所以,如果一個線程在另一個線程調(diào)用wait方法等待之前,就調(diào)用了notify方法,那么這個notify的信號就被丟失了,這就可能導(dǎo)致那個等待的線程將一直不會被喚醒,因為notify的喚醒信號丟失了。
To avoid losing signals they should be stored inside the signal class. In the MyWaitNotify example the notify signal should be stored in a member variable inside the MyWaitNotify instance. Here is a modified version of MyWaitNotify that does this:
為了避免信號的丟失,我們可以想辦法將信號存起來,利用一個變量。如下面這個例子:
public class MyWaitNotify2{
MonitorObject myMonitorObject = new MonitorObject();
boolean wasSignalled = false;
public void doWait(){
synchronized(myMonitorObject){
if(!wasSignalled){
try{
myMonitorObject.wait();
} catch(InterruptedException e){...}
}
//clear signal and continue running.
wasSignalled = false;
}
}
public void doNotify(){
synchronized(myMonitorObject){
wasSignalled = true;
myMonitorObject.notify();
}
}
}
我們可以看到,上面的方法在調(diào)用notidy之前先將wasSignalled設(shè)置為true。dowait方法會先檢查wasSignalled變量,如果為true,就直接跳過wait方法,因為已經(jīng)有notify信號發(fā)出了。如果為false,則說明還沒有信號發(fā)出,就進入wait方法,進行等待。所以,我們利用一個boolean變量就可以解決通知過早的問題。
虛假喚醒(Spurious Wakeups)
有時候因為某些原因,線程可能會在沒有調(diào)用notify或者notifyAll的情況下被喚醒,這也叫做虛假喚醒(Spurious Wakeups)。如果一個線程被虛假喚醒就會產(chǎn)生很多意想不到的問題,所以必須重視這個問題。
我們使用一個自旋鎖機制,也就是用while循環(huán)替代if循環(huán),循環(huán)檢查這樣就可以避免虛假喚醒的情況。
public class MyWaitNotify3{
MonitorObject myMonitorObject = new MonitorObject();
boolean wasSignalled = false;
public void doWait(){
synchronized(myMonitorObject){
while(!wasSignalled){
try{
myMonitorObject.wait();
} catch(InterruptedException e){...}
}
//clear signal and continue running.
wasSignalled = false;
}
}
public void doNotify(){
synchronized(myMonitorObject){
wasSignalled = true;
myMonitorObject.notify();
}
}
}
wait方法現(xiàn)在放在了一個while循環(huán)里,如果一個線程被喚醒,但是沒有獲得信號,那么wasSignalled 仍是false,while循環(huán)會進行多次判斷,重新將線程變?yōu)閣ait。
我們更好的理解,我們舉一個具體的例子:
假設(shè)有兩個類負責(zé)加減:
package Thread;
public class Add {
private String lock;
public Add(String lock) {
super();
this.lock = lock;
}
public void add() {
synchronized (lock) {
ValueObject.list.add("anything");
lock.notifyAll();
}
}
}
package Thread;
public class Subtract {
private String lock;
public Subtract(String lock) {
super();
this.lock = lock;
}
public void subtract() {
try {
synchronized (lock) {
if(ValueObject.list.size() == 0) {
System.out.println("Wait begin ThreadName:" + Thread.currentThread().getName());
lock.wait();
System.out.println("Wait end ThreadName:" + Thread.currentThread().getName());
}
ValueObject.list.remove(0);
System.out.println("list size : " + ValueObject.list.size());
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
package Thread;
import java.util.ArrayList;
import java.util.List;
public class ValueObject {
public static List<String> list = new ArrayList<>();
}
我們建立兩個線程
package Thread;
public class ThreadAdd extends Thread {
private Add p;
public ThreadAdd(Add p) {
this.p = p;
}
@Override
public void run() {
p.add();
}
}
package Thread;
public class ThreadSubtract extends Thread {
private Subtract p;
public ThreadSubtract(Subtract p) {
this.p = p;
}
@Override
public void run() {
p.subtract();
}
}
我們測試
package Thread;
public class Run {
public static void main(String[] args) throws InterruptedException {
String lock = new String("");
Add add = new Add(lock);
Subtract sub = new Subtract(lock);
ThreadAdd addthread = new ThreadAdd(add);
ThreadSubtract sub1 = new ThreadSubtract(sub);
sub1.start();
ThreadSubtract sub2 = new ThreadSubtract(sub);
sub2.start();
Thread.sleep(1000);
addthread.start();
}
}

我們發(fā)現(xiàn)發(fā)生了異常,這是為什么呢?因為notifyAll同時喚醒了兩個減的線程,然后第二個減的線程獲得了鎖,將size減為0,隨后第一個減線程獲得鎖,再去減就拋異常了,因為它沒有繼續(xù)判斷是否為0的條件,所以我們需要在獲得鎖之后依然去判斷條件,也就是將if改為while
package Thread;
public class Subtract {
private String lock;
public Subtract(String lock) {
super();
this.lock = lock;
}
public void subtract() {
try {
synchronized (lock) {
while(ValueObject.list.size() == 0) {
System.out.println("Wait begin ThreadName:" + Thread.currentThread().getName());
lock.wait();
System.out.println("Wait end ThreadName:" + Thread.currentThread().getName());
}
ValueObject.list.remove(0);
System.out.println("list size : " + ValueObject.list.size());
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

這樣就可以正確運行了。
多個線程等待相同的信號
如果你有多個線程在等待隊列中,然后你又要調(diào)用notifyAll方法,那么使用while來替代if,是一個很好的解決虛假喚醒的方法。只有一個線程在一個時刻會被喚醒,然后可以獲得鎖,離開wait方法,并清楚wasSignalled 的標識,一旦這個線程離開了synchronize的語句塊,其他線程可以獲得鎖并且離開wait方法。但是,由于wasSignalled 被第一個線程清除了,其他等待的線程因為while的存在會繼續(xù)回到wait的狀態(tài),知道下一個信號來了
不要對String對象或者全局對象調(diào)用wait方法
如果我們對一個String對象調(diào)用wait方法
public class MyWaitNotify{
String myMonitorObject = "";
boolean wasSignalled = false;
public void doWait(){
synchronized(myMonitorObject){
while(!wasSignalled){
try{
myMonitorObject.wait();
} catch(InterruptedException e){...}
}
//clear signal and continue running.
wasSignalled = false;
}
}
public void doNotify(){
synchronized(myMonitorObject){
wasSignalled = true;
myMonitorObject.notify();
}
}
}
如果我們在一個空emptyString或者其他的常量String對象上調(diào)用wait方法會產(chǎn)生問題。JVM/Compiler 在內(nèi)部將常量的String變成相同的對象。這就意味著,即使我們有兩個不同的MyWaitNotify實例,他們確實引用著同一個對象。這就意味著本來不相關(guān)的兩個實例,最后通信的結(jié)果可能發(fā)生不可預(yù)測的交叉結(jié)果。
如下圖所示:

需要注意的是,即使四個線程調(diào)用wait和notify都是在同一個對象上的,但是信號都是存儲在各自的實例中的,也就是wasSignal是存儲在各自實例中的,這就會引起很大的問題。一個來自MyWaitNotify 1的信號可能會喚醒MyWaitNotify 2中的等待線程,但是wasSignal確實存在MyWaitNotify 1中的。
如果notify作用在第二個實例上MyWaitNotify 2,那就可能發(fā)生線程A和B被喚醒的情況,但是線程A和B會在while循環(huán)中檢查wasSignal信號,結(jié)果發(fā)現(xiàn)依然是false,就會繼續(xù)等待,所以notify并沒有起到作用,這就類似虛假喚醒的情況。
這樣發(fā)生的情況就是,如果我們調(diào)用notify方法,然后notify的又不是自己這個實例的線程,結(jié)果就沒有線程會被喚醒,這就類似于信號丟失的情況。
但如果我們調(diào)用的notifyAll方法就不會出現(xiàn)信號丟失的情況,因為wasSignal會被正確的設(shè)置,相應(yīng)的線程會被喚醒,其他對象的線程會因為while循環(huán)繼續(xù)回到wait狀態(tài)。
那你也許會說,我們直接調(diào)用notifyAll不就可以避免String帶來的問題么?確實是這樣,但是我們?nèi)绻谌壳闆r都調(diào)用notifyAll的話,就會出現(xiàn)性能的問題,我們完全沒有必要在只有一個線程的情況下,調(diào)用notifyAll。
所以,我們不要使用全局的對象或者String變量調(diào)用wait。