轉(zhuǎn)帖|其它|編輯:郝浩|2011-02-01 10:13:56.000|閱讀 636 次
概述:一個(gè)多線程應(yīng)用可能運(yùn)行了好幾天都沒(méi)問(wèn)題,然后突然就出現(xiàn)了問(wèn)題,之后卻又無(wú)法再次重現(xiàn)出來(lái)。如果在正確性之外,還需要考慮應(yīng)用的吞吐量和性能優(yōu)化的話,就會(huì)更加復(fù)雜。本文主要介紹Java中的線程的基本概念、可見(jiàn)性和線程同步相關(guān)的內(nèi)容。
# 界面/圖表報(bào)表/文檔/IDE等千款熱門(mén)軟控件火熱銷(xiāo)售中 >>
開(kāi)發(fā)高性能并發(fā)應(yīng)用不是一件容易的事情。這類應(yīng)用的例子包括高性能Web服務(wù)器、游戲服務(wù)器和搜索引擎爬蟲(chóng)等。這樣的應(yīng)用可能需要同時(shí)處理成千上萬(wàn)個(gè)請(qǐng)求。對(duì)于這樣的應(yīng)用,一般采用多線程或事件驅(qū)動(dòng)的架構(gòu)。對(duì)于Java來(lái)說(shuō),在語(yǔ)言內(nèi)部提供了線程的支持。但是Java的多線程應(yīng)用開(kāi)發(fā)會(huì)遇到很多問(wèn)題。首先是很難編寫(xiě)正確,其次是很難測(cè)試是否正確,最后是出現(xiàn)問(wèn)題時(shí)很難調(diào)試。一個(gè)多線程應(yīng)用可能運(yùn)行了好幾天都沒(méi)問(wèn)題,然后突然就出現(xiàn)了問(wèn)題,之后卻又無(wú)法再次重現(xiàn)出來(lái)。如果在正確性之外,還需要考慮應(yīng)用的吞吐量和性能優(yōu)化的話,就會(huì)更加復(fù)雜。本文主要介紹Java中的線程的基本概念、可見(jiàn)性和線程同步相關(guān)的內(nèi)容。
Java線程基本概念
在操作系統(tǒng)中兩個(gè)比較容易混淆的概念是進(jìn)程(process)和線程(thread)。操作系統(tǒng)中的進(jìn)程是資源的組織單位。進(jìn)程有一個(gè)包含了程序內(nèi)容和數(shù)據(jù)的地址空間,以及其它的資源,包括打開(kāi)的文件、子進(jìn)程和信號(hào)處理器等。不同進(jìn)程的地址空間是互相隔離的。而線程表示的是程序的執(zhí)行流程,是CPU調(diào)度的基本單位。線程有自己的程序計(jì)數(shù)器、寄存器、棧和幀等。引入線程的動(dòng)機(jī)在于操作系統(tǒng)中阻塞式I/O的存在。當(dāng)一個(gè)線程所執(zhí)行的I/O被阻塞的時(shí)候,同一進(jìn)程中的其它線程可以使用CPU來(lái)進(jìn)行計(jì)算。這樣的話,就提高了應(yīng)用的執(zhí)行效率。線程的概念在主流的操作系統(tǒng)和編程語(yǔ)言中都得到了支持。
一部分的Java程序是單線程的。程序的機(jī)器指令按照程序中給定的順序依次執(zhí)行。Java語(yǔ)言提供了java.lang.Thread類來(lái)為線程提供抽象。有兩種方式創(chuàng)建一個(gè)新的線程:一種是繼承java.lang.Thread類并覆寫(xiě)其中的run()方法,另外一種則是在創(chuàng)建java.lang.Thread類的對(duì)象的時(shí)候,在構(gòu)造函數(shù)中提供一個(gè)實(shí)現(xiàn)了java.lang.Runnable接口的類的對(duì)象。在得到了java.lang.Thread類的對(duì)象之后,通過(guò)調(diào)用其start()方法就可以啟動(dòng)這個(gè)線程的執(zhí)行。
一個(gè)線程被創(chuàng)建成功并啟動(dòng)之后,可以處在不同的狀態(tài)中。這個(gè)線程可能正在占用CPU時(shí)間運(yùn)行;也可能處在就緒狀態(tài),等待被調(diào)度執(zhí)行;還可能阻塞在某個(gè)資源或是事件上。多個(gè)就緒狀態(tài)的線程會(huì)競(jìng)爭(zhēng)CPU時(shí)間以獲得被執(zhí)行的機(jī)會(huì),而CPU則采用某種算法來(lái)調(diào)度線程的執(zhí)行。不同線程的運(yùn)行順序是不確定的,多線程程序中的邏輯不能依賴于CPU的調(diào)度算法。
可見(jiàn)性
可見(jiàn)性(visibility)的問(wèn)題是Java多線程應(yīng)用中的錯(cuò)誤的根源。在一個(gè)單線程程序中,如果首先改變一個(gè)變量的值,再讀取該變量的值的時(shí)候,所讀取到的值就是上次寫(xiě)操作寫(xiě)入的值。也就是說(shuō)前面操作的結(jié)果對(duì)后面的操作是肯定可見(jiàn)的。但是在多線程程序中,如果不使用一定的同步機(jī)制,就不能保證一個(gè)線程所寫(xiě)入的值對(duì)另外一個(gè)線程是可見(jiàn)的。造成這種情況的原因可能有下面幾個(gè):
CPU 內(nèi)部的緩存:現(xiàn)在的CPU一般都擁有層次結(jié)構(gòu)的幾級(jí)緩存。CPU直接操作的是緩存中的數(shù)據(jù),并在需要的時(shí)候把緩存中的數(shù)據(jù)與主存進(jìn)行同步。因此在某些時(shí)刻,緩存中的數(shù)據(jù)與主存內(nèi)的數(shù)據(jù)可能是不一致的。某個(gè)線程所執(zhí)行的寫(xiě)入操作的新值可能當(dāng)前還保存在CPU的緩存中,還沒(méi)有被寫(xiě)回到主存中。這個(gè)時(shí)候,另外一個(gè)線程的讀取操作讀取的就還是主存中的舊值。
CPU的指令執(zhí)行順序:在某些時(shí)候,CPU可能改變指令的執(zhí)行順序。這有可能導(dǎo)致一個(gè)線程過(guò)早的看到另外一個(gè)線程的寫(xiě)入操作完成之后的新值。
編譯器代碼重排:出于性能優(yōu)化的目的,編譯器可能在編譯的時(shí)候?qū)ι傻哪繕?biāo)代碼進(jìn)行重新排列。
現(xiàn)實(shí)的情況是:不同的CPU可能采用不同的架構(gòu),而這樣的問(wèn)題在多核處理器和多處理器系統(tǒng)中變得尤其復(fù)雜。而Java的目標(biāo)是要實(shí)現(xiàn)“編寫(xiě)一次,到處運(yùn)行”,因此就有必要對(duì)Java程序訪問(wèn)和操作主存的方式做出規(guī)范,以保證同樣的程序在不同的CPU架構(gòu)上的運(yùn)行結(jié)果是一致的。Java內(nèi)存模型(Java Memory Model)就是為了這個(gè)目的而引入的。JSR 133則進(jìn)一步修正了之前的內(nèi)存模型中存在的問(wèn)題??偟脕?lái)說(shuō),Java內(nèi)存模型描述了程序中共享變量的關(guān)系以及在主存中寫(xiě)入和讀取這些變量值的底層細(xì)節(jié)。Java內(nèi)存模型定義了Java語(yǔ)言中的synchronized、volatile和final等關(guān)鍵詞對(duì)主存中變量讀寫(xiě)操作的意義。Java開(kāi)發(fā)人員使用這些關(guān)鍵詞來(lái)描述程序所期望的行為,而編譯器和JVM負(fù)責(zé)保證生成的代碼在運(yùn)行時(shí)刻的行為符合內(nèi)存模型的描述。比如對(duì)聲明為volatile的變量來(lái)說(shuō),在讀取之前,JVM會(huì)確保CPU中緩存的值首先會(huì)失效,重新從主存中進(jìn)行讀??;而寫(xiě)入之后,新的值會(huì)被馬上寫(xiě)入到主存中。而synchronized和volatile關(guān)鍵詞也會(huì)對(duì)編譯器優(yōu)化時(shí)候的代碼重排帶來(lái)額外的限制。比如編譯器不能把 synchronized塊中的代碼移出來(lái)。對(duì)volatile變量的讀寫(xiě)操作是不能與其它讀寫(xiě)操作一塊重新排列的。
Java 內(nèi)存模型中一個(gè)重要的概念是定義了“在之前發(fā)生(happens-before)”的順序。如果一個(gè)動(dòng)作按照“在之前發(fā)生”的順序發(fā)生在另外一個(gè)動(dòng)作之前,那么前一個(gè)動(dòng)作的結(jié)果在多線程的情況下對(duì)于后一個(gè)動(dòng)作就是肯定可見(jiàn)的。最常見(jiàn)的“在之前發(fā)生”的順序包括:對(duì)一個(gè)對(duì)象上的監(jiān)視器的解鎖操作肯定發(fā)生在下一個(gè)對(duì)同一個(gè)監(jiān)視器的加鎖操作之前;對(duì)聲明為volatile的變量的寫(xiě)操作肯定發(fā)生在后續(xù)的讀操作之前。有了“在之前發(fā)生”順序,多線程程序在運(yùn)行時(shí)刻的行為在關(guān)鍵部分上就是可預(yù)測(cè)的了。編譯器和JVM會(huì)確保“在之前發(fā)生”順序可以得到保證。比如下面的一個(gè)簡(jiǎn)單的方法:
public void increase() {
this.count++;
}
這是一個(gè)常見(jiàn)的計(jì)數(shù)器遞增方法,this.count++實(shí)際是this.count = this.count + 1,由一個(gè)對(duì)變量this.count的讀取操作和寫(xiě)入操作組成。如果在多線程情況下,兩個(gè)線程執(zhí)行這兩個(gè)操作的順序是不可預(yù)期的。如果 this.count的初始值是1,兩個(gè)線程可能都讀到了為1的值,然后先后把this.count的值設(shè)為2,從而產(chǎn)生錯(cuò)誤。錯(cuò)誤的原因在于其中一個(gè)線程對(duì)this.count的寫(xiě)入操作對(duì)另外一個(gè)線程是不可見(jiàn)的,另外一個(gè)線程不知道this.count的值已經(jīng)發(fā)生了變化。如果在increase() 方法聲明中加上synchronized關(guān)鍵詞,那就在兩個(gè)線程的操作之間強(qiáng)制定義了一個(gè)“在之前發(fā)生”順序。一個(gè)線程需要首先獲得當(dāng)前對(duì)象上的鎖才能執(zhí)行,在它擁有鎖的這段時(shí)間完成對(duì)this.count的寫(xiě)入操作。而另一個(gè)線程只有在當(dāng)前線程釋放了鎖之后才能執(zhí)行。這樣的話,就保證了兩個(gè)線程對(duì) increase()方法的調(diào)用只能依次完成,保證了線程之間操作上的可見(jiàn)性。
如果一個(gè)變量的值可能被多個(gè)線程讀取,又能被最少一個(gè)線程鎖寫(xiě)入,同時(shí)這些讀寫(xiě)操作之間并沒(méi)有定義好的“在之前發(fā)生”的順序的話,那么在這個(gè)變量上就存在數(shù)據(jù)競(jìng)爭(zhēng)(data race)。數(shù)據(jù)競(jìng)爭(zhēng)的存在是Java多線程應(yīng)用中要解決的首要問(wèn)題。解決的辦法就是通過(guò)synchronized和volatile關(guān)鍵詞來(lái)定義好“在之前發(fā)生”順序。
Java中的鎖
當(dāng)數(shù)據(jù)競(jìng)爭(zhēng)存在的時(shí)候,最簡(jiǎn)單的解決辦法就是加鎖。鎖機(jī)制限制在同一時(shí)間只允許一個(gè)線程訪問(wèn)產(chǎn)生競(jìng)爭(zhēng)的數(shù)據(jù)的臨界區(qū)。Java語(yǔ)言中的 synchronized關(guān)鍵字可以為一個(gè)代碼塊或是方法進(jìn)行加鎖。任何Java對(duì)象都有一個(gè)自己的監(jiān)視器,可以進(jìn)行加鎖和解鎖操作。當(dāng)受到 synchronized關(guān)鍵字保護(hù)的代碼塊或方法被執(zhí)行的時(shí)候,就說(shuō)明當(dāng)前線程已經(jīng)成功的獲取了對(duì)象的監(jiān)視器上的鎖。當(dāng)代碼塊或是方法正常執(zhí)行完成或是發(fā)生異常退出的時(shí)候,當(dāng)前線程所獲取的鎖會(huì)被自動(dòng)釋放。一個(gè)線程可以在一個(gè)Java對(duì)象上加多次鎖。同時(shí)JVM保證了在獲取鎖之前和釋放鎖之后,變量的值是與主存中的內(nèi)容同步的。
Java線程的同步
在有些情況下,僅依靠線程之間對(duì)數(shù)據(jù)的互斥訪問(wèn)是不夠的。有些線程之間存在協(xié)作關(guān)系,需要按照一定的協(xié)議來(lái)協(xié)同完成某項(xiàng)任務(wù),比如典型的生產(chǎn)者-消費(fèi)者模式。這種情況下就需要用到Java提供的線程之間的等待-通知機(jī)制。當(dāng)線程所要求的條件不滿足時(shí),就進(jìn)入等待狀態(tài);而另外的線程則負(fù)責(zé)在合適的時(shí)機(jī)發(fā)出通知來(lái)喚醒等待中的線程。Java中的java.lang.Object類中的wait/notify/notifyAll方法組就是完成線程之間的同步的。
在某個(gè)Java對(duì)象上面調(diào)用wait方法的時(shí)候,首先要檢查當(dāng)前線程是否獲取到了這個(gè)對(duì)象上的鎖。如果沒(méi)有的話,就會(huì)直接拋出java.lang.IllegalMonitorStateException異常。如果有鎖的話,就把當(dāng)前線程添加到對(duì)象的等待集合中,并釋放其所擁有的鎖。當(dāng)前線程被阻塞,無(wú)法繼續(xù)執(zhí)行,直到被從對(duì)象的等待集合中移除。引起某個(gè)線程從對(duì)象的等待集合中移除的原因有很多:對(duì)象上的notify方法被調(diào)用時(shí),該線程被選中;對(duì)象上的notifyAll方法被調(diào)用;線程被中斷;對(duì)于有超時(shí)限制的wait操作,當(dāng)超過(guò)時(shí)間限制時(shí);JVM內(nèi)部實(shí)現(xiàn)在非正常情況下的操作。
從上面的說(shuō)明中,可以得到幾條結(jié)論:wait/notify/notifyAll操作需要放在synchronized代碼塊或方法中,這樣才能保證在執(zhí)行 wait/notify/notifyAll的時(shí)候,當(dāng)前線程已經(jīng)獲得了所需要的鎖。當(dāng)對(duì)于某個(gè)對(duì)象的等待集合中的線程數(shù)目沒(méi)有把握的時(shí)候,最好使用 notifyAll而不是notify。notifyAll雖然會(huì)導(dǎo)致線程在沒(méi)有必要的情況下被喚醒而產(chǎn)生性能影響,但是在使用上更加簡(jiǎn)單一些。由于線程可能在非正常情況下被意外喚醒,一般需要把wait操作放在一個(gè)循環(huán)中,并檢查所要求的邏輯條件是否滿足。典型的使用模式如下所示:
private Object lock = new Object();
synchronized (lock) {
while (/* 邏輯條件不滿足的時(shí)候 */) {
try {
lock.wait();
} catch (InterruptedException e) {}
}
//處理邏輯
}
上述代碼中使用了一個(gè)私有對(duì)象lock來(lái)作為加鎖的對(duì)象,其好處是可以避免其它代碼錯(cuò)誤的使用這個(gè)對(duì)象。
中斷線程
通過(guò)一個(gè)線程對(duì)象的interrupt()方法可以向該線程發(fā)出一個(gè)中斷請(qǐng)求。中斷請(qǐng)求是一種線程之間的協(xié)作方式。當(dāng)線程A通過(guò)調(diào)用線程B的interrupt()方法來(lái)發(fā)出中斷請(qǐng)求的時(shí)候,線程A 是在請(qǐng)求線程B的注意。線程B應(yīng)該在方便的時(shí)候來(lái)處理這個(gè)中斷請(qǐng)求,當(dāng)然這不是必須的。當(dāng)中斷發(fā)生的時(shí)候,線程對(duì)象中會(huì)有一個(gè)標(biāo)記來(lái)記錄當(dāng)前的中斷狀態(tài)。通過(guò)isInterrupted()方法可以判斷是否有中斷請(qǐng)求發(fā)生。如果當(dāng)中斷請(qǐng)求發(fā)生的時(shí)候,線程正處于阻塞狀態(tài),那么這個(gè)中斷請(qǐng)求會(huì)導(dǎo)致該線程退出阻塞狀態(tài)。可能造成線程處于阻塞狀態(tài)的情況有:當(dāng)線程通過(guò)調(diào)用wait()方法進(jìn)入一個(gè)對(duì)象的等待集合中,或是通過(guò)sleep()方法來(lái)暫時(shí)休眠,或是通過(guò)join()方法來(lái)等待另外一個(gè)線程完成的時(shí)候。在線程阻塞的情況下,當(dāng)中斷發(fā)生的時(shí)候,會(huì)拋出java.lang.InterruptedException,代碼會(huì)進(jìn)入相應(yīng)的異常處理邏輯之中。實(shí)際上在調(diào)用wait/sleep/join方法的時(shí)候,是必須捕獲這個(gè)異常的。中斷一個(gè)正在某個(gè)對(duì)象的等待集合中的線程,會(huì)使得這個(gè)線程從等待集合中被移除,使得它可以在再次獲得鎖之后,繼續(xù)執(zhí)行java.lang.InterruptedException異常的處理邏輯。
通過(guò)中斷線程可以實(shí)現(xiàn)可取消的任務(wù)。在任務(wù)的執(zhí)行過(guò)程中可以定期檢查當(dāng)前線程的中斷標(biāo)記,如果線程收到了中斷請(qǐng)求,那么就可以終止這個(gè)任務(wù)的執(zhí)行。當(dāng)遇到 java.lang.InterruptedException的異常,不要捕獲了之后不做任何處理。如果不想在這個(gè)層次上處理這個(gè)異常,就把異常重新拋出。當(dāng)一個(gè)在阻塞狀態(tài)的線程被中斷并且拋出java.lang.InterruptedException異常的時(shí)候,其對(duì)象中的中斷狀態(tài)標(biāo)記會(huì)被清空。如果捕獲了java.lang.InterruptedException異常但是又不能重新拋出的話,需要通過(guò)再次調(diào)用interrupt()方法來(lái)重新設(shè)置這個(gè)標(biāo)記。
本站文章除注明轉(zhuǎn)載外,均為本站原創(chuàng)或翻譯。歡迎任何形式的轉(zhuǎn)載,但請(qǐng)務(wù)必注明出處、不得修改原文相關(guān)鏈接,如果存在內(nèi)容上的異議請(qǐng)郵件反饋至chenjj@fc6vip.cn
文章轉(zhuǎn)載自:網(wǎng)絡(luò)轉(zhuǎn)載