線程通信的目的是為了能夠讓線程之間相互發送信號。另外,線程通信還能夠使得線程等待其它線程的信號,比如,線程B可以等待線程A的信號,這個信號可以是線程A已經處理完成的信號。
通過共享對象通信
有一個簡單的實現線程之間通信的方式,就是在共享對象的變量中設置信號值。比如線程A在一個同步塊中設置一個成員變量hasDataToProcess值為true,而線程B同樣在一個同步塊中讀取這個成員變量。下面例子演示了一個持有信號值的對象,並提供了設置信號值和獲取信號值的同步方法:
public class MySignal {
private boolean hasDataToProcess;
public synchronized void setHasDataToProcess(boolean hasData){
this.hasDataToProcess=hasData;
}
public synchronized boolean hasDataToProcess(){
return this.hasDataToProcess;
}
}
ThreadB計算完成會在共享對象中設置信號值:
public class ThreadB extends Thread{
int count;
MySignal mySignal;
public ThreadB(MySignal mySignal){
this.mySignal=mySignal;
}
@Override
public void run(){
for(int i=0;i<100;i++){
count=count+1;
}
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
mySignal.setHasDataToProcess(true);
}
}
ThreadA在循環中一直檢測共享對象的信號值,等待ThreadB計算完成的信號:
public class ThreadA extends Thread{
MySignal mySignal;
ThreadB threadB;
public ThreadA(MySignal mySignal, ThreadB threadB){
this.mySignal=mySignal;
this.threadB=threadB;
}
@Override
public void run(){
while (true){
if(mySignal.hasDataToProcess()){
System.out.println("線程B計算結果為:"+threadB.count);
break;
}
}
}
public static void main(String[] args) {
MySignal mySignal=new MySignal();
ThreadB threadB=new ThreadB(mySignal);
ThreadA threadA=new ThreadA(mySignal,threadB);
threadB.start();
threadA.start();
}
}
很明顯,采用共享對象方式通信的線程A和線程B必須持有同一個MySignal對象的引用,這樣它們才能彼此檢測到對方設置的信號。當然,信號也可存儲在共享內存buffer中,它和實例是分開的。
線程的忙等
從上面例子中可以看出,線程A一直在等待數據就緒,或者說線程A一直在等待線程B設置hasDataToProcess的信號值為true:
public void run(){
while(true){
if(mySignal.hasDataToProcess()){
System.out.println("線程B計算結果為:"+threadB.count);
break;
}
}
}
為什么說是忙等呢?因為上面代碼一直在執行循環,直到hasDataToProcess被設置為true。
忙等意味着線程還處於運行狀態,一直在消耗CPU資源,所以,忙等不是一種很好的現象。那么能不能讓線程在等待信號時釋放CPU資源進入阻塞狀態呢?其實java.lang.Object提供的wait()、notify()、notifyAll()方法就可以解決忙等問題。
wait()、notify()、notifyAll()
Java提供了一種內聯機制可以讓線程在等待信號時進入非運行狀態。當一個線程調用任何對象上的wait()方法時便會進入非運行狀態,直到另一個線程調用同一個對象上的notify()或notifyAll()方法。
為了能夠調用一個對象的wait()、notify()方法,調用線程必須先獲得這個對象的鎖。因為線程只有在同步塊中才會占用對象的鎖,所以線程必須在同步塊中調用wait()、notify()方法。
我們把上面通過共享對象通信的例子改成調用對象wait()、notify()方法來實現:
首先我們先構造一個任意對象,我們又把它稱作監控對象:
public class MonitorObject {
}
ThreadD負責計算,在計算完成時喚醒被阻塞的ThreadC:
public class ThreadD extends Thread {
int count;
MonitorObject mySignal;
public ThreadD(MonitorObject mySignal) {
this.mySignal = mySignal;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
count = count + 1;
}
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (mySignal) {
mySignal.notify();//計算完成調用對象的notify()方法,喚醒因調用這個對象wait()方法而掛起的線程
}
}
}
ThreadC等待ThreadD的喚醒:
public class ThreadC extends Thread {
MonitorObject mySignal;
ThreadD threadD;
public ThreadC(MonitorObject mySignal, ThreadD threadD) {
this.mySignal = mySignal;
this.threadD = threadD;
}
@Override
public void run() {
synchronized (mySignal) {
try {
mySignal.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("線程B計算結果為:" + threadD.count);
}
}
public static void main(String[] args) {
MonitorObject mySignal = new MonitorObject();
ThreadD threadD = new ThreadD(mySignal);
ThreadC threadC = new ThreadC(mySignal, threadD);
threadC.start();
threadD.start();
}
}
在這個例子中,線程C因調用了監控對象的wait()方法而掛起,線程D通過調用監控對象的notify()方法喚醒掛起的線程C。我們還可以看到,兩個線程都是在同步塊中調用的wait()和notify()方法。如果一個線程在沒有獲得對象鎖的前提下調用了這個對象的wait()或notify()方法,方法調用時將會拋出 IllegalMonitorStateException異常。
注意,當一個線程調用一個對象的notify()方法,則會喚醒正在等待這個對象所有線程中的一個線程(喚醒的線程是隨機的),當線程調用的是對象的notifyAll()方法,則會喚醒所有等待這個對象的線程(喚醒的所有線程中哪一個會執行也是不確定的)。
這里還有一個問題,既然調用對象wait()方法的線程需要獲得這個對象的鎖,那么這會不會阻塞其它線程調用這個對象的notify()方法呢?答案是不會阻塞,當一個線程調用監控對象的wait()方法時,它便會釋放掉這個監控對象鎖,以便讓其它線程能夠調用這個對象的notify()方法或者wait()方法。
另外,當一個線程被喚醒時不會立刻退出wait()方法,只有當調用notify()的線程退出它的同步塊為止。也就是說,被喚醒的線程只有重新獲得監控對象鎖時才會退出wait()方法,因為wait()方法在同步塊中,它的執行需要再次獲得對象鎖。所以,當通過notifyAll()方法喚醒被阻塞的線程時,一次只能有一個線程會退出wait()方法,同樣是因為每個線程都需要先獲得監控對象鎖才能執行同步塊中的wait()方法退出。