執行緒物件的幾個重要的方法
本來打算用一節的篇幅來把執行緒方法中一些重要的知識說完,但這樣下來估計要很常的篇幅,可能要用好幾節才能說把和執行緒方法相關的一些重要的知識說完。
首先我們接基礎篇(二)來說明start()方法。
一個執行緒物件生成後,如果要產生一個執行的執行緒,就一定要呼叫它的start()方法.在介紹這個方法時不得不同時說明run方法.其實執行緒物件的run方法完全是一個介面回撥方法,它是你這個執行緒物件要完成的具體邏輯.簡單說你要做什麼就你在run中完成,而如何做,什麼時候做就不需要你控制了,你只要呼叫start()方法,JVM就會管理這個執行緒物件讓它產生一個執行緒並註冊到執行緒處理系統中。
從表面上看,start()方法呼叫了run()方法,事實上,start()方法並沒有直接呼叫run方法.在JDK1.5以前start()方法是本地方法,它如何最終呼叫run方法已經不是JAVA程式設計師所能瞭解的.而在JDK1.5中,原來的那個本地start()方法被start0()代替,另個一個純JAVA的start()中呼叫本地方法start0(),而在start()方法中做了一個驗證,就是對一個全域性變數(物件變數)started做檢驗,如果為true,則start()丟擲異常,不會呼叫本地方法start0(),否則,先將該變數設有true,然後呼叫start0()。
從中我們可以看到這個為了控制一個執行緒物件只能執行成功一次start()方法.這是因為執行緒的執行要獲取當前環境,包括安全,父執行緒的許可權,優先順序等條件,如果一個執行緒物件可以執行多次,那麼定義一個static 的執行緒在一個環境中獲取相應許可權和優先順序,執行完成後它在另一個環境中利用原來的許可權和優先順序等屬性在當前環境中執行,這樣就造成無法預知的結果.簡單說來,讓一個執行緒物件只能成功執行一次,是基於對執行緒管理的需要。
start()方法最本質的功能是從CPU中申請另一個執行緒空間來執行run()方法中的程式碼,它和當前的執行緒是兩條線,在相對獨立的執行緒空間執行,也就是說,如果你直接呼叫執行緒物件的run()方法,當然也會執行,但那是在當前執行緒中執行,run()方法執行完成後繼續執行下面的程式碼.而呼叫start()方法後,run()方法的程式碼會和當前執行緒併發(單CPU)或並行(多CPU)執行。
所以請記住一句話[呼叫執行緒物件的run方法不會產生一個新的執行緒],雖然可以達到相同的執行結果,但執行過程和執行效率不同。
[執行緒的interrupt()方法,interrupted()和isInterrupted()]
這三個方法是關係非常密切而且又比較複雜的,雖然它們各自的功能很清楚,但它們之間的關係有大多數人不是真正的瞭解。
先說interrupt()方法,它是例項方法,而它也是最奇怪的方法,在java語言中,執行緒最初被設計為"隱晦難懂"的東西,直到現在它的語義不沒有象它的名字那樣準確。大多數人以為,一個執行緒象呼叫了interrupt()方法,那它對應的執行緒就應該被中斷而丟擲異常,事實中,當一個執行緒物件呼叫interrupt()方法,它對應的執行緒並沒有被中斷,只是改變了它的中斷狀態。
使當前執行緒的狀態變以中斷狀態,如果沒有其它影響,執行緒還會自己繼續執行。
只有當執行緒執行到sleep,wait,join等方法時,或者自己檢查中斷狀態而丟擲異常的情況下,執行緒才會丟擲異常。
如果執行緒物件呼叫interrupt()後它對應的執行緒就立即中斷,那麼interrupted()方法就不可能執行。
因為interrupted()方法是一個static方法,就是說只能在當前執行緒上呼叫,而如果一個執行緒interrupt()後它已經中斷了,那它又如何讓自己interrupted()?
正因為一個執行緒呼叫interrupt()後只是改變了中斷狀態,它可以繼續執行下去,在沒有呼叫sleep,wait,join等法或自己丟擲異常之前,它就可以呼叫interrupted()來清除中斷狀態(還會原狀)interrupted()方法會檢查當前執行緒的中斷狀態,如果為 "被中斷狀態"則改變當前執行緒為"非中斷狀態"並返回true,如果為"非中斷狀態"則返回false,它不僅檢查當前執行緒是否為中斷狀態,而且在保證當前執行緒回來非中斷狀態,所以它叫"interrupted",是說中斷的狀態已經結束(到非中斷狀態了)isInterrupted()方法則僅僅檢查執行緒物件對應的執行緒是否是中斷狀態,並不改變它的狀態。
目前大家只能先記住這三個方法的功能,只有真正深入到多執行緒程式設計實踐中,才會體會到它們為什麼是物件方法,為什麼是類方法。
執行緒到底什麼時候才被中斷丟擲InterruptedException異常,我們將在提高篇中詳細討論。
[sleep(),join(),yield()方法]
在現在的環節中,我只能先說明這些方法的作用和呼叫原則,至於為什麼,在基礎篇中無法深入,只能在提高篇中詳細說明。
sleep()方法中是類方法,也就是對當前執行緒而言的,程式設計師不能指定某個執行緒去sleep,只能是當前執行緒執行到sleep()方法時,睡眠指定的時間(讓其它執行緒執行).事實上也只能是類方法,在當前執行緒上呼叫.試想如果你呼叫一個執行緒物件的sleep()方法,那麼這個物件對應的執行緒如果不是正在執行,它如何sleep()?所以只有當前執行緒,因為它正在執行,你才能保證它可以呼叫sleep()方法。
原則:[在同步方法中儘量不要呼叫執行緒的sleep()方法],或者簡單說,對於一般水平的程式設計師你基本不應該呼叫sleep()方法。
join()方法,正如第一節所言,在一個執行緒物件上呼叫join方法,是當前執行緒等待這個執行緒物件對應的執行緒結束,比如有兩個工作,工作A要耗時10秒鐘,工作B要耗時10秒或更多。我們在程式中先生成一個執行緒去做工作B,然後做工作A。
new?B().start();//做工作B
A();//做工作A
工作A完成後,下面要等待工作B的結果來進行處理.如果工作B還沒有完成我就不能進行下面的工作C,所以
B?b?=?new?B();
b.start();//做工作B
A();//做工作A
b.join();//等工作B完成。
C();//繼續工作C。
原則:[join是測試其它工作狀態的唯一正確方法],我見過很多人,甚至有的是博士生,在處理一項工作時如果另一項工作沒有完成,說讓當前工作執行緒sleep(x),我問他,你這個x是如何指定的,你怎麼知道是100毫秒而不是99毫秒或是101毫秒?其實這就是OnXXX事件的實質,我們不是要等多長時間才去做什麼事,而是當等待的工作正好完成的時候去做。
yield()方法也是類方法,只在當前執行緒上呼叫,理由同上,它主是讓當前執行緒放棄本次分配到的時間片原則:[不是非常必要的情況下,沒有理由呼叫它].呼叫這個方法不會提高任何效率,只是降低了CPU的總週期上面介紹的執行緒一些方法,基於(基礎篇)而言只能簡單提及.以後具體應用中我會結合例項詳細論述。
執行緒本身的其它方法請參看API文件.下一節介紹非執行緒的方法,但和執行緒密切相關的兩[三]個物件方法:
[wait(),notify()/notifyAll()]
這是在多執行緒中非常重要的方法。
[wait(),notify()/notityAll()方法]
關於這兩個方法,有很多的內容需要說明.在下面的說明中可能會有很多地方不能一下子明白,但在看完本節後,即使不能完全明白,你也一定要回過頭來記住下面的兩句話:
[wait(),notify()/notityAll()方法是普通物件的方法(Object超類中實現),而不是執行緒物件的方法]
[wait(),notify()/notityAll()方法只能在同步方法中呼叫]
[執行緒的互斥控制]
多個執行緒同時操作某一物件時,一個執行緒對該物件的操作可能會改變其狀態,而該狀態會影響另一執行緒對該物件的真正結果.
這個例子我們在太多的文件中可以看到,就象兩個操售票員同時售出同一張票一樣.
執行緒A | 執行緒B |
---|---|
1.執行緒A在資料庫中查詢存票,發現票C可以賣出 | |
class="left"2.執行緒A接受使用者訂票請求,準備出票. | |
3.這時切換到了執行緒B執行 | |
4.執行緒B在資料庫中查詢存票,發現票C可以賣出 | |
5.執行緒B將票賣了出去 | |
6.切換到執行緒A執行,執行緒A賣了一張已經賣出的票 |
所以需要一種機制來管理這類問題的發生,當某個執行緒正在執行一個不可分割的部分時,其它執行緒不能不能同時執行這一部分.
象這種控制某一時刻只能有一個執行緒執行某個執行單元的機制就叫互斥控制或共享互斥(mutual exclusion)
在JAVA中,用synchornized關鍵字來實現互斥控制(暫時這樣認為,JDK1.5已經發展了新的機制)
[synchornized關鍵字]
把一個單元宣告為synchornized,就可以讓在同一時間只有一個執行緒操作該方法.
有人說synchornized就是一把鎖,事實上它確實存在鎖,但是是誰的鎖,鎖誰,這是一個非常複雜的問題.
每個物件只有一把監視鎖(monitor lock),一次只能被一個執行緒獲取.當一個執行緒獲取了這一個鎖後,其它執行緒就只能等待這個執行緒釋放鎖才能再獲取.
那麼synchornized關鍵字到底鎖什麼?得到了誰的鎖?
對於同步塊,synchornized獲取的是引數中的物件鎖:
synchornized(obj){ //............... }
執行緒執行到這裡時,首先要獲取obj這個例項的鎖,如果沒有獲取到執行緒只能等待.如果多個執行緒執行到這裡,只能有一個執行緒獲取obj的鎖,然後執行{}中的語句,所以,obj物件的作用範圍不同,控制程式不同.
假如:
public void test(){ Object o = new Object(); synchornized(obj){ //............... } }
這段程式控制不了任何,多個執行緒之間執行到Object o = new Object();時會各自產生一個物件然後獲取這個物件有監視鎖,各自皆大歡喜地執行.
而如果是類的屬性:
class Test{ Object o = new Object(); public void test(){ synchornized(o){ //............... } } }
所有執行到Test例項的synchornized(o)的執行緒,只有一個執行緒可以獲取到監視鎖.
有時我們會這樣:
public void test(){ synchornized(this){ //............... } }
那麼所有執行Test例項的執行緒只能有一個執行緒執行.而synchornized(o)和synchornized(this)的範圍是不同的,因為執行到Test例項的synchornized(o)的執行緒等待時,其它執行緒可以執行Test例項的synchornized(o1)部分,但多個執行緒同時只有一個可以執行Test例項的synchornized(this).]
而對於
synchornized(Test.class){ //............... }
這樣的同步塊而言,所有呼叫Test多個例項的執行緒賜教只能有一個執行緒可以執行.
[synchornized方法]
如果一個方法宣告為synchornized的,則等同於把在為個方法上呼叫synchornized(this).
如果一個靜態方法被宣告為synchornized,則等同於把在為個方法上呼叫synchornized(類.class).
現在進入wait方法和notify/notifyAll方法.這兩個(或叫三個)方法都是Object物件的方法,而不是執行緒物件的方法.如同鎖一樣,它們是線上程中呼叫某一物件上執行的.
class Test{ public synchornized void test(){ //獲取條件,int x 要求大於100; if(x < 100) wait(); } }
這裡為了說明方法沒有加在try{}catch(){}中,如果沒有明確在哪個物件上呼叫wait()方法,則為this.wait();
假如:
Test t = new Test();
現在有兩個執行緒都執行到t.test();方法.其中執行緒A獲取了t的物件鎖,進入test()方法內.
這時x小於100,所以執行緒A進入等待.
當一個執行緒呼叫了wait方法後,這個執行緒就進入了這個物件的休息室(waitset),這是一個虛擬的物件,但JVM中一定存在這樣的一個數據結構用來記錄當前物件中有哪些程執行緒在等待.
當一個執行緒進入等待時,它就會釋放鎖,讓其它執行緒來獲取這個鎖.
所以執行緒B有機會獲得了執行緒A釋放的鎖,進入test()方法,如果這時x還是小於100,執行緒B也進入了t的休息室.
這兩個執行緒只能等待其它執行緒呼叫notity[All]來喚醒.
但是如果呼叫的是有引數的wait(time)方法,則執行緒A,B都會在休息室中等待這個時間後自動喚醒.
[為什麼真正的應用都是用while(條件)而不用if(條件)]
在實際的程式設計中我們看到大量的例子都是用?
while(x < 100)
wait();go();而不是用if,為什麼呢?
在多個執行緒同時執行時,if(x <100)是不安全的.因為如果執行緒A和執行緒B都在t的休息室中等待,這時另一個執行緒使x==100了,並呼叫notifyAll方法,執行緒A繼續執行下面的go().而它執行完成後,x有可能又小於100,比如下面的程式中呼叫了--x,這時切換到執行緒B,執行緒B沒有繼續判斷,直接執行go();就產生一個錯誤的條件,只有while才能保證執行緒B又繼續檢查一次.
[notify/notifyAll方法]
這兩個方法都是把某個物件上休息區內的執行緒喚醒,notify只能喚醒一個,但究竟是哪一個不能確定,而notifyAll則喚醒這個物件上的休息室中所有的執行緒.
一般有為了安全性,我們在絕對多數時候應該使用notifiAll(),除非你明確知道只喚醒其中的一個執行緒.
那麼是否是隻要呼叫一個物件的wait()方法,當前執行緒就進入了這個物件的休息室呢?事實中,要呼叫一個物件的wait()方法,只有當前執行緒獲取了這個物件的鎖,換句話說一定要在這個物件的同步方法或以這個物件為引數的同步塊中.
class MyThread extends Thread{ Test t = new Test(); public void run(){ t.test(); System.out.println("Thread say:Hello,World!"); } } public class Test { int x = 0; public void test(){ if(x==0) try{ wait(); }catch(Exception e){} } public static void main(String[] args) throws Exception{ new MyThread().start(); } }
這個執行緒就不會進入t的wait方法而直接打印出Thread say:Hello,World!.
而如果改成:
public class Test { int x = 0; public synchornized void test(){ if(x==0) try{ wait(); }catch(Exception e){} } public static void main(String[] args) throws Exception{ new MyThread().start(); } }
我們就可以看到執行緒一直等待,注意這個執行緒進入等待後沒有其它執行緒喚醒,除非強行退出JVM環境,否則它一直等待.
所以請記住:
[執行緒要想呼叫一個物件的wait()方法就要先獲得該物件的監視鎖,而一旦呼叫wait()後又立即釋放該鎖]
轉載自dev2dev網友axman的go deep into java專欄。