關於java線程並發的一些破事


Java並發結構

原文鏈接:http://gee.cs.oswego.edu/dl/cpj/mechanics.html

 

內容

  • 線程
  • 同步
  • 監視器

線程

線程是一個獨立執行的調用序列,同一個進程的線程在同一時刻共享一些系統資源(比如文件句柄等)也能訪問同一個進程所創建的對象資源(內存資源)。java.lang.Thread對象負責統計和控制這種行為。

每個程序都至少擁有一個線程-即作為Java虛擬機(JVM)啟動參數運行在主類main方法的線程。在Java虛擬機初始化過程中也可能啟動其他的后台線程。這種線程的數目和種類因JVM的實現而異。然而所有用戶級線程都是顯式被構造並在主線程或者是其他用戶線程中被啟動。

這里對Thread類中的主要方法和屬性以及一些使用注意事項作出總結。這些內容會在這本書(《Java Concurrency Constructs》)上進行進一步的討論闡述。Java語言規范以及已發布的API文檔中都會有更詳細權威的描述。

構造方法

Thread類中不同的構造方法接受如下參數的不同組合:

  • 一個Runnable對象,這種情況下,Thread.start方法將會調用對應Runnable對象的run方法。如果沒有提供Runnable對象,那么就會立即得到一個Thread.run的默認實現。
  • 一個作為線程標識名的String字符串,該標識在跟蹤和調試過程中會非常有用,除此別無它用。
  • 線程組(ThreadGroup),用來放置新創建的線程,如果提供的ThreadGroup不允許被訪問,那么就會拋出一個SecurityException 。

Thread類本身就已經實現了Runnable接口,因此,除了提供一個用於執行的Runnable對象作為構造參數的辦法之外,也可以創建一個Thread的子類,通過重寫其run方法來達到同樣的效果。然而,比較好的實踐方法卻是分開定義一個Runnable對象並用來作為構造方法的參數。將代碼分散在不同的類中使得開發人員無需糾結於Runnable和Thread對象中使用的同步方法或同步塊之間的內部交互。更普遍的是,這種分隔使得對操作的本身與其運行的上下文有着獨立的控制。更好的是,同一個Runnable對象可以同時用來初始化其他的線程,也可以用於構造一些輕量化的執行框架(Executors)。另外需要提到的是通過繼承Thread類實現線程的方式有一個缺點:使得該類無法再繼承其他的類。

Thread對象擁有一個守護(daemon)標識屬性,這個屬性無法在構造方法中被賦值,但是可以在線程啟動之前設置該屬性(通過setDaemon方法)。當程序中所有的非守護線程都已經終止,調用setDaemon方法可能會導致虛擬機粗暴的終止線程並退出。isDaemon方法能夠返回該屬性的值。守護狀態的作用非常有限,即使是后台線程在程序退出的時候也經常需要做一些清理工作。(daemon的發音為”day-mon”,這是系統編程傳統的遺留,系統守護進程是一個持續運行的進程,比如打印機隊列管理,它總是在系統中運行。)

啟動線程

調用start方法會觸發Thread實例以一個新的線程啟動其run方法。新線程不會持有調用線程的任何同步鎖。

當一個線程正常地運行結束或者拋出某種未檢測的異常(比如,運行時異常(RuntimeException),錯誤(ERROR) 或者其子類)線程就會終止。當線程終止之后,是不能被重新啟動的。在同一個Thread上調用多次start方法會拋出InvalidThreadStateException異常。

如果線程已經啟動但是還沒有終止,那么調用isAlive方法就會返回true.即使線程由於某些原因處於阻塞(Blocked)狀態該方法依然返回true。如果線程已經被取消(cancelled),那么調用其isAlive在什么時候返回false就因各Java虛擬機的實現而異了。沒有方法可以得知一個處於非活動狀態的線程是否已經被啟動過了(譯者注:即線程在開始運行前和結束運行后都會返回false,你無法得知處於false的線程具體的狀態)。另一點,雖然一個線程能夠得知同一個線程組的其他線程的標識,但是卻無法得知自己是由哪個線程調用啟動的。

優先級

Java虛擬機為了實現跨平台(不同的硬件平台和各種操作系統)的特性,Java語言在線程調度與調度公平性上未作出任何的承諾,甚至都不會嚴格保證線程會被執行。但是Java線程卻支持優先級的方法,這些方法會影響線程的調度:

每個線程都有一個優先級,分布在Thread.MIN_PRIORITY和Thread.MAX_PRIORITY之間(分別為1和10)
默認情況下,新創建的線程都擁有和創建它的線程相同的優先級。main方法所關聯的初始化線程擁有一個默認的優先級,這個優先級是Thread.NORM_PRIORITY (5).
線程的當前優先級可以通過getPriority方法獲得。
線程的優先級可以通過setPriority方法來動態的修改,一個線程的最高優先級由其所在的線程組限定。

當可運行的線程數超過了可用的CPU數目的時候,線程調度器更偏向於去執行那些擁有更高優先級的線程。具體的策略因平台而異。比如有些Java虛擬機實現總是選擇當前優先級最高的線程執行。有些虛擬機實現將Java中的十個優先級映射到系統所支持的更小范圍的優先級上,因此,擁有不同優先級的線程可能最終被同等對待。還有些虛擬機會使用老化策略(隨着時間的增長,線程的優先級逐漸升高)動態調整線程優先級,另一些虛擬機實現的調度策略會確保低優先級的線程最終還是能夠有機會運行。設置線程優先級可以影響在同一台機器上運行的程序之間的調度結果,但是這不是必須的。

線程優先級對語義和正確性沒有任何的影響。特別是,優先級管理不能用來代替鎖機制。優先級僅僅是用來表明哪些線程是重要緊急的,當存在很多線程在激勵進行CPU資源競爭的情況下,線程的優先級標識將會顯得非常有用。比如,在ParticleApplet中將particle animation線程的優先級設置的比創建它們的applet線程低,在某些系統上能夠提高對鼠標點擊的響應,而且不會對其他功能造成影響。但是即使setPriority方法被定義為空實現,程序在設計上也應該保證能夠正確執行(盡管可能會沒有響應)。

下面這個表格列出不同類型任務在線程優先級設定上的通常約定。在很多並發應用中,在任一指定的時間點上,只有相對較少的線程處於可執行的狀態(另外的線程可能由於各種原因處於阻塞狀態),在這種情況下,沒有什么理由需要去管理線程的優先級。另一些情況下,在線程優先級上的調整可能會對並發系統的調優起到一些作用。

范圍  用途
10      Crisis management(應急處理)
7-9    Interactive, event-driven(交互相關,事件驅動)
4-6    IO-bound(IO限制類)
2-3    Background computation(后台計算)
1        Run only if nothing else can(僅在沒有任何線程運行時運行的)

控制方法

只有很少幾個方法可以用於跨線程交流:

  • 每個線程都有一個相關的Boolean類型的中斷標識。在線程t上調用t.interrupt會將該線程的中斷標識設為true,除非線程t正處於Object.wait,Thread.sleep,或者Thread.join,這些情況下interrupt調用會導致t上的這些操作拋出InterruptedException異常,但是t的中斷標識會被設為false。
  • 任何一個線程的中斷狀態都可以通過調用isInterrupted方法來得到。如果線程已經通過interrupt方法被中斷,這個方法將會返回true。
  • 但是如果調用了Thread.interrupted方法且中斷標識還沒有被重置,或者是線程處於wait,sleep,join過程中,調用isInterrupted方法將會拋出InterruptedException異常。調用t.join()方法將會暫停執行調用線程,直到線程t執行完畢:當t.isAlive()方法返回false的時候調用t.join()將會直接返回(return)。另一個帶參數毫秒(millisecond)的join方法在被調用時,如果線程沒能夠在指定的時間內完成,調用線程將重新得到控制權。因為isAlive方法的實現原理,所以在一個還沒有啟動的線程上調用join方法是沒有任何意義的。同樣的,試圖在一個還沒有創建的線程上調用join方法也是不明智的。

起初,Thread類還支持一些另外一些控制方法:suspend,resume,stop以及destroy。這幾個方法已經被聲明過期。其中destroy方法從來沒有被實現,估計以后也不會。而通過使用等待/喚醒機制增加suspend和resume方法在安全性和可靠性的效果有所欠缺,將在3.2章節進行具體討論。而stop方法所帶來的問題也將在3.1.2.3進行探討。

 

靜態方法

Thread類中的部分方法被設計為只適用於當前正在運行的線程(即調用Thread方法的線程)。為強調這點,這些方法都被聲明為靜態的。

  • Thread.currentThread方法會返回當前線程的引用,得到這個引用可以用來調用其他的非靜態方法,比如Thread.currentThread().getPriority()會返回調用線程的優先級。
  • Thread.interrupted方法會清除當前線程的中斷狀態並返回前一個狀態。(一個線程的中斷狀態是不允許被其他線程清除的)
  • Thread.sleep(long msecs)方法會使得當前線程暫停執行至少msecs毫秒。

Thread.yield方法純粹只是建議Java虛擬機對其他已經處於就緒狀態的線程(如果有的話)調度執行,而不是當前線程。最終Java虛擬機如何去實現這種行為就完全看其喜好了。

盡管缺乏保障,但在不支持分時間片/可搶占式的線程調度方式的單CPU的Java虛擬機實現上,yield方法依然能夠起到切實的作用。在這種情況下,線程只在被阻塞的情況下(比如等待IO,或是調用了sleep等)才會進行重新調度。在這些系統上,那些執行非阻塞的耗時的計算任務的線程就會占用CPU很長的時間,最終導致應用的響應能力降低。如果一個非阻塞的耗時計算線程會導致時間處理線程或者其他交互線程超出可容忍的限度的話,就可以在其中插入yield操作(或者是sleep),使得具有較低線程優先級的線程也可以執行。為了避免不必要的影響,你可以只在偶然間調用yield方法,比如,可以在一個循環中插入如下代碼:if (Math.random() < 0.01) Thread.yield();

在支持可搶占式調度的Java虛擬機實現上,線程調度器忽略yield操作可能是最完美的策略,特別是在多核處理器上。

線程組

每一個線程都是一個線程組中的成員。默認情況下,新建線程和創建它的線程屬於同一個線程組。線程組是以樹狀分布的。當創建一個新的線程組,這個線程組成為當前線程組的子組。getThreadGroup方法會返回當前線程所屬的線程組,對應地,ThreadGroup類也有方法可以得到哪些線程目前屬於這個線程組,比如enumerate方法。

ThreadGroup類存在的一個目的是支持安全策略來動態的限制對該組的線程操作。比如對不屬於同一組的線程調用interrupt是不合法的。這是為避免某些問題(比如,一個applet線程嘗試殺掉主屏幕的刷新線程)所采取的措施。ThreadGroup也可以為該組所有線程設置一個最大的線程優先級。

線程組往往不會直接在程序中被使用。在大多數的應用中,如果僅僅是為在程序中跟蹤線程對象的分組,那么普通的集合類(比如java.util.Vector)應是更好的選擇。

在ThreadGroup類為數不多的幾個方法中,uncaughtException方法卻是非常有用的,當線程組中的某個線程因拋出未檢測的異常(比如空指針異常NullPointerException)而中斷的時候,調用這個方法可以打印出線程的調用棧信息。


同步

對象與鎖

每一個Object類及其子類的實例都擁有一個鎖。其中,標量類型int,float等不是對象類型,但是標量類型可以通過其包裝類來作為鎖。單獨的成員變量是不能被標明為同步的。鎖只能用在使用了這些變量的方法上。然而正如在2.2.7.4上描述的,成員變量可以被聲明為volatile,這種方式會影響該變量的原子性,可見性以及排序性。

類似的,持有標量變量元素的數組對象擁有鎖,但是其中的標量元素卻不擁有鎖。(也就是說,沒有辦法將數組成員聲明為volatile類型的)。如果鎖住了一個數組並不代表其數組成員都可以被原子的鎖定。也沒有能在一個原子操作中鎖住多個對象的方法。

Class實例本質上是個對象。正如下所述,在靜態同步方法中用的就是類對象的鎖。

同步方法和同步塊

使用synchronized關鍵字,有兩種語法結構:同步代碼塊和同步方法。同步代碼塊需要提供一個作為鎖的對象參數。這就允許了任意方法可以去鎖任一一個對象。但在同步代碼塊中使用的最普通的參數卻是this。

同步代碼塊被認為比同步方法更加的基礎。如下兩種聲明方式是等同的:

1 synchronized void f() { /* body */ }
2 void f() { synchronized(this) { /* body */ } }

synchronized關鍵字並不是方法簽名的一部分。所以當子類覆寫父類中的同步方法或是接口中聲明的同步方法的時候,synchronized修飾符是不會被自動繼承的,另外,構造方法不可能是真正同步的(盡管可以在構造方法中使用同步塊)。

同步實例方法在其子類和父類中使用同樣的鎖。但是內部類方法的同步卻獨立於其外部類, 然而一個非靜態的內部類方法可以通過下面這種方式鎖住其外部類:

1 synchronized(OuterClass.this) { /* body */ }

等待鎖與釋放鎖

使用synchronized關鍵字須遵循一套內置的鎖等待-釋放機制。所有的鎖都是塊結構的。當進入一個同步方法或同步塊的時候必須獲得該鎖,而退出的時候(即使是異常退出)必須釋放這個鎖。你不能忘記釋放鎖。

鎖操作是建立在獨立的線程上的而不是獨立的調用基礎上。一個線程能夠進入一個同步代碼的條件是當前鎖未被占用或者是當前線程已經占用了這個鎖,否則線程就會阻塞住。(這種可重入鎖或是遞歸鎖不同於POSIX線程)。這就允許一個同步方法可以去直接調用同一個鎖管理的另一個同步方法,而不需要被凍結(注:即不需要再經歷釋放鎖-阻塞-申請鎖的過程)。

同步方法或同步塊遵循這種鎖獲取/鎖釋放的機制有一個前提,那就是所有的同步方法或同步塊都是在同一個鎖對象上。如果一個同步方法正在執行中,其他的非同步方法也可以在任何時候執行。也就是說,同步不等於原子性,但是同步機制可以用來實現原子性。

當一個線程釋放鎖的時候,另一個線程可能正等待這個鎖(也可能是同一個線程,因為這個線程可能需要進入另一個同步方法)。但是關於哪一個線程能夠緊接着獲得這個鎖以及什么時候,這是沒有任何保證的。(也就是,沒有任何的公平性保證-見3.4.1.5)另外,沒有什么辦法能夠得到一個給定的鎖正被哪個線程擁有着。

正如2.2.7討論的,除了鎖控制之外,同步也會對底層的內存系統帶來副作用。

靜態變量/方法

鎖住一個對象並不會原子性的保護該對象類或其父類的靜態成員變量。而應該通過同步的靜態方法或代碼塊來保證訪問一個靜態的成員變量。靜態同步使用的是靜態方法鎖聲明的類對象所擁有的鎖。類C的靜態鎖可以通過內置的實例方法獲取到:
synchronized(C.class) { /* body */ }

每個類所對應的靜態鎖和其他的類(包括其父類)沒有任何的關系。通過在子類中增加一個靜態同步方法來試圖保護父類中的靜態成員變量是無效的。應使用顯式的代碼塊來代替。

如下這種方式也是一種不好的實踐:
synchronized(getClass()) { /* body */ } // Do not use
這種方式,可能鎖住的實際中的類,並不是需要保護的靜態成員變量所對應的類(有可能是其子類)

Java虛擬機在類加載和類初始化階段,內部獲得並釋放類鎖。除非你要去寫一個特殊的類加載器或者需要使用多個鎖來控制靜態初始順序,這些內部機制不應該干擾普通類對象的同步方法和同步塊的使用。Java虛擬機沒有什么內部操作可以獨立的獲取你創建和使用的類對象的鎖。然而當你繼承java.*的類的時候,你需要特別小心這些類中使用的鎖機制。


監視器

正如每個對象都有一個鎖一樣,每一個對象同時擁有一個由這些方法(wait,notify,notifyAll,Thread,interrupt)管理的一個等待集合。擁有鎖和等待集合的實體通常被稱為監視器(雖然每種語言定義的細節略有不同),任何一個對象都可以作為一個監視器。

對象的等待集合是由Java虛擬機來管理的。每個等待集合上都持有在當前對象上等待但尚未被喚醒或是釋放的阻塞線程。

因為與等待集合交互的方法(wait,notify,notifyAll)只在擁有目標對象的鎖的情況下才被調用,因此無法在編譯階段驗證其正確性,但在運行階段錯誤的操作會導致拋出IllegalMonitorStateException異常。

這些方法的操作描述如下:

Wait
調用wait方法會產生如下操作:

  • 如果當前線程已經終止,那么這個方法會立即退出並拋出一個InterruptedException異常。否則當前線程就進入阻塞狀態。
  • Java虛擬機將該線程放置在目標對象的等待集合中。
  • 釋放目標對象的同步鎖,但是除此之外的其他鎖依然由該線程持有。即使是在目標對象上多次嵌套的同步調用,所持有的可重入鎖也會完整的釋放。這樣,后面恢復的時候,當前的鎖狀態能夠完全地恢復。

Notify
調用Notify會產生如下操作:

  • Java虛擬機從目標對象的等待集合中隨意選擇一個線程(稱為T,前提是等待集合中還存在一個或多個線程)並從等待集合中移出T。當等待集合中存在多個線程時,並沒有機制保證哪個線程會被選擇到。
  • 線程T必須重新獲得目標對象的鎖,直到有線程調用notify釋放該鎖,否則線程會一直阻塞下去。如果其他線程先一步獲得了該鎖,那么線程T將繼續進入阻塞狀態。
  • 線程T從之前wait的點開始繼續執行。

NotifyAll

notifyAll方法與notify方法的運行機制是一樣的,只是這些過程是在對象等待集合中的所有線程上發生(事實上,是同時發生)的。但是因為這些線程都需要獲得同一個鎖,最終也只能有一個線程繼續執行下去。

Interrupt(中斷)
如果在一個因wait而中斷的線程上調用Thread.interrupt方法,之后的處理機制和notify機制相同,只是在重新獲取這個鎖之后,該方法將會拋出一個InterruptedException異常並且線程的中斷標識將被設為false。如果interrupt操作和一個notify操作在同一時間發生,那么不能保證那個操作先被執行,因此任何一個結果都是可能的。(JLS的未來版本可能會對這些操作結果提供確定性保證)

Timed Wait(定時等待)
定時版本的wait方法,wait(long mesecs)和wait(long msecs,int nanosecs),參數指定了需要在等待集合中等待的最大時間值。如果在時間限制之內沒有被喚醒,它將自動釋放,除此之外,其他的操作都和無參數的wait方法一樣。並沒有狀態能夠表明線程正常喚醒與超時喚醒之間的不同。需要注意的是,wait(0)與wait(0,0)方法其實都具有特殊的意義,其相當於不限時的wait()方法,這可能與你的直覺相反。

由於線程競爭,調度策略以及定時器粒度等方面的原因,定時等待方法可能會消耗任意的時間。(注:關於定時器粒度並沒有任何的保證,目前大多數的Java虛擬機實現當參數設置小於1毫秒的時候,觀察的結果基本上在1~20毫秒之間)

Thread.sleep(long msecs)方法使用了定時等待的wait方法,但是使用的並不是當前對象的同步鎖。它的效果如下描述:
if (msecs != 0)  {
Object s = new Object();
synchronized(s) { s.wait(msecs); }
}
當然,系統不需要使用這種方式去實現sleep方法。需要注意的,sleep(0)方法的含義是中斷線程至少零時間,隨便怎么解釋都行。(譯者注:該方法有着特殊的作用,從原理上它可以促使系統重新進行一次CPU競爭)。

。第一:降低資源消耗。通過重復利用已創建的線程降低線程創建和銷毀造成的消耗。

第二:提高響應速度。當任務到達時,任務可以不需要等到線程創建就能立即執行。

第三:提高線程的可管理性。線程是稀缺資源,如果無限制的創建,不僅會消耗系統資源,還會降低系統的穩定性,使用線程池可以進行統一的分配,調優和監控。但是要做到合理的利用線程池,必須對其原理了如指掌。

 

 

其線程池的一些參數:

  • corePoolSize(線程池的基本大小):當提交一個任務到線程池時,線程池會創建一個線程來執行任務,即使其他空閑的基本線程能夠執行新任務也會創建線程,等到需要執行的任務數大於線程池基本大小時就不再創建。如果調用了線程池的prestartAllCoreThreads方法,線程池會提前創建並啟動所有基本線程。
  • runnableTaskQueue(任務隊列):用於保存等待執行的任務的阻塞隊列。 可以選擇以下幾個阻塞隊列。
    • ArrayBlockingQueue:是一個基於數組結構的有界阻塞隊列,此隊列按 FIFO(先進先出)原則對元素進行排序。
    • LinkedBlockingQueue:一個基於鏈表結構的阻塞隊列,此隊列按FIFO (先進先出) 排序元素,吞吐量通常要高於ArrayBlockingQueue。靜態工廠方法Executors.newFixedThreadPool()使用了這個隊列。
    • SynchronousQueue:一個不存儲元素的阻塞隊列。每個插入操作必須等到另一個線程調用移除操作,否則插入操作一直處於阻塞狀態,吞吐量通常要高於LinkedBlockingQueue,靜態工廠方法Executors.newCachedThreadPool使用了這個隊列。
    • PriorityBlockingQueue:一個具有優先級的無限阻塞隊列。
  • maximumPoolSize(線程池最大大小):線程池允許創建的最大線程數。如果隊列滿了,並且已創建的線程數小於最大線程數,則線程池會再創建新的線程執行任務。值得注意的是如果使用了無界的任務隊列這個參數就沒什么效果。
  • ThreadFactory:用於設置創建線程的工廠,可以通過線程工廠給每個創建出來的線程設置更有意義的名字。
  • RejectedExecutionHandler(飽和策略):當隊列和線程池都滿了,說明線程池處於飽和狀態,那么必須采取一種策略處理提交的新任務。這個策略默認情況下是AbortPolicy,表示無法處理新任務時拋出異常。以下是JDK1.5提供的四種策略。
    • AbortPolicy:直接拋出異常。
    • CallerRunsPolicy:只用調用者所在線程來運行任務。
    • DiscardOldestPolicy:丟棄隊列里最近的一個任務,並執行當前任務。
    • DiscardPolicy:不處理,丟棄掉。
    • 當然也可以根據應用場景需要來實現RejectedExecutionHandler接口自定義策略。如記錄日志或持久化不能處理的任務。
  • keepAliveTime(線程活動保持時間):線程池的工作線程空閑后,保持存活的時間。所以如果任務很多,並且每個任務執行的時間比較短,可以調大這個時間,提高線程的利用率。
  • TimeUnit(線程活動保持時間的單位):可選的單位有天(DAYS),小時(HOURS),分鍾(MINUTES),毫秒(MILLISECONDS),微秒(MICROSECONDS, 千分之一毫秒)和毫微秒(NANOSECONDS, 千分之一微秒)。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM