- 利用共享對象實現(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方法乞旦。當(dāng)一個線程在一個對象上調(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ā)出昂芜,就進(jìn)入wait方法莹规,進(jìn)行等待。所以泌神,我們利用一個boolean變量就可以解決通知過早的問題良漱。
虛假喚醒(Spurious Wakeups)
有時候因為某些原因舞虱,線程可能會在沒有調(diào)用notify或者notifyAll的情況下被喚醒,這也叫做虛假喚醒(Spurious Wakeups)母市。如果一個線程被虛假喚醒就會產(chǎn)生很多意想不到的問題矾兜,所以必須重視這個問題。
我們使用一個自旋鎖機(jī)制患久,也就是用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)會進(jìn)行多次判斷溉旋,重新將線程變?yōu)閣ait。
我們更好的理解嫉髓,我們舉一個具體的例子:
假設(shè)有兩個類負(fù)責(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 的標(biāo)識摘刑,一旦這個線程離開了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。