Java深度歷險(三)Java線程基本概念可見性與同步_第1頁
Java深度歷險(三)Java線程基本概念可見性與同步_第2頁
Java深度歷險(三)Java線程基本概念可見性與同步_第3頁
Java深度歷險(三)Java線程基本概念可見性與同步_第4頁
全文預(yù)覽已結(jié)束

下載本文檔

版權(quán)說明:本文檔由用戶提供并上傳,收益歸屬內(nèi)容提供方,若內(nèi)容存在侵權(quán),請進行舉報或認(rèn)領(lǐng)

文檔簡介

1、Java深度歷險(三)Java線程?:基本概念、可見性與同步開發(fā)高性能并發(fā)應(yīng)用不是一件容易的事情。這類應(yīng)用的例子包括高性能Web服務(wù)器、游戲服務(wù)器和搜索引擎爬蟲等。這樣的應(yīng)用可能需要同時處理成千上萬個請求。對于這樣的應(yīng)用,一般采用多線程或事件驅(qū)動的架構(gòu)。 對于Java來說,在語言內(nèi)部提供了線程的支持。但是Java的多線程應(yīng)用開發(fā)會遇到很多問題。首先是很難編寫正確,其次是很難測試是否正確,最后是出現(xiàn) 問題時很難調(diào)試。一個多線程應(yīng)用可能運行了好幾天都沒問題,然后突然就出現(xiàn)了問題,之后卻又無法再次重現(xiàn)出來。如果在正確性之外,還需要考慮應(yīng)用的吞吐量 和性能優(yōu)化的話,就會更加復(fù)雜。本文主要介紹Java中

2、的線程的基本概念、可見性和線程同步相關(guān)的內(nèi)容。Java線程基本概念在操作系統(tǒng)中兩個比較容易混淆的概念是進程(process)和線程(thread)。 操作系統(tǒng)中的進程是資源的組織單位。進程有一個包含了程序內(nèi)容和數(shù)據(jù)的地址空間,以及其它的資源,包括打開的文件、子進程和信號處理器等。不同進程的地址 空間是互相隔離的。而線程表示的是程序的執(zhí)行流程,是CPU調(diào)度的基本單位。線程有自己的程序計數(shù)器、寄存器、棧和幀等。引入線程的動機在于操作系統(tǒng)中阻 塞式I/O的存在。當(dāng)一個線程所執(zhí)行的I/O被阻塞的時候,同一進程中的其它線程可以使用CPU來進行計算。這樣的話,就提高了應(yīng)用的執(zhí)行效率。線程的概 念在主流的操

3、作系統(tǒng)和編程語言中都得到了支持。一部分的Java程序是單線程的。程序的機器指令按照程序中給定的順序依次執(zhí)行。Java語言提供了java.lang.Thread類來為線程提供抽象。有兩種方式創(chuàng)建一個新的線程:一種是繼承java.lang.Thread類并覆寫其中的run()方法,另外一種則是在創(chuàng)建java.lang.Thread類的對象的時候,在構(gòu)造函數(shù)中提供一個實現(xiàn)了java.lang.Runnable接口的類的對象。在得到了java.lang.Thread類的對象之后,通過調(diào)用其start()方法就可以啟動這個線程的執(zhí)行。一個線程被創(chuàng)建成功并啟動之后,可以處在不同的狀態(tài)中。這個線程可能正在占

4、用CPU時間運行;也可能處在就緒狀態(tài),等待被調(diào)度執(zhí)行;還可能阻塞在某 個資源或是事件上。多個就緒狀態(tài)的線程會競爭CPU時間以獲得被執(zhí)行的機會,而CPU則采用某種算法來調(diào)度線程的執(zhí)行。不同線程的運行順序是不確定的,多 線程程序中的邏輯不能依賴于CPU的調(diào)度算法。可見性可見性(visibility)的問題是Java多線程應(yīng)用中的錯誤的根源。在一個單線程程序中,如果首先改變一個變量的值,再讀取該變量的值的時 候,所讀取到的值就是上次寫操作寫入的值。也就是說前面操作的結(jié)果對后面的操作是肯定可見的。但是在多線程程序中,如果不使用一定的同步機制,就不能保證 一個線程所寫入的值對另外一個線程是可見的。造成這

5、種情況的原因可能有下面幾個: CPU 內(nèi)部的緩存:現(xiàn)在的CPU一般都擁有層次結(jié)構(gòu)的幾級緩存。CPU直接操作的是緩存中的數(shù)據(jù),并在需要的時候把緩存中的數(shù)據(jù)與主存進行同步。因此在某些時 刻,緩存中的數(shù)據(jù)與主存內(nèi)的數(shù)據(jù)可能是不一致的。某個線程所執(zhí)行的寫入操作的新值可能當(dāng)前還保存在CPU的緩存中,還沒有被寫回到主存中。這個時候,另外 一個線程的讀取操作讀取的就還是主存中的舊值。 CPU的指令執(zhí)行順序:在某些時候,CPU可能改變指令的執(zhí)行順序。這有可能導(dǎo)致一個線程過早的看到另外一個線程的寫入操作完成之后的新值。 編譯器代碼重排:出于性能優(yōu)化的目的,編譯器可能在編譯的時候?qū)ι傻哪繕?biāo)代碼進行重新排列。現(xiàn)實

6、的情況是:不同的CPU可能采用不同的架構(gòu),而這樣的問題在多核處理器和多處理器系統(tǒng)中變得尤其復(fù)雜。而Java的目標(biāo)是要實現(xiàn)“編寫一次,到 處運行”,因此就有必要對Java程序訪問和操作主存的方式做出規(guī)范,以保證同樣的程序在不同的CPU架構(gòu)上的運行結(jié)果是一致的。Java內(nèi)存模型(Java Memory Model)就是為了這個目的而引入的。JSR 133則進一步修正了之前的內(nèi)存模型中存在的問題。總得來說,Java內(nèi)存模型描述了程序中共享變量的關(guān)系以及在主存中寫入和讀取這些變量值的底層細(xì)節(jié)。Java內(nèi)存模型定義了Java語言中的synchronized、volatile和final等 關(guān)鍵詞對主存中

7、變量讀寫操作的意義。Java開發(fā)人員使用這些關(guān)鍵詞來描述程序所期望的行為,而編譯器和JVM負(fù)責(zé)保證生成的代碼在運行時刻的行為符合內(nèi) 存模型的描述。比如對聲明為volatile的變量來說,在讀取之前,JVM會確保CPU中緩存的值首先會失效,重新從主存中進行讀取;而寫入之后,新的 值會被馬上寫入到主存中。而synchronized和volatile關(guān)鍵詞也會對編譯器優(yōu)化時候的代碼重排帶來額外的限制。比如編譯器不能把 synchronized塊中的代碼移出來。對volatile變量的讀寫操作是不能與其它讀寫操作一塊重新排列的。Java 內(nèi)存模型中一個重要的概念是定義了“在之前發(fā)生(happens-b

8、efore)”的順序。如果一個動作按照“在之前發(fā)生”的順序發(fā)生在另外一個動作之 前,那么前一個動作的結(jié)果在多線程的情況下對于后一個動作就是肯定可見的。最常見的“在之前發(fā)生”的順序包括:對一個對象上的監(jiān)視器的解鎖操作肯定發(fā)生在 下一個對同一個監(jiān)視器的加鎖操作之前;對聲明為volatile的變量的寫操作肯定發(fā)生在后續(xù)的讀操作之前。有了“在之前發(fā)生”順序,多線程程序在運行時 刻的行為在關(guān)鍵部分上就是可預(yù)測的了。編譯器和JVM會確?!霸谥鞍l(fā)生”順序可以得到保證。比如下面的一個簡單的方法: public void increase() this.count+; 這是一個常見的計數(shù)器遞增方法,this.

9、count+實際是this.count = this.count + 1,由一個對變量this.count的讀取操作和寫入操作組成。如果在多線程情況下,兩個線程執(zhí)行這兩個操作的順序是不可預(yù)期的。如果 this.count的初始值是1,兩個線程可能都讀到了為1的值,然后先后把this.count的值設(shè)為2,從而產(chǎn)生錯誤。錯誤的原因在于其中一個線 程對this.count的寫入操作對另外一個線程是不可見的,另外一個線程不知道this.count的值已經(jīng)發(fā)生了變化。如果在increase() 方法聲明中加上synchronized關(guān)鍵詞,那就在兩個線程的操作之間強制定義了一個“在之前發(fā)生”順序。一個

10、線程需要首先獲得當(dāng)前對象上的鎖才能執(zhí) 行,在它擁有鎖的這段時間完成對this.count的寫入操作。而另一個線程只有在當(dāng)前線程釋放了鎖之后才能執(zhí)行。這樣的話,就保證了兩個線程對 increase()方法的調(diào)用只能依次完成,保證了線程之間操作上的可見性。如果一個變量的值可能被多個線程讀取,又能被最少一個線程鎖寫入,同時這些讀寫操作之間并沒有定義好的“在之前發(fā)生”的順序的話,那么在這個變量上 就存在數(shù)據(jù)競爭(data race)。數(shù)據(jù)競爭的存在是Java多線程應(yīng)用中要解決的首要問題。解決的辦法就是通過synchronized和volatile關(guān)鍵詞來定義好“在 之前發(fā)生”順序。Java中的鎖當(dāng)數(shù)據(jù)

11、競爭存在的時候,最簡單的解決辦法就是加鎖。鎖機制限制在同一時間只允許一個線程訪問產(chǎn)生競爭的數(shù)據(jù)的臨界區(qū)。Java語言中的 synchronized關(guān)鍵字可以為一個代碼塊或是方法進行加鎖。任何Java對象都有一個自己的監(jiān)視器,可以進行加鎖和解鎖操作。當(dāng)受到 synchronized關(guān)鍵字保護的代碼塊或方法被執(zhí)行的時候,就說明當(dāng)前線程已經(jīng)成功的獲取了對象的監(jiān)視器上的鎖。當(dāng)代碼塊或是方法正常執(zhí)行完成或是 發(fā)生異常退出的時候,當(dāng)前線程所獲取的鎖會被自動釋放。一個線程可以在一個Java對象上加多次鎖。同時JVM保證了在獲取鎖之前和釋放鎖之后,變量的值 是與主存中的內(nèi)容同步的。Java線程的同步在有些情況

12、下,僅依靠線程之間對數(shù)據(jù)的互斥訪問是不夠的。有些線程之間存在協(xié)作關(guān)系,需要按照一定的協(xié)議來協(xié)同完成某項任務(wù),比如典型的生產(chǎn)者-消 費者模式。這種情況下就需要用到Java提供的線程之間的等待-通知機制。當(dāng)線程所要求的條件不滿足時,就進入等待狀態(tài);而另外的線程則負(fù)責(zé)在合適的時機 發(fā)出通知來喚醒等待中的線程。Java中的java.lang.Object類中的wait/notify/notifyAll方法組就是完成線程之間的同步的。在某個Java對象上面調(diào)用wait方法的時候,首先要檢查當(dāng)前線程是否獲取到了這個對象上的鎖。如果沒有的話,就會直接拋出java.lang.IllegalMonitorSta

13、teException異 常。如果有鎖的話,就把當(dāng)前線程添加到對象的等待集合中,并釋放其所擁有的鎖。當(dāng)前線程被阻塞,無法繼續(xù)執(zhí)行,直到被從對象的等待集合中移除。引起某個線 程從對象的等待集合中移除的原因有很多:對象上的notify方法被調(diào)用時,該線程被選中;對象上的notifyAll方法被調(diào)用;線程被中斷;對于有超 時限制的wait操作,當(dāng)超過時間限制時;JVM內(nèi)部實現(xiàn)在非正常情況下的操作。從上面的說明中,可以得到幾條結(jié)論:wait/notify/notifyAll操作需要放在synchronized代碼塊或方法中,這樣才能保 證在執(zhí)行 wait/notify/notifyAll的時候,當(dāng)前線

14、程已經(jīng)獲得了所需要的鎖。當(dāng)對于某個對象的等待集合中的線程數(shù)目沒有把握的時候,最好使用 notifyAll而不是notify。notifyAll雖然會導(dǎo)致線程在沒有必要的情況下被喚醒而產(chǎn)生性能影響,但是在使用上更加簡單一些。由于線程 可能在非正常情況下被意外喚醒,一般需要把wait操作放在一個循環(huán)中,并檢查所要求的邏輯條件是否滿足。典型的使用模式如下所示: private Object lock = new Object(); synchronized (lock) while (/* 邏輯條件不滿足的時候 */) try lock.wait(); catch (InterruptedExcep

15、tion e) /處理邏輯 上述代碼中使用了一個私有對象lock來作為加鎖的對象,其好處是可以避免其它代碼錯誤的使用這個對象。中斷線程通過一個線程對象的interrupt()方 法可以向該線程發(fā)出一個中斷請求。中斷請求是一種線程之間的協(xié)作方式。當(dāng)線程A通過調(diào)用線程B的interrupt()方法來發(fā)出中斷請求的時候,線程A 是在請求線程B的注意。線程B應(yīng)該在方便的時候來處理這個中斷請求,當(dāng)然這不是必須的。當(dāng)中斷發(fā)生的時候,線程對象中會有一個標(biāo)記來記錄當(dāng)前的中斷狀態(tài)。 通過isInterrupted()方法可以判斷是否有中斷請求發(fā)生。如果當(dāng)中斷請求發(fā)生的時候,線程正處于阻塞狀態(tài),那么這個中斷請求會

16、導(dǎo)致該線程退出阻塞狀態(tài)。可能造成線程處于阻塞狀態(tài)的情況有:當(dāng)線程通過調(diào)用wait()方法進入一個對象的等待集合中,或是通過sleep()方法來暫時休眠,或是通過join()方法來等待另外一個線程完成的時候。在線程阻塞的情況下,當(dāng)中斷發(fā)生的時候,會拋出java.lang.InterruptedException, 代碼會進入相應(yīng)的異常處理邏輯之中。實際上在調(diào)用wait/sleep/join方法的時候,是必須捕獲這個異常的。中斷一個正在某個對象的等待集合中的 線程,會使得這個線程從等待集合中被移除,使得它可以在再次獲得鎖之后,繼續(xù)執(zhí)行java.lang.InterruptedException異常的處 理邏輯。通過中斷線程可以實現(xiàn)可取消的任務(wù)。在任務(wù)的執(zhí)行過程中可以定期檢查當(dāng)前線程的中斷標(biāo)記,如果線程

溫馨提示

  • 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請下載最新的WinRAR軟件解壓。
  • 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請聯(lián)系上傳者。文件的所有權(quán)益歸上傳用戶所有。
  • 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁內(nèi)容里面會有圖紙預(yù)覽,若沒有圖紙預(yù)覽就沒有圖紙。
  • 4. 未經(jīng)權(quán)益所有人同意不得將文件中的內(nèi)容挪作商業(yè)或盈利用途。
  • 5. 人人文庫網(wǎng)僅提供信息存儲空間,僅對用戶上傳內(nèi)容的表現(xiàn)方式做保護處理,對用戶上傳分享的文檔內(nèi)容本身不做任何修改或編輯,并不能對任何下載內(nèi)容負(fù)責(zé)。
  • 6. 下載文件中如有侵權(quán)或不適當(dāng)內(nèi)容,請與我們聯(lián)系,我們立即糾正。
  • 7. 本站不保證下載資源的準(zhǔn)確性、安全性和完整性, 同時也不承擔(dān)用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。

評論

0/150

提交評論