JAVA併發程式設計——執行緒協作通訊(二)
執行緒間的協作
在前面我們瞭解了很多關於同步(互斥鎖)的問題,下面來看一下執行緒之間的協作。這裡主要說一下Java執行緒中的join()、sleep()、yield()、wait()、notify()和notifyAll()方法。其中wait()、notify()和notifyAll()是執行緒間的協作的主要方法。
一、join()
join :讓一個執行緒等待另一個執行緒完成才繼續執行。如A執行緒執行緒執行體中呼叫B執行緒的join()方法,則A執行緒被阻塞,一直等到 B執行緒執行完為止,A才能得以繼續執行。類似於生活中排隊的時候,有人插隊,一直等到前面插隊的人的事情處理完才可以。
public class UseJoin { static class JumpQueue implements Runnable { private Thread thread; public JumpQueue(Thread thread) { this.thread = thread; } public void run() { try { thread.join();//呼叫傳入執行緒的join方法,必須等這個方法返回後,當前執行緒才能繼續執行 } catch (InterruptedException e) { } System.out.println(Thread.currentThread().getName() + " terminate."); } } public static void main(String[] args) throws Exception { Thread previous = Thread.currentThread();//現在previous是主執行緒 for (int i = 0; i < 10; i++) { // 每個執行緒擁有前一個執行緒的引用,需要等待前一個執行緒終止,才能從等待中返回 Thread thread = new Thread(new JumpQueue(previous), String.valueOf(i)); System.out.println(previous.getName()+" jump a queue the thread:" +thread.getName()); thread.start(); previous = thread; } SleepTools.second(2);//讓主執行緒休眠2秒 System.out.println(Thread.currentThread().getName() + " terminate."); } }
可以看出,程式中定義了10個執行緒,將主執行緒作為引數傳遞給第一個執行緒,在run方法中呼叫了主執行緒的join方法,然後將第一個引數作為引數傳遞給第二個執行緒 ,在run方法中呼叫了第一個執行緒的join方法。。。所以,第一個執行緒要等到主執行緒執行完才能執行,第二個執行緒要等到第一個執行緒執行完才能執行。。。
main jump a queue the thread:0 0 jump a queue the thread:1 1 jump a queue the thread:2 2 jump a queue the thread:3 3 jump a queue the thread:4 4 jump a queue the thread:5 5 jump a queue the thread:6 6 jump a queue the thread:7 7 jump a queue the thread:8 8 jump a queue the thread:9 main terminate. 0 terminate. 1 terminate. 2 terminate. 3 terminate. 4 terminate. 5 terminate. 6 terminate. 7 terminate. 8 terminate. 9 terminate.
二、sleep()
sleep:讓當前的正在執行的執行緒暫停指定的時間,並進入阻塞狀態。在其睡眠的時間段內,該執行緒由於不是處於就緒狀態,因此不會得到執行的機會。即使此時系統中沒有任何其他可執行的執行緒,出於sleep()中的執行緒也不會執行。因此sleep()方法常用來暫停執行緒執行。
前面有講到,當呼叫了新建的執行緒的start()方法後,執行緒進入到就緒狀態,可能會在接下來的某個時間獲取CPU時間片得以執行,如果希望這個新執行緒必然性的立即執行,直接呼叫原來執行緒的sleep(1)即可。
ublic class ThreadTest { public static void main(String[] args) { MyRunnable myRunnable = new MyRunnable(); Thread thread = new Thread(myRunnable); for (int i = 0; i < 100; i++) { System.out.println(Thread.currentThread().getName() + " " + i); if (i == 30) { thread.start(); try { Thread.sleep(1); // 使得thread必然能夠馬上得以執行 } catch (InterruptedException e) { e.printStackTrace(); } } } } } class MyRunnable implements Runnable { @Override public void run() { for (int i = 0; i < 100; i++) { System.out.println(Thread.currentThread().getName() + " " + i); } } }
三、執行緒讓步:yield()
Java執行緒中的Thread.yield( )方法,即執行緒讓步。顧名思義,就是說當一個執行緒使用了這個方法之後,它就會把自己CPU執行的時間讓掉,讓自己或者其它的執行緒執行,注意是讓自己或者其他執行緒執行,並不是單純的讓給其他執行緒。
yield()的作用是讓步。它能讓當前執行緒由“執行狀態”進入到“就緒狀態”,從而讓其它具有相同優先順序的等待執行緒獲取執行權;但是,並不能保證在當前執行緒呼叫yield()之後,其它具有相同優先順序的執行緒就一定能獲得執行權;也有可能是當前執行緒又進入到“執行狀態”繼續執行!
舉個例子:
一幫朋友在排隊上公交車,輪到Yield的時候,他突然說:我不想先上去了,咱們大家來競賽上公交車。然後所有人就一塊衝向公交車,有可能是其他人先上車了,也有可能是Yield先上車了。 但是執行緒是有優先順序的,優先順序越高的人,就一定能第一個上車嗎?這是不一定的,優先順序高的人僅僅只是第一個上車的概率大了一點而已,最終第一個上車的,也有可能是優先順序最低的人。並且所謂的優先順序執行,是在大量執行次數中才能體現出來的。
四、wait()、notify()和notifyAll()
wait():導致當前執行緒等待並使其進入到等待阻塞狀態。直到其他執行緒呼叫該同步鎖物件的notify()或notifyAll()方法來喚醒此執行緒。
notify():喚醒在此同步鎖物件上等待的單個執行緒,如果有多個執行緒都在此同步鎖物件上等待,則會任意選擇其中某個執行緒進行喚醒操作,只有當前執行緒放棄對同步鎖物件的鎖定,才可能執行被喚醒的執行緒。
notifyAll():喚醒在此同步鎖物件上等待的所有執行緒,只有當前執行緒放棄對同步鎖物件的鎖定,才可能執行被喚醒的執行緒。
下面看一個例子:
在快遞郵寄過程中,公里數大於100的時候更新城市,當城市為ShangHai的時候顯示到站。在main方法中啟動了三個檢查里程數變化的執行緒,三個檢查地點變化的執行緒。
以下是示例程式碼:
public class Express {
public final static String CITY = "ShangHai";
private int km;/*快遞運輸里程數*/
private String site;/*快遞到達地點*/
public Express() {
}
public Express(int km, String site) {
this.km = km;
this.site = site;
}
/* 變化公里數,然後通知處於wait狀態並需要處理公里數的執行緒進行業務處理*/
public synchronized void changeKm(){
this.km = 101;
notifyAll();
}
/* 變化地點,然後通知處於wait狀態並需要處理地點的執行緒進行業務處理*/
public synchronized void changeSite(){
this.site = "BeiJing";
notifyAll();
}
public synchronized void waitKm(){
while(this.km<=100){//公里數小於100不做處理
try {
wait();
System.out.println("Check Km thread["+Thread.currentThread().getId()
+"] is be notified");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("the Km is "+this.km+",I will change db");
}
public synchronized void waitSite(){
while(this.site.equals(CITY)){//快遞到達目的地
try {
wait();
System.out.println("Check Site thread["+Thread.currentThread().getId()
+"] is be notified");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("the site is "+this.site+",I will call user");
}
}
public class TestWN {
private static Express express = new Express(0,Express.CITY);
/*檢查里程數變化的執行緒,不滿足條件,執行緒一直等待*/
private static class CheckKm extends Thread{
@Override
public void run() {
express.waitKm();
}
}
/*檢查地點變化的執行緒,不滿足條件,執行緒一直等待*/
private static class CheckSite extends Thread{
@Override
public void run() {
express.waitSite();
}
}
public static void main(String[] args) throws InterruptedException {
for(int i=0;i<3;i++){
new CheckSite().start();
}
for(int i=0;i<3;i++){
new CheckKm().start();
}
Thread.sleep(1000);
express.changeKm();//快遞里程數變化
express.changeSite();//快遞地點變化
}
}
Check Km thread[17] is be notified
the Km is 101,I will change db
Check Km thread[16] is be notified
the Km is 101,I will change db
Check Km thread[15] is be notified
the Km is 101,I will change db
Check Site thread[14] is be notified
the site is BeiJing,I will call user
Check Site thread[13] is be notified
the site is BeiJing,I will call user
Check Site thread[12] is be notified
the site is BeiJing,I will call user
五、wait()/notify()/nitifyAll()有關說明
1.、wait()方法執行後,當前執行緒立即進入到等待阻塞狀態,其後面的程式碼不會執行。
2、notify()/notifyAll()方法執行後,將喚醒此同步鎖物件上的執行緒物件,但是,此時還並沒有釋放同步鎖物件,也就是說,如果notify()/notifyAll()後面還有程式碼,還會繼續進行,知道當前執行緒執行完畢才會釋放同步鎖物件。
3、wait()/notify()/nitifyAll()完成執行緒間的通訊或協作都是基於不同物件鎖的,因此,如果是不同的同步物件鎖將失去意義。同時,同步物件鎖最好是與共享資源物件保持一一對應關係。
4、當wait執行緒喚醒後並執行時,是接著上次執行到的wait()方法程式碼後面繼續往下執行的。
5、wait()、notify()和notifyAll()方法是本地方法,並且為final方法,無法被重寫。
6、呼叫某個物件的wait()方法能讓當前執行緒阻塞,並且當前執行緒必須擁有此物件的鎖。
7、呼叫某個物件的notify()方法能夠喚醒一個正在等待這個物件的鎖的執行緒,如果有多個執行緒都在等待這個物件的鎖,則只能喚醒其中一個執行緒。
8、呼叫notifyAll()方法能夠喚醒所有正在等待這個物件的鎖的執行緒。
9、wait()、notify()和notifyAll()這三個不是Thread類宣告中的方法,而是Object類中宣告的方法,當然由於Thread類繼承了Object類,所以Thread也可以呼叫者三個方法。WHY?由於每個物件都擁有鎖,所以讓當前執行緒等待某個物件的鎖,當然應該通過這個物件來操作了。而不是用當前執行緒來操作,因為當前執行緒可能會等待多個執行緒的鎖,如果通過執行緒來操作,就非常複雜了。
10、呼叫某個物件的wait()方法,當前執行緒必須擁有這個物件的鎖,因此呼叫wait()方法必須在同步塊或者同步方法中進行(synchronized塊或者synchronized方法)。
11、呼叫某個物件的wait()方法,相當於讓當前執行緒交出此物件的鎖,然後進入等待狀態,等待後續再次獲得此物件的鎖(Thread類中的sleep方法使當前執行緒暫停執行一段時間,從而讓其他執行緒有機會繼續執行,但它並不釋放物件鎖);
12、呼叫某個物件的notify()方法,當前執行緒也必須擁有這個物件的鎖,因此呼叫notify()方法必須在同步塊或者同步方法中進行(synchronized塊或者synchronized方法)。
13、notify()和notifyAll()方法只是喚醒等待該物件的鎖的執行緒,並不決定哪個執行緒能夠獲取到鎖。
14、一個執行緒被喚醒不代表立即獲取了物件的鎖,只有等呼叫完notify()或者notifyAll()並退出synchronized塊,釋放物件鎖後,其餘執行緒才可獲得鎖執行。
六、呼叫yield() 、sleep()、wait()、notify()等方法對鎖有何影響
yield() :不會釋放鎖的。
sleep():不會釋放鎖的。
wait():呼叫前持有鎖,呼叫了wait方法以後,鎖會被釋放。
notify() :呼叫前持有鎖,包含了notify()的方法結束以後,鎖才會被釋放。