公眾號:mywangxiao
及時發(fā)布考試資訊
分享考試技巧、復(fù)習(xí)經(jīng)驗
新浪微博 @wangxiaocn關(guān)注微博
聯(lián)系方式 400-18-8000
java多線程完全版解析
盡管線程對象的常用方法可以通過API文檔來了解,但是有很多方法僅僅從API說明是無法詳細(xì)了解的。我們先來說一下線程對象的幾個重要的方法:
首先我們來說明start()方法。
一個線程對象生成后,如果要產(chǎn)生一個執(zhí)行的線程,就一定要調(diào)用它的start()方法.在介紹這個方法時不得不同時說明run方法.其實線程對 象的run方法完全是一個接口回調(diào)方法,它是你這個線程對象要完成的具體邏輯.簡單說你要做什么就你在run中完成,而如何做,什么時候做就不需要你控制 了,你只要調(diào)用start()方法,JVM就會管理這個線程對象讓它產(chǎn)生一個線程并注冊到線程處理系統(tǒng)中。
從表面上看,start()方法調(diào)用了run()方法,事實上,start()方法并沒有直接調(diào)用run方法.在JDK1.5以前 start()方法是本地方法,它如何最終調(diào)用run方法已經(jīng)不是JAVA程序員所能了解的.而在JDK1.5中,原來的那個本地start()方法被 start0()代替,另個一個純JAVA的start()中調(diào)用本地方法start0(),而在start()方法中做了一個驗證,就是對一個全局變量 (對象變量)started做檢驗,如果為true,則start()拋出異常,不會調(diào)用本地方法start0(),否則,先將該變量設(shè)有true,然后 調(diào)用start0()。
從中我們可以看到這個為了控制一個線程對象只能運(yùn)行成功一次start()方法.這是因為線程的運(yùn)行要獲取當(dāng)前環(huán)境,包括安全,父線程的權(quán)限, 優(yōu)先級等條件,如果一個線程對象可以運(yùn)行多次,那么定義一個static 的線程在一個環(huán)境中獲取相應(yīng)權(quán)限和優(yōu)先級,運(yùn)行完成后它在另一個環(huán)境中利用原來的權(quán)限和優(yōu)先級等屬性在當(dāng)前環(huán)境中運(yùn)行,這樣就造成無法預(yù)知的結(jié)果.簡單說 來,讓一個線程對象只能成功運(yùn)行一次,是基于對線程管理的需要。
start()方法最本質(zhì)的功能是從CPU中申請另一個線程空間來執(zhí)行 run()方法中的代碼,它和當(dāng)前的線程是兩條線,在相對獨立的線程空間運(yùn)行,也就是說,如果你直接調(diào)用線程對象的run()方法,當(dāng)然也會執(zhí)行,但那是 在當(dāng)前線程中執(zhí)行,run()方法執(zhí)行完成后繼續(xù)執(zhí)行下面的代碼.而調(diào)用start()方法后,run()方法的代碼會和當(dāng)前線程并發(fā)(單CPU)或并行 (多CPU)執(zhí)行。
所以請記住一句話[調(diào)用線程對象的run方法不會產(chǎn)生一個新的線程],雖然可以達(dá)到相同的執(zhí)行結(jié)果,但執(zhí)行過程和執(zhí)行效率不同。
[線程的interrupt()方法,interrupted()和isInterrupted()]
這三個方法是關(guān)系非常密切而且又比較復(fù)雜的,雖然它們各自的功能很清楚,但它們之間的關(guān)系有大多數(shù)人不是真正的了解。
先說interrupt()方法,它是實例方法,而它也是最奇怪的方法,在java語言中,線程最初被設(shè)計為"隱晦難懂"的東西,直到現(xiàn)在它的 語義不沒有象它的名字那樣準(zhǔn)確。大多數(shù)人以為,一個線程象調(diào)用了interrupt()方法,那它對應(yīng)的線程就應(yīng)該被中斷而拋出異常,事實中,當(dāng)一個線程 對象調(diào)用interrupt()方法,它對應(yīng)的線程并沒有被中斷,只是改變了它的中斷狀態(tài)。
使當(dāng)前線程的狀態(tài)變以中斷狀態(tài),如果沒有其它影響,線程還會自己繼續(xù)執(zhí)行。
只有當(dāng)線程執(zhí)行到sleep,wait,join等方法時,或者自己檢查中斷狀態(tài)而拋出異常的情況下,線程才會拋出異常。
如果線程對象調(diào)用interrupt()后它對應(yīng)的線程就立即中斷,那么interrupted()方法就不可能執(zhí)行。
因為interrupted()方法是一個static方法,就是說只能在當(dāng)前線程上調(diào)用,而如果一個線程interrupt()后它已經(jīng)中斷了,那它又如何讓自己interrupted()?
正因為一個線程調(diào)用interrupt()后只是改變了中斷狀態(tài),它可以繼續(xù)執(zhí)行下去,在沒有調(diào)用sleep,wait,join等法或自己拋 出異常之前,它就可以調(diào)用interrupted()來清除中斷狀態(tài)(還會原狀)interrupted()方法會檢查當(dāng)前線程的中斷狀態(tài),如果為 "被中斷狀態(tài)"則改變當(dāng)前線程為"非中斷狀態(tài)"并返回true,如果為"非中斷狀態(tài)"則返回false,它不僅檢查當(dāng)前線程是否為中斷狀態(tài),而且在保證當(dāng) 前線程回來非中斷狀態(tài),所以它叫"interrupted",是說中斷的狀態(tài)已經(jīng)結(jié)束(到非中斷狀態(tài)了)isInterrupted()方法則僅僅檢查線 程對象對應(yīng)的線程是否是中斷狀態(tài),并不改變它的狀態(tài)。
目前大家只能先記住這三個方法的功能,只有真正深入到多線程編程實踐中,才會體會到它們?yōu)槭裁词菍ο蠓椒?為什么是類方法。
線程到底什么時候才被中斷拋出InterruptedException異常,我們將在提高篇中詳細(xì)討論。
[sleep(),join(),yield()方法]
在現(xiàn)在的環(huán)節(jié)中,我只能先說明這些方法的作用和調(diào)用原則,至于為什么,在基礎(chǔ)篇中無法深入,只能在提高篇中詳細(xì)說明。
sleep()方法中是類方法,也就是對當(dāng)前線程而言的,程序員不能指定某個線程去sleep,只能是當(dāng)前線程執(zhí)行到sleep()方法時,睡 眠指定的時間(讓其它線程運(yùn)行).事實上也只能是類方法,在當(dāng)前線程上調(diào)用.試想如果你調(diào)用一個線程對象的sleep()方法,那么這個對象對應(yīng)的線程如 果不是正在運(yùn)行,它如何sleep()?所以只有當(dāng)前線程,因為它正在執(zhí)行,你才能保證它可以調(diào)用sleep()方法。
原則:[在同步方法中盡量不要調(diào)用線程的sleep()方法],或者簡單說,對于一般水平的程序員你基本不應(yīng)該調(diào)用sleep()方法。
join()方法,正如第一節(jié)所言,在一個線程對象上調(diào)用join方法,是當(dāng)前線程等待這個線程對象對應(yīng)的線程結(jié)束,比如有兩個工作,工作A要耗時10秒鐘,工作B要耗時10秒或更多。我們在程序中先生成一個線程去做工作B,然后做工作A。
new?B().start();//做工作B
A();//做工作A
工作A完成后,下面要等待工作B的結(jié)果來進(jìn)行處理.如果工作B還沒有完成我就不能進(jìn)行下面的工作C,所以
B?b?=?new?B();
b.start();//做工作B
A();//做工作A
b.join();//等工作B完成。
C();//繼續(xù)工作C。
原則:[join是測試其它工作狀態(tài)的唯一正確方法],我見過很多人,甚至有的是博士生,在處理一項工作時如果另一項工作沒有完成,說讓當(dāng)前工 作線程sleep(x),我問他,你這個x是如何指定的,你怎么知道是100毫秒而不是99毫秒或是101毫秒?其實這就是OnXXX事件的實質(zhì),我們不 是要等多長時間才去做什么事,而是當(dāng)?shù)却墓ぷ髡猛瓿傻臅r候去做。
yield()方法也是類方法,只在當(dāng)前線程上調(diào)用,理由同上,它主是讓當(dāng)前線程放棄本次分配到的時間片原則:[不是非常必要的情況下,沒有理 由調(diào)用它].調(diào)用這個方法不會提高任何效率,只是降低了CPU的總周期上面介紹的線程一些方法,基于(基礎(chǔ)篇)而言只能簡單提及.以后具體應(yīng)用中我會結(jié)合 實例詳細(xì)論述。
線程本身的其它方法請參看API文檔.下一節(jié)介紹非線程的方法,但和線程密切相關(guān)的兩[三]個對象方法:
[wait(),notify()/notifyAll()]
這是在多線程中非常重要的方法。
關(guān)于這兩個方法,有很多的內(nèi)容需要說明.在下面的說明中可能會有很多地方不能一下子明白,但在看完本節(jié)后,即使不能完全明白,你也一定要回過頭來記住下面的兩句話:
[wait(),notify()/notityAll()方法是普通對象的方法(Object超類中實現(xiàn)),而不是線程對象的方法]
[wait(),notify()/notityAll()方法只能在同步方法中調(diào)用]
[線程的互斥控制]
多個線程同時操作某一對象時,一個線程對該對象的操作可能會改變其狀態(tài),而該狀態(tài)會影響另一線程對該對象的真正結(jié)果.
這個例子我們在太多的文檔中可以看到,就象兩個操售票員同時售出同一張票一樣.
線程A 線程B
1.線程A在數(shù)據(jù)庫中查詢存票,發(fā)現(xiàn)票C可以賣出
class="left"2.線程A接受用戶訂票請求,準(zhǔn)備出票.
3.這時切換到了線程B執(zhí)行
4.線程B在數(shù)據(jù)庫中查詢存票,發(fā)現(xiàn)票C可以賣出
5.線程B將票賣了出去
6.切換到線程A執(zhí)行,線程A賣了一張已經(jīng)賣出的票
所以需要一種機(jī)制來管理這類問題的發(fā)生,當(dāng)某個線程正在執(zhí)行一個不可分割的部分時,其它線程不能不能同時執(zhí)行這一部分.
象這種控制某一時刻只能有一個線程執(zhí)行某個執(zhí)行單元的機(jī)制就叫互斥控制或共享互斥(mutual exclusion)
在JAVA中,用synchornized關(guān)鍵字來實現(xiàn)互斥控制(暫時這樣認(rèn)為,JDK1.5已經(jīng)發(fā)展了新的機(jī)制)
[synchornized關(guān)鍵字]
把一個單元聲明為synchornized,就可以讓在同一時間只有一個線程操作該方法.
有人說synchornized就是一把鎖,事實上它確實存在鎖,但是是誰的鎖,鎖誰,這是一個非常復(fù)雜的問題.
每個對象只有一把監(jiān)視鎖(monitor lock),一次只能被一個線程獲取.當(dāng)一個線程獲取了這一個鎖后,其它線程就只能等待這個線程釋放鎖才能再獲取.
那么synchornized關(guān)鍵字到底鎖什么?得到了誰的鎖?
對于同步塊,synchornized獲取的是參數(shù)中的對象鎖:
synchornized(obj){ //............... } 線程執(zhí)行到這里時,首先要獲取obj這個實例的鎖,如果沒有獲取到線程只能等待.如果多個線程執(zhí)行到這里,只能有一個線程獲取obj的鎖,然后執(zhí)行{}中的語句,所以,obj對象的作用范圍不同,控制程序不同.
假如:
public void test(){ Object o = new Object(); synchornized(obj){ //............... } } 這段程序控制不了任何,多個線程之間執(zhí)行到Object o = new Object();時會各自產(chǎn)生一個對象然后獲取這個對象有監(jiān)視鎖,各自皆大歡喜地執(zhí)行.
而如果是類的屬性:
class Test{ Object o = new Object(); public void test(){ synchornized(o){ //............... } }} 所有執(zhí)行到Test實例的synchornized(o)的線程,只有一個線程可以獲取到監(jiān)視鎖.
有時我們會這樣:
public void test(){ synchornized(this){ //............... } } 那么所有執(zhí)行Test實例的線程只能有一個線程執(zhí)行.而synchornized(o)和synchornized(this)的范圍是不同 的,因為執(zhí)行到Test實例的synchornized(o)的線程等待時,其它線程可以執(zhí)行Test實例的synchornized(o1)部分,但多 個線程同時只有一個可以執(zhí)行Test實例的synchornized(this).]
而對于
synchornized(Test.class){ //............... } 這樣的同步塊而言,所有調(diào)用Test多個實例的線程賜教只能有一個線程可以執(zhí)行.
[synchornized方法]
如果一個方法聲明為synchornized的,則等同于把在為個方法上調(diào)用synchornized(this).
如果一個靜態(tài)方法被聲明為synchornized,則等同于把在為個方法上調(diào)用synchornized(類.class).
現(xiàn)在進(jìn)入wait方法和notify/notifyAll方法.這兩個(或叫三個)方法都是Object對象的方法,而不是線程對象的方法.如同鎖一樣,它們是在線程中調(diào)用某一對象上執(zhí)行的.
class Test{ public synchornized void test(){ //獲取條件,int x 要求大于100; if(x < 100) wait(); } } 這里為了說明方法沒有加在try{}catch(){}中,如果沒有明確在哪個對象上調(diào)用wait()方法,則為this.wait();
假如:
Test t = new Test();
現(xiàn)在有兩個線程都執(zhí)行到t.test();方法.其中線程A獲取了t的對象鎖,進(jìn)入test()方法內(nèi).
這時x小于100,所以線程A進(jìn)入等待.
當(dāng)一個線程調(diào)用了wait方法后,這個線程就進(jìn)入了這個對象的休息室(waitset),這是一個虛擬的對象,但JVM中一定存在這樣的一個數(shù)據(jù)結(jié)構(gòu)用來記錄當(dāng)前對象中有哪些程線程在等待.
當(dāng)一個線程進(jìn)入等待時,它就會釋放鎖,讓其它線程來獲取這個鎖.
所以線程B有機(jī)會獲得了線程A釋放的鎖,進(jìn)入test()方法,如果這時x還是小于100,線程B也進(jìn)入了t的休息室.
這兩個線程只能等待其它線程調(diào)用notity[All]來喚醒.
但是如果調(diào)用的是有參數(shù)的wait(time)方法,則線程A,B都會在休息室中等待這個時間后自動喚醒.
[為什么真正的應(yīng)用都是用while(條件)而不用if(條件)]
在實際的編程中我們看到大量的例子都是用?
while(x < 100)
wait();go();而不是用if,為什么呢?
在多個線程同時執(zhí)行時,if(x <100)是不安全的.因為如果線程A和線程B都在t的休息室中等待,這時另一個線程使x==100了,并調(diào)用notifyAll方法,線程A繼續(xù) 執(zhí)行下面的go().而它執(zhí)行完成后,x有可能又小于100,比如下面的程序中調(diào)用了--x,這時切換到線程B,線程B沒有繼續(xù)判斷,直接執(zhí)行g(shù)o(); 就產(chǎn)生一個錯誤的條件,只有while才能保證線程B又繼續(xù)檢查一次.
[notify/notifyAll方法]
這兩個方法都是把某個對象上休息區(qū)內(nèi)的線程喚醒,notify只能喚醒一個,但究竟是哪一個不能確定,而notifyAll則喚醒這個對象上的休息室中所有的線程.
一般有為了安全性,我們在絕對多數(shù)時候應(yīng)該使用notifiAll(),除非你明確知道只喚醒其中的一個線程.
那么是否是只要調(diào)用一個對象的wait()方法,當(dāng)前線程就進(jìn)入了這個對象的休息室呢?事實中,要調(diào)用一個對象的wait()方法,只有當(dāng)前線程獲取了這個對象的鎖,換句話說一定要在這個對象的同步方法或以這個對象為參數(shù)的同步塊中.
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(); } } 這個線程就不會進(jìn)入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(); } }
編輯推薦
JAVA認(rèn)證考試報考指南 / 更多JAVA考試資料
(責(zé)任編輯:xy)