1. 程式人生 > >Java系列筆記(5)

Java系列筆記(5)

我想關注這個系列部落格的粉絲們都應該已經發現了,我一定是個懶蟲,在這裡向大家道歉了。這個系列的部落格是在我工作之餘寫的,經常幾天才寫一小節,不過本著寧缺毋濫的精神,所有寫的東西都是比較精煉的。這篇文章是本系列的第五篇,主要講Java執行緒相關的內容,基本上包含了執行緒要了解的比較深入的東西。技術在於積累,在於總結,在於表達,在於分享,這4點都做到了,一個技術才是我們自己的。
另外說一下,本Java系列筆記,目前一共計劃寫12篇,在這個系列中,側重於Java技術的原理和深入理解,相對之下,程式碼和例項較少,不過需要例項的地方,都給出了網上相關的例項。本系列文章的主要來源是我自己的積累、自己的理解、看的書、以及網上的資料,在文中我儘可能註明了引用出處,如果有發現引用了您的資料而沒有註明的,請儘快聯絡我:[email protected]
這是第5篇,接下是第6篇《併發》,我一定儘快寫,儘快寫,快寫,寫。。。 目錄 1,執行緒原理和概念 當代作業系統,大多數都支援多工處理。對於多工的處理,有兩個常見的概念:程序和執行緒。      程序是作業系統分配資源的單位,這裡的資源包括CPU、記憶體、IO、磁碟等等裝置,程序之間切換時,作業系統需要分配和回收這些資源,所以其開銷相對較大(遠大於執行緒切換);      執行緒
是CPU分配時間的單位,理論上,每個程序至少包含一個執行緒,每個執行緒都寄託在一個程序中。一個執行緒相當於是一個程序在記憶體中的某個程式碼段,多個執行緒在切換時,CPU會根據其優先順序和相互關係分配時間片。除時間切換之外,執行緒切換時一般沒有其它資源(或只有很少的記憶體資源)需要切換,所以切換速度遠遠高於程序切換。      程序的排程以時間片為單位進行,如果兩個程序都分得一個同等長度的時間片,其中程序A只有一個執行緒,另一個程序B有10個執行緒,那麼兩個程序執行的時間片相同,但A的執行緒執行的時間是B的執行緒的10倍。      一般來說,執行緒可以分為核心級執行緒使用者級執行緒(注意區別於Java內部所使用的使用者執行緒
守護執行緒的概念).      核心級執行緒是需要核心支援的執行緒,其建立、撤銷、切換,需要核心系統的支援,在作業系統核心中為每個核心執行緒分配有一個核心控制模組,用以感知和控制執行緒。事實上,可以將核心系統程式當成一個大的核心程序,核心級執行緒就是這個核心程序的一個執行緒。      使用者級執行緒只存在於使用者空間中,其建立、撤銷、切換,都不需要核心系統支援,使用者級執行緒的切換,發生在使用者程序內部,切換很快捷。      在java中,我們一般只關注使用者級執行緒,所有java的執行緒,就是在java的JVM主程序下啟動的各個執行緒。      java的各個執行緒併發執行,其實往往只是一種錯覺,對於單核cpu而言,java各執行緒只是按排程策略執行一個個的時間段,所以在一個cpu中,一個時間點上只有一個執行緒在執行的,但可能還沒執行完,就輪到下個執行緒執行了。對於多核cpu而言,可能存在絕對意義上的執行緒併發(即兩個執行緒在兩個cpu中同時執行)。      在Java中,我們還常常遇到使用者執行緒和守護(後臺)執行緒的概念。這將在第4節中介紹。 2,執行緒狀態
在Java中,執行緒通常都有五種狀態,建立、就緒、執行、阻塞和死亡。   第一是建立狀態。在生成執行緒物件,並且還沒有呼叫該物件的start方法時,這是執行緒處於建立狀態,在這種狀態下,用getState()方法可以獲取當前執行緒的狀態,狀態值為State.NEW。   第二是就緒狀態。當呼叫了執行緒物件的start方法之後,並且該執行緒沒有被BLOCK,該執行緒就進入了就緒狀態,但是此時執行緒排程程式還沒有把該執行緒設定為當前執行緒,此時處於就緒狀態。線上程執行之後,從等待或者睡眠中回來之後,也會處於就緒狀態。如果此時呼叫getState()方法,會得到State.RUNABLE;   第三是執行狀態。執行緒排程程式將處於就緒狀態的執行緒設定為當前執行緒,此時執行緒就進入了執行狀態,開始執行run函式當中的程式碼。執行狀態僅僅發生在處於就緒狀態的執行緒獲得了JVM的排程的情況下,所以處於執行狀態的執行緒沒有專門定義RUNNING狀態,對於處於執行狀態的執行緒,呼叫的getState()獲得的仍然是State.RUNABLE。   第四是阻塞狀態。執行緒正在執行的時候,被暫停,通常是為了等待某個時間的發生(比如說某項資源就緒)之後再繼續執行。sleep,suspend,wait,join等方法都可以導致執行緒阻塞。在阻塞狀態下,呼叫getState()可能獲得3種執行緒狀態:      1,BLOCKED:這種狀況僅僅會發生線上程期望進入同步程式碼塊或同步方法(synchronized or Lock),並且尚未獲得鎖的情況下,這有兩種可能,一種是執行緒直接從執行(RUNNING)狀態搶鎖進入同步程式碼;另一種是執行緒在執行了wait方法後,被notify/notifyAll喚醒(或wait(long)時間到期),然後希望重新搶鎖的情況下,可以直接進入BLOCKED狀態。處於BLOCKED狀態的執行緒,只有在獲得了鎖之後,才會脫離阻塞狀態。      2,TIMED_WAITING:如果執行緒執行了sleep(long)/join(long),wait(long),會觸發執行緒進入TIMED_WAITING狀態,在這種狀態下,與普通的WAITING狀態相似,但是當設定的時間到了,就會脫離阻塞狀態;      3,WAITING:如果呼叫join()或wait()方法,就進入了WAITING狀態,在該狀態下,如果是因為呼叫了join()方法進入WAITING,則當join的目標執行緒執行完畢,該執行緒就會進入到RUNNABLE狀態,如果是因為呼叫了wait()進入的WAITING,則需要等鎖物件執行了notify()或notifyAll()之後才能脫離阻塞。      注1:無論上面3種哪種阻塞狀態,都只能是從執行(RUNNING,而不是RUNABLE)狀態而非其它任何狀態轉換得來,比如,不可能直接從NEW狀態進入BLOCKED狀態;      注2:什麼是鎖物件?從本質上來講,所有的鎖,都是加在物件維度的,無論是同步塊還是同步方法,甚至是靜態同步方法(靜態同步方法加在class物件上),這一點可以參考下節《Java系列筆記(6) - 併發》,所以加鎖的物件,就叫鎖物件,wait必須處於一個鎖物件的同步塊中才能呼叫,所以就需要等到鎖物件notify()或notifyAll()被呼叫後才能脫離;      注3:wait()方法是對object的,而不僅僅是對執行緒的,所以當呼叫obj.wait()後,該執行緒就會進入物件obj的鎖定池中,並等待鎖釋放,所以需要注意wait()方法必須防止同步程式碼塊中使用,否則會出現java.lang.IllegalMonitorStateException異常   第五是死亡狀態。如果一個執行緒的run方法執行結束或丟擲異常或者呼叫stop方法並完成對執行緒的中止之後後,該執行緒就會死亡。此時,再呼叫getState()方法,得到的是State.TERMINATED。對於已經死亡的執行緒,無法再使用start方法令其進入就緒。 對於執行緒的幾種狀態,下面的圖很好的說明了狀態之間的轉換關係。 3,執行緒的區域性變量表 本節請結合《Java系列筆記(3) - Java記憶體區域和GC機制》來閱讀。在本節,我們需要將Thread類和Java中執行緒的概念分開來講了:
  • 一個Thread類例項只是一個物件,像Java中的任何其他物件一樣,具有變數和方法,生死於堆上。
  • Java中,每個執行緒都有一個呼叫棧,即使你不在Java程式中建立任何新的執行緒,執行緒也在後臺執行著(如:main執行緒)。一個Java應用總是從main()方法開始執行,mian()方法執行在一個執行緒內,它被稱為主執行緒。一旦建立一個新的執行緒,就產生一個新的呼叫棧。
 在《Java系列筆記(3) - Java記憶體區域和GC機制》一文第2節中,我們將Java的虛擬機器記憶體分為幾個部分,其中執行緒共享的是堆區和方法區,執行緒私有的是虛擬機器棧,本地方法棧和程式計數器。在本文,我們只關注Java執行緒記憶體模型中最突出的兩個模組:堆記憶體 和 棧記憶體 (注:這種劃分方式“粗糙”,但卻是最直觀,也最容易理解的。)      在這裡,堆記憶體,就是前面說的堆區,由各個執行緒共享,儲存的是物件的例項;而棧記憶體,指的是虛擬機器棧(在HotSpot虛擬機器中,本地方法棧是與虛擬機器棧放在一起實現的,所以這裡不再專門區分),儲存的是區域性變量表、動態連結、方法出口。 下面要詳細說這個“區域性變量表”,區域性變量表是什麼呢?它是每個執行緒私有一份的,記憶體空間在編譯時期就已經確定了,在執行時,不會改變區域性變量表的大小。其中儲存的主要是下面的資料, 1,基本資料型別(boolean、byte、char、short、int、float),每個變數佔有一個記憶體單元。這些變數在堆記憶體中也有一份,在每個執行緒的區域性變量表中,儲存的不過是堆記憶體中這份資料的一份拷貝(接下來會介紹這些變數從區域性變量表到堆記憶體的同步方法)。 2,基本資料型別(long、double),與其它基本資料型別相同,他們也是區域性變量表中的一份拷貝,不過區別在於這兩個型別的資料佔有2個記憶體單元(64位機器除外); 3,String型別,無論從哪個方面說,String型別起始是一個物件,在區域性變量表中儲存的也永遠都只是一個應用,但是這裡把String型別獨立出來,是因為String是一個很特殊的型別:String與基本資料型別一樣,是覆蓋型修改的,也就是說,String是不可變字元序列,String一旦建立,就不能對其value進行修改,如果要修改,只能先建立一個新的物件,並將引用指向新物件。 (請參考這裡:《Java String型別剖析及其JVM記憶體分配詳解 》http://blog.csdn.net/yihuiworld/article/details/13511925) 簡單來說,String有兩種宣告方式: String aaa="abcd"; String bbb=new String("abcd"); 第一種宣告方式是直接在常量池中找一下有沒有現成的"abcd"串的存在,如果有,將aaa的引用指向該串,如果沒有,在常量池中新產生一個"abcd",並將aaa的引用指向該串; 第二種宣告方式是現在堆中new一個String物件(只要用到new,就一定是先在堆中建立物件"abcd"),然後bbb的應用指向該物件。這個物件與常量池中的"abcd"沒有關係,只有在呼叫bbb.intern()方法時,才能查到常量池中的"abcd"串。      無論上面哪種宣告方式,String都具有不可變性,所以,雖然區域性變量表中保持的是String的一份引用,但是這份引用是堆中引用的一個副本,可能出現主記憶體和執行緒local記憶體不同步更新的情況,因此類似於基本資料型別。 4,普通物件引用,注意,執行緒中儲存的沒有物件,只有物件引用,可能通過控制代碼方式或直接引用方式來查詢到堆上的物件(具體參考:《Java系列筆記(3) - Java記憶體區域和GC機制》第3節Java物件的訪問方式) 5,返回地址(returnAddress):指向了一條位元組碼指令的地址,不屬於變數的一部分,這裡先不關注。 在上面的描述中,我們把那些非常量的 基本資料型別、String、普通物件引用 統稱為執行緒中的“變數”4,Java記憶體模型 接下來,我們把上面所說的堆記憶體中的物件和基本資料型別的備份,稱為主記憶體(main memory),把上面所說的棧記憶體中用於儲存變數的部分記憶體,稱為本地記憶體(local memory)(或叫工作記憶體),這就組成了Java記憶體模型(JMM)。 1,Java執行緒對於變數的所有操作(讀取、賦值),都是在自己的工作記憶體中進行的,執行緒不直接讀寫主記憶體中的變數。 2,不同執行緒無法直接訪問對方工作記憶體中的變數; 3,執行緒間變數值的傳遞,需要主記憶體來完成。   關於主記憶體與工作記憶體之間的具體互動協議,即一個變數如何從主記憶體拷貝到工作記憶體、如何從工作記憶體同步到主記憶體之間的實現細節,Java記憶體模型定義了以下八種操作來完成(參見:Java記憶體模型:http://www.cnblogs.com/nexiyi/p/java_memory_model_and_thread.html):
  • lock(鎖定):作用於主記憶體的變數,把一個變數標識為一條執行緒獨佔狀態。
  • unlock(解鎖):作用於主記憶體變數,把一個處於鎖定狀態的變數釋放出來,釋放後的變數才可以被其他執行緒鎖定。
  • read(讀取):作用於主記憶體變數,把一個變數值從主記憶體傳輸到執行緒的工作記憶體中,以便隨後的load動作使用
  • load(載入):作用於工作記憶體的變數,它把read操作從主記憶體中得到的變數值放入工作記憶體的變數副本中。
  • use(使用):作用於工作記憶體的變數,把工作記憶體中的一個變數值傳遞給執行引擎,每當虛擬機器遇到一個需要使用變數的值的位元組碼指令時將會執行這個操作。
  • assign(賦值):作用於工作記憶體的變數,它把一個從執行引擎接收到的值賦值給工作記憶體的變數,每當虛擬機器遇到一個給變數賦值的位元組碼指令時執行這個操作。
  • store(儲存):作用於工作記憶體的變數,把工作記憶體中的一個變數的值傳送到主記憶體中,以便隨後的write的操作。
  • write(寫入):作用於主記憶體的變數,它把store操作從工作記憶體中一個變數的值傳送到主記憶體的變數中。

  如果要把一個變數從主記憶體中複製到工作記憶體,就需要按順尋地執行read和load操作,如果把變數從工作記憶體中同步回主記憶體中,就要按順序地執行store和write操作。Java記憶體模型只要求上述操作必須按順序執行,而沒有保證必須是連續執行。也就是read和load之間,store和write之間是可以插入其他指令的,如對主記憶體中的變數a、b進行訪問時,可能的順序是read a,read b,load b, load a。Java記憶體模型還規定了在執行上述八種基本操作時,必須滿足如下規則:

  • 不允許read和load、store和write操作之一單獨出現
  • 不允許一個執行緒丟棄它的最近assign的操作,即變數在工作記憶體中改變了之後必須同步到主記憶體中。
  • 不允許一個執行緒無原因地(沒有發生過任何assign操作)把資料從工作記憶體同步回主記憶體中。
  • 一個新的變數只能在主記憶體中誕生,不允許在工作記憶體中直接使用一個未被初始化(load或assign)的變數。即就是對一個變數實施use和store操作之前,不許先執行過了assign和load操作。
  • 一個變數在同一時刻只允許一條線成對其進行lock操作,lock和unlock必須成對出現
  • 如果對一個變數執行lock操作,將會清空工作記憶體中此變數的值,在執行引擎使用這個變數前需要重新執行load或assign操作初始化變數的值
  • 如果一個變數事先沒有被lock操作鎖定,則不允許對它執行unlock操作;也不允許去unlock一個被其他執行緒鎖定的變數。
  • 對一個變數執行unlock操作之前,必須先把次變數同步到主記憶體中(執行store和write操作)。
5,執行緒間通訊 上節所講到的Java記憶體模型(JMM)控制了執行緒間的通訊,Java執行緒間通訊,使用的是共享記憶體模型,而非訊息傳遞模型,也就是說,執行緒間是通過write-read記憶體中的公共狀態來進行隱式通訊的,多個執行緒之間不能直接傳遞資料互動,它們之間的互動只能通過共享變數來實現。 執行緒間通訊的步驟:
  • 執行緒A把本地記憶體A中更新過的共享變數重新整理到主記憶體中去。
  • 執行緒B到主記憶體中去讀取執行緒A之前已更新過的共享變數。
6,重排序和一致性規則 重排序      Java的記憶體模型與硬體系統記憶體模型是相對應的,主記憶體相當於硬體的記憶體,而為了獲取更好的執行速度,虛擬機器及硬體系統可能會讓工作記憶體優先儲存於暫存器和快取記憶體中,而每個執行緒的本地記憶體就相當於是暫存器和告訴快取。       基於快取記憶體的儲存互動很好地解決了處理器與記憶體的速度矛盾,但是引入了一個新的問題:快取一致性(Cache Coherence)。在多處理器系統中,每個處理器都有自己的快取記憶體,而他們又共享同一主存,多個處理器運算任務都涉及同一塊主存,需要 一種協議可以保障資料的一致性,這類協議有MSI、MESI、MOSI及Dragon Protocol等。第4節記憶體模型中介紹的8種操作,就類似於這樣的一個協議。       為了使得處理器內部的運算單元能儘可能被充分利用,處理器可能會對輸入程式碼進行亂起執行(Out-Of-Order Execution)優化,處理器會在計算之後將對亂序執行的程式碼進行結果重組,保證結果準確性。與處理器的亂序執行優化類似,Java虛擬機器的即時編譯 器中也有類似的指令重排序(Instruction Recorder)優化。

重排序分成三種類型:

  1. 編譯器優化的重排序。編譯器在不改變單執行緒程式語義放入前提下,可以重新安排語句的執行順序。
  2. 指令級並行的重排序。現代處理器採用了指令級並行技術來將多條指令重疊執行。如果不存在資料依賴性,處理器可以改變語句對應機器指令的執行順序。
  3. 記憶體系統的重排序。由於處理器使用快取和讀寫緩衝區,這使得載入和儲存操作看上去可能是在亂序執行。
JMM屬於語言級的記憶體模型,它確保在不同的編譯器和不同的處理器平臺之上,通過禁止特定型別的編譯器重排序和處理器重排序,為程式設計師提供一致的記憶體可見性保證。

對於編譯器重排序,JMM的編譯器重排序規則會禁止特定型別的編譯器重排序(不是所有的編譯器重排序都要禁止)。

對於處理器重排序,JMM的處理器重排序規則會要求java編譯器在生成指令序列時,插入特定型別的記憶體屏障(memory barriers,intel稱之為memory fence)指令,通過記憶體屏障指令來禁止特定型別的處理器重排序(不是所有的處理器重排序都要禁止)。

為了保證記憶體的可見性,Java編譯器在生成指令序列的適當位置會插入記憶體屏障指令來禁止特定型別的處理器重排序。Java記憶體模型把記憶體屏障分為LoadLoad、LoadStore、StoreLoad和StoreStore四種:

關於重排序的原理和一致性規則,可以參考:http://blog.csdn.net/vking_wang/article/details/8574376,需要注意一些常見的規則: 資料依賴性 as-if-serial語義 happens-before 資料競爭 順序一致性 上面的原理和規則,有一個共同目標:在不改變程式執行結果的前提下,儘可能的提高並行度。這裡做了解即可,這裡不再敘述,在網上可以找到很多類似的文件有詳述。 7,執行緒排程和常見內部方法 Java執行緒的排程是多執行緒執行的核心,良好的排程策略,可以充分發揮系統的效能,並提高程式的執行效率。不過,需要注意一點,Java執行緒的執行具有一定的控制粒度,也就是說,你編寫的執行緒排程策略,只能最大限度的控制和影響執行緒執行次序,而無法做到精準控制。 下面介紹執行緒排程常見的一些方法。 執行緒互動和協作方法 wait():呼叫一個物件的wait方法,會導致當前持有該物件鎖的執行緒等待,直到該物件的另一個持鎖執行緒呼叫notify/notifyAll喚醒。 wait(long timeout):與wait相似,不過除了被notify/notifyAll喚醒以外,超過long定義的超時時間,也會自動喚醒。 wait(long timeout, int nanos):與wait(long)相同,不過nanos可以提供納秒(毫微秒)級別的更精確的超時控制。 notify():呼叫一個物件的notify()方法,會導致當前持有該物件鎖的所有執行緒中的隨機某一個執行緒被喚醒。 notifyAll():呼叫一個物件的notifyAll(),會導致當前持有該物件鎖的所有執行緒同時被喚醒。 注意1: 1,對於wait/notify/notifyAll的呼叫,必須在該物件的同步方法或同步程式碼塊中。當然,一個Thread例項也是一個物件,所以,對於thread自身的同步程式碼內,可以呼叫自身的wait。 2,wait方法的呼叫會釋放鎖,而sleep或yield不會。 3,在本文第2節有過講述,當wait被喚醒或超時時,並不是直接進入執行態或就緒態,而是先進入Blocked態,搶鎖成功,才能進入執行態。 4,notify和notifyAll的區別在於:      notify喚醒的是物件多個鎖執行緒中的一個執行緒,這個執行緒進入Blocked狀態,開始搶鎖,當這個執行緒執行完釋放鎖的時候,即使現在沒有其它執行緒佔用鎖,其它處於wait狀態的執行緒也會繼續等待notify而不是主動去搶鎖。      notifyAll要殘酷的多,一單notifyAll訊息發出,所有wait在這個物件上的執行緒都會去搶鎖,搶到鎖的執行,其它執行緒Blocked在這個鎖上,當搶到鎖的執行緒執行完成釋放鎖之後,其它執行緒自動搶鎖。      也就是說,執行緒wait後的喚醒過程必須是:wait-notify-搶鎖-執行-釋放鎖。 5,notify和wait必須加迴圈進行保證,這是一個良好的程式設計習慣,這是因為,沒有迴圈條件保證的話,如果有多個wait執行緒在等待notify,當notifyAll發出時,兩個wait執行緒同時被喚醒,進入RUNABLE狀態,如果此時他們競爭一個非鎖資源,則只有一個能搶到,另一個雖然搶不到,但因為是非鎖資源,所以會繼續執行,就容易造成問題。 注意2,在java中,還有另一對方法suspend()和resume(),他們的作用類似於wait()/notify(),區別在於,suspend()和resume()不會釋放鎖,所以這兩個方法容易造成死鎖問題(ta suspend tb,tb又在等著ta釋放鎖),現在,這兩個方法已經不再使用了。 休眠 sleep方法如其名,是讓執行緒休眠的,而且是那個執行緒呼叫,就是哪個執行緒休眠。可以是呼叫TimeUtil.sleep(long)、Thread.sleep(long),或當前執行緒物件t上的t.sleep(long),其結果都是當前執行緒休眠。當然如果是非當前執行緒t2.sleep(long),則為t2休眠。。 sleep方法有兩個:sleep(long timeout), sleep(long timeout,int nanos),兩個方法功能相似,後一種方法能夠提供納秒級別的控制。 sleep是Thread類的方法,不是物件的,也無法通過notify來喚醒,當sleep的時間到了,自然會喚醒。 在sleep休眠期間,執行緒會釋放出CPU資源給其它執行緒,但線上本身仍佔有鎖,而不會釋放鎖。 sleep()的呼叫使得其它低優先順序、同等優先順序、高優先順序的執行緒有了執行的機會。 優先順序 java執行緒的優先順序並不絕對,它所控制的是執行的機會,也就是說,優先順序高的執行緒執行的概率比較大,而優先順序低的執行緒也並不是沒有機會,只是執行的概率相對低一些。 Java執行緒一共有10個優先順序,分別為1-10,數值越大,表明優先順序越高,一個普通的執行緒,其優先順序為5; 執行緒的優先順序具有繼承性,如果一個執行緒B是在另一個執行緒A中建立的,則B叫做A的子執行緒,B的初始優先順序與A保持一致。 java中使用  t.setPriority(n)來設定優先順序,n必須為1-10之間的整數,否則會拋異常。 Java各優先順序執行緒間具有不確定性,由於作業系統的不同,不同優先順序的執行緒會有很大的表現上的不同,所以很難比較或統計。 不過,需要注意的是編碼過程中,最好不要有程式碼邏輯是依賴於執行緒優先順序的,不然可能造成問題,因為在Java中,高優先順序不一定比低優先順序先執行,也不一定比他低優先順序執行緒被排程到的機率大(只能說。。。更傾向於高優先順序) 注:對於執行緒組ThreadGroup的優先順序,具有特殊性,簡單的說,就是執行緒組內的執行緒,優先順序不能超過執行緒組的整體設定。由於Threadgroup是一個失敗的東西,用的少,而且將來可能會被淘汰,所以這裡也就不細說了,有興趣的可以參考:http://silentlakeside.iteye.com/blog/1175981 讓步 Java的讓步使用的是Thread.yield()靜態方法,功能是暫停當前執行緒的執行,並讓步於其它同優先順序執行緒,讓其它執行緒先執行。 yield()僅僅是讓出CPU資源,但是讓給誰,是由系統決定的,是不確定的,當前執行緒使用yield()讓出資源後,執行緒不會釋放鎖,而是回到就緒狀態,等待排程執行。 yield()只是使當前執行緒重新回到可執行狀態,所有執行yield()的執行緒有可能在進入到可執行狀態後馬上又被執行,所以yield()方法只能使同優先順序的執行緒有執行的機會,而且僅僅是機會而已,如果呼叫yield()後發現沒有同等級別的其它執行緒,則當前執行緒會立即重新進入執行態。 yield()從某種程度上與sleep()相似,但yield不能指定讓步的時間。而且,sleep()讓出的機會並不限制其它執行緒的優先順序,而yield僅限於其它同優先順序執行緒。 實際上,yield()方法對應瞭如下操作;先檢測當前是否有相同優先順序的執行緒處於同可執行狀態,如有,則把CPU的佔有權交給次執行緒,否則繼續執行原來的執行緒,所以yield()方法稱為“退讓”,它把執行機會讓給了同等級的其他執行緒。 合併 Java執行緒見合併,使用的是join方法。join()方法做的事情是將並行執行的執行緒合併為序列執行的,例如,如果線上程ta中呼叫tb.join(),則ta會停止當前執行,並讓tb先執行,直到tb執行完畢,ta才會繼續執行。 join方法有3個過載方法。 t.join()是允許t插隊到自己前面,等t執行完成再執行自己; t.join(long timeout)是允許t插隊到自己前面,等待t執行,且最長只等待timeout毫秒(不管t有沒有被排程執行,當前呼叫t.join的執行緒都只等timeout的時間); t.join(long timeout, int nanos)與t.join(long)一樣,只不過可以提供納秒級的精度; 如果當前執行緒ta呼叫tb.join(),tb開始執行,ta進入WAITING或TIMED_WAITING狀態。 注意,如果ta呼叫tb.join(),則ta會釋放當前持有的鎖。事實上,join是通過wait/notify來實現的,當ta呼叫tb.join(),ta就wait在tb物件上,同時釋放鎖,tb物件搶鎖執行,當執行完成後,tb自己發出notify通知。觸發ta繼續執行,注意,當tb用notify通知ta後,ta還要重新搶鎖。 Java7中,出現了一個新的模式:fork()/join()模式,採用的是分而治之的思想來實現併發程式設計,fork用於將現場拆分成多個小塊並行執行,join用於合併結果。不過這個模式容易出問題,要慎用,有興趣的可以參考:http://www.ibm.com/developerworks/cn/java/j-lo-forkjoin/ 守護執行緒 如果對一個執行緒t,呼叫t. setDaemon(true),則可以將該執行緒設定為守護執行緒,JVM判斷程式執行結束的標準是所有使用者執行緒執行完成,當用戶執行緒全部結束,即使守護執行緒仍在執行,或尚未開始,JVM都會結束。 不能將正在執行的執行緒設定為守護執行緒,因此t.setDaemon(true)方法必須在t.start()之前呼叫; 如果在一個守護執行緒中new出來一個新執行緒,及時不執行setDaemon(true),新的執行緒也是守護執行緒; 守護執行緒一般用來做GC、後臺監控、記憶體管理等後臺型任務,且這些任務即使隨時被結束,也不影響整體程式的執行。 中斷 線上程中,中斷是一個重要的功能,java執行緒中的interrupt也是一個很容易出現誤用的方法,。 在Java執行緒中,中斷有且只有一個含義,就是讓執行緒退出阻塞狀態t.interrupt()只是向執行緒t發出一箇中斷訊號,讓該執行緒退出阻塞狀態。 所以, 1,如果執行緒t當前是可中斷的阻塞狀態(如呼叫了sleep、join等方法導致執行緒進入WATING / TIMED_WAITING狀態),在任意其它執行緒中呼叫t.interrupt(),那麼執行緒會立即丟擲一個InterruptedException,退出阻塞狀態; 2,如果是呼叫wait進入的WAITING / TIMED_WAITING狀態,呼叫了t.interrupt()後,需要先等執行緒搶到鎖,脫離BLOCKED狀態,才會丟擲InterruptExceptiong; 3,如果執行緒t當前是不可中斷的阻塞狀態(如不能中斷的IO操作、尚未獲取鎖的BLOCKED狀態),呼叫了t.interrupt()後,則需要等到脫離了阻塞狀態之後,才立即丟擲InterruptedException; 4,如果執行緒t當前處在執行狀態,則呼叫了t.interrupt(),執行緒會繼續執行,直到發生了sleep、join、wait等方法的呼叫,才會在進入阻塞之後,隨後立即丟擲InterruptedException,跳出阻塞狀態; 事實上,在sleep、wait、join方法中,會不斷的檢查中斷狀態的值,如果發現中斷狀態為true,則立即丟擲InterruptedExceptiong,並嘗試跳出阻塞(用嘗試的原因是wait方法阻塞的執行緒可能需要先搶鎖); 呼叫這3個方法的實際效果如下: 1. sleep() & interrupt()     執行緒A正在使用sleep()暫停著: Thread.sleep(100000);     如果要取消他的等待狀態,可以在正在執行的執行緒裡(比如這裡是B)呼叫         a.interrupt();     令執行緒A放棄睡眠操作,這裡a是執行緒A對應到的Thread例項     當在sleep中時 執行緒被呼叫interrupt()時,就馬上會放棄暫停的狀態.並丟擲InterruptedException.丟出異常的,是A執行緒. 2. wait() & interrupt()     執行緒A呼叫了wait()進入了等待狀態,也可以用interrupt()取消.     不過這時候要小心鎖定的問題.執行緒在進入等待區,會把鎖定解除,當對等待中的執行緒呼叫interrupt()時     ,會先重新獲取鎖定,再丟擲異常.在獲取鎖定之前,是無法丟擲異常的. 3. join() & interrupt()     當執行緒以join()等待其他執行緒結束時,當它被呼叫interrupt(),它與sleep()時一樣, 會馬上跳到catch塊裡.      注意,是對誰呼叫interrupt()方法,一定是呼叫被阻塞執行緒的interrupt方法.如線上程a中呼叫來執行緒t.join().     則a會等t執行完後在執行t.join後的程式碼,當線上程b中呼叫來 a.interrupt()方法,則會丟擲InterruptedException,當然join()也就被取消了。 Thread.interrupted()方法可以用來判斷當前執行緒的中斷狀態,不過,需要注意的是,該方法同時具有清除中斷位的作用,也就是說,如果一個執行緒t被中斷了,當該方法第一次被呼叫時,返回結果為true,同時中斷狀態被清除,第二次被呼叫時,返回結果為false; 一般的,線上程的run方法中,可以採用try catch捕獲異常+ 中斷狀態判斷相結合的方式來判斷:  try {  //檢查程式是否發生中斷   while (!Thread. interrupted()) {                                                                  System.out.println( "I am running!");  //point1 before sleep                                                                   Thread.sleep( 20);  //point2 after sleep                                                                   System.out.println( "Calculating");                                                 }                                 } catch (InterruptedException e) {                                                  System.out.println( "Exiting by Exception");                                 }                                  System.out.println( "ATask.run() interrupted!" ); 如果一個執行緒無法響應中斷(比如執行緒的run中是一個不sleep的死迴圈),則可能永遠無法用interrupt()方法來結束它的執行。這一點在Java執行緒池(ThreadPoolExecutor)中用到,因為執行緒池有可能呼叫shutdown()或shutdownNow()來結束池中執行緒,這兩個方法都是通過interrupt來執行的,如果其中一個執行緒無法中斷,那執行緒池的這個方法就可能達不到預期效果。 8,執行緒池 Runable與Callable 建立Java執行緒除了使用Thread類之外,還可以有Runable和Callable兩種,這兩者都是可以實現併發執行緒的介面,他們的區別是: 1,Runnable是JDK1.1中就出現的,屬於包java.lang,而Callable是在JDK1.5才提供的,屬於java.util.concurrent; 2,Runnable中要實現的是void run()方法,沒有返回值,而Callable要實現的是V call()方法,返回一個泛型V的返回值(通過Future.get()方法獲取); 3,Runnable中丟擲的異常,線上程外是無法捕獲的,而Callable是可以丟擲Exception; 4,Runnable和Callable都可以用於ExecutorService,而Thread類只支援Runnable,當然,可以用FutureTask對Callable進行封裝,並用Thread類才能執行; 5,執行Callable可以得到一個Future物件,用於表示非同步計算的結果,類似於CallBack,而Runable不行; 執行Runnable和Callable的程式碼如下: Runnable: class SomeRunnable implements Runnable {  public void run()                 {  //do something here                 } } Runnable oneRunnable = new SomeRunnable (); Thread oneThread = new Thread(oneRunnable); oneThread.start(); Callable: class SomeCallable implements Callable <String> {  public String call() throws Exception {  // do something  return "" ;                 } } Callable<String> oneCallable = new SomeCallable(); FutureTask<String > oneTask = new FutureTask <String>(oneCallable); Thread twoThread = new Thread (oneTask); twoThread.start(); Future和FutureTask Future是對Runnable或Callable的任務結果進行查詢、獲取結果、取消等操作的非同步管理類; (注:Future對Runnable的管理是通過FutureTask實現的) Future與Callable一樣位於java.util.concurrent包下,是一個泛型介面。提供如下方法:    1,boolean cancel(boolean mayInterruptIfRunning);該方法用於取消任務,如果取消成功,返回true,如果取消失敗,返回false。 mayInterruptIfRunning引數表示的是是否允許取消正在執行且沒有完畢的任務,true表示可以取消正在執行的任務; 如果任務已經執行完成,無論引數是什麼,該方法都返回false; 如果任務正在執行:若引數為true,則取消成功的話返回true;若引數為false,則直接返回false; 若任務尚未執行,則無論引數是什麼,都在取消成功後返回true;    2,boolean isCancelled();該方法判斷任務是否被取消成功,如果在任務正常完成前被取消成功,則返回true;    3,boolean isDone(); 該方法判斷任務是否正常完成,如果是,返回true;    4,V get() throws InterruptedException, ExecutionException; 該方法用於獲取執行結果,呼叫該方法後,會產生阻塞,呼叫者會一直阻塞知道任務完畢返回結果才繼續執行;    5,V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;該方法與get相似,只不過,有超時限制,如果到了指定時間還沒有得到結果,則返回null; Future是一個介面,無法用於直接建立物件,而且Runnable也無法直接用Future,所以就有了FutureTask,FutureTask也位於java.util.concurrent包,FuntureTask的實現如下: public class FutureTask<V> implements RunnableFuture<V>,就是說,FutureTask實現了RunnableFuture介面,而RunnableFuture介面是怎麼回事呢? public interface RunnableFuture<V> extends Runnable, Future<V> { void run(); } 可見,FutureTask實際上同時實現了Future介面和Runnable介面,所以它既可以作為Runnable被Thread執行緒執行,也可以作為Future得到Callable的返回值; FutureTask提供了兩個構造器: public FutureTask(Callable<V> callable) {} public FutureTask(Runnable runnable, V result) {} 用這兩個構造器,FutureTask可以對callable和runnable的做出實現,並且由於FutureTask實現了Future介面,所以可以實現對於callable和runnable的管理。 執行緒池 執行緒池存在的目的在於:提前建立好需要的執行緒,減少臨時建立執行緒帶來的資源消耗。而且每個ThreadPoolExecutor執行緒池還維護者一些統計資料,如完成的任務數,可以方便的進行統計,同時該類還提供了很多可調整的引數和擴充套件的鉤子(hook)。 java.util.concurrent中,關於執行緒池提供了很多介面和類,這些介面和類的關係如下:(圖片來自:http://www.blogjava.net/xylz/archive/2010/12/21/341281.html,深入淺出 Java Concurrency (29): 執行緒池 part 2 Executor 以及Executors 從圖中可以看出: Executor是頂層介面,其中只有一個execute(Runnable)的宣告,返回值是void; ExecutorService介面集成了Executor介面,同時提供了submit、invokeAll、invokeAny、shutDown等方法; AbstractExecutorService實現了ExecutorService介面,並基本實現其所有方法; ThreadPoolExecutor繼承了類AbstractExecutorService;並提供了execute()/submit()/shutdown()/shudownNow()等方法的具體實現(execute是提供了具體實現,其它方法用了超類的實現); 注:execute和submit的區別在於: execute是定義在Executor中,並在ThreadPoolExecutor中具體實現,沒有返回值的,其作用就是向執行緒池提交一個任務並執行; submit是定義在ExecutorService中,並在AbstractExecutorService中具體實現,且在ThreadPoolExecutor中沒有對其進行重寫,submit能夠返回結果,其內部實現,其實還是在呼叫execute(),不過,它利用Future&FutureTask來獲取任務結果。 ExecutorService提供了管理終止的方法,以及可為跟蹤一個或多個非同步任務執行狀況而生成 Future 的方法。可以關閉 ExecutorService,這將導致其拒絕新任務。 提供兩個方法來關閉 ExecutorService:
  • shutdown()方法在終止前允許執行以前提交的任務;
  • shutdownNow() 方法阻止等待任務的啟動並試圖停止當前正在執行的任務。在終止後,執行程式沒有任務在執行,也沒有任務在等待執行,並且無法提交新任務。應該關閉未使用的 ExecutorService以允許回收其資源;
通過建立並返回一個可用於取消執行和/或等待完成的 Future,方法submit擴充套件了基本方法Executor.execute(java.lang.Runnable)。 方法 invokeAny 和 invokeAll 是批量執行的最常用形式,它們執行任務 collection,然後等待至少一個, 或全部任務完成(可使用 ExecutorCompletionService類來編寫這些方法的自定義變體)。 注意1:它只有一個直接實現類ThreadPoolExecutor和間接實現類ScheduledThreadPoolExecutor。 關於ThreadPoolExecutor的更多內容請參考《ThreadPoolExecutor》 對於執行緒池的其它更深理解,可以從下面的幾個方面進行:   1.執行緒池狀態   2.任務的執行   3.執行緒池中的執行緒初始化   4.任務快取佇列及排隊策略   5.任務拒絕策略   6.執行緒池的關閉 這些內容可以參考下面的內容:《Java併發程式設計:執行緒池的使用》,下面摘錄如下: 執行緒池的狀態 在ThreadPoolExecutor中定義了一個volatile變數,另外定義了幾個static final變量表示執行緒池的各個狀態: volatile int runState; static final int RUNNING    = 0; static final int SHUTDOWN   = 1; static final int STOP       = 2; static final int TERMINATED = 3; runState表示當前執行緒池的狀態,它是一個volatile變數用來保證執行緒之間的可見性; 下面的幾個static final變量表示runState可能的幾個取值。
  • 當建立執行緒池後,初始時,執行緒池處於RUNNING狀態;
  • 如果呼叫了shutdown()方法,則執行緒池處於SHUTDOWN狀態,此時執行緒池不能夠接受新的任務,它會等待所有任務執行完畢;
  • 如果呼叫了shutdownNow()方法,則執行緒池處於STOP狀態,此時執行緒池不能接受新的任務,並且會去嘗試終止正在執行的任務;
  • 當執行緒池處於SHUTDOWN或STOP狀態,並且所有工作執行緒已經銷燬,任務快取佇列已經清空或執行結束後,執行緒池被設定為TERMINATED狀態。
任務的執行 在任務提交到執行緒池並且執行完畢之前,需要先了解ThreadPoolExecutor中的幾個重要成員變數:
  • private final BlockingQueue<Runnable> workQueue;              //任務快取佇列,用來存放等待執行的任務
  • private final ReentrantLock mainLock = new ReentrantLock();   //執行緒池的主要狀態鎖,對執行緒池狀態(比如執行緒池大小、runState等)的改變都要使用這個鎖
  • private final HashSet<Worker> workers = new HashSet<Worker>();  //用來存放工作集
  • private volatile long  keepAliveTime;    //執行緒存貨時間   
  • private volatile boolean allowCoreThreadTimeOut;   //是否允許為核心執行緒設定存活時間
  • private volatile int   corePoolSize;     //核心池的大小(即執行緒池中的執行緒數目大於這個引數時,提交的任務會被放進任務快取佇列)
  • private volatile int   maximumPoolSize;   //執行緒池最大能容忍的執行緒數
  • private volatile int   poolSize;       //執行緒池中當前的執行緒數
  • private volatile RejectedExecutionHandler handler; //任務拒絕策略
  • private volatile ThreadFactory threadFactory;   //執行緒工廠,用來建立執行緒
  • private int largestPoolSize;   //用來記錄執行緒池中曾經出現過的最大執行緒數
  • private long completedTaskCount;   //用來記錄已經執行完畢的任務個數
其中,corePoolSize指的是核心池大小,如果執行緒池中的執行緒數目大於這個引數時,提交的任務會被放進任務快取佇列,如果快取佇列也放不下了,則會考慮擴充套件執行緒池中的執行緒數,一直擴充套件到maximumPoolSize; maximumPoolSize是執行緒池最大能容忍多少執行緒數; 上面兩個引數遵循下面的規則:
  • 如果當前執行緒池中的執行緒數目小於corePoolSize,則每來一個任務,就會建立一個執行緒去執行這個任務;
  • 如果當前執行緒池中的執行緒數目>=corePoolSize,則每來一個任務,會嘗試將其新增到任務快取隊列當中,若新增成功,則該任務會等待空閒執行緒將其取出去執行;若新增失敗(一般來說是任務快取佇列已滿),則會嘗試建立新的執行緒去執行這個任務;
  • 如果當前執行緒池中的執行緒數目達到maximumPoolSize,則會採取任務拒絕策略進行處理;
  • 如果執行緒池中的執行緒數量大於 corePoolSize時,如果某執行緒空閒時間超過keepAliveTime,執行緒將被終止,直至執行緒池中的執行緒數目不大於 corePoolSize;如果允許為核心池中的執行緒設定存活時間,那麼核心池中的執行緒空閒時間超過keepAliveTime,執行緒也會被終止。
通過 setCorePoolSize()和setMaximumPoolSize()可以動態設定執行緒池容量的大小。 執行緒池中的執行緒初始化

預設情況下,建立執行緒池之後,執行緒池中是沒有執行緒的,需要提交任務之後才會建立執行緒。

  在實際中如果需要執行緒池建立之後立即建立執行緒,可以通過以下兩個方法辦到:

  • prestartCoreThread():初始化一個核心執行緒;
  • prestartAllCoreThreads():初始化所有核心執行緒
任務快取佇列及排隊策略

在前面我們多次提到了任務快取佇列,即workQueue,它用來存放等待執行的任務。

  workQueue的型別為BlockingQueue<Runnable>,通常可以取下面三種類型:

  1)ArrayBlockingQueue:基於陣列的先進先出佇列,此佇列建立時必須指定大小;

  2)LinkedBlockingQueue:基於連結串列的先進先出佇列,如果建立時沒有指定此佇列大小,則預設為Integer.MAX_VALUE;

  3)synchronousQueue:這個佇列比較特殊,它不會儲存提交