詳細解讀Java 內存模型

1、引言

多任務和高併發是衡量一臺計算機處理器的能力重要指標之一。一般衡量一個服務器性能的高低好壞,使用每秒事務處理數(Transactions Per Second,TPS)這個指標比較能說明問題,它代表著一秒內服務器平均能響應的請求數,而TPS值與程序的併發能力有著非常密切的關係。在討論Java內存模型和線程之前,先簡單介紹一下硬件的效率與一致性。

2

硬件的效率與一致性

由於計算機的存儲設備與處理器的運算能力之間有幾個數量級的差距,所以現代計算機系統都不得不加入一層讀寫速度儘可能接近處理器運算速度的高速緩存(cache)來作為內存與處理器之間的緩衝:將運算需要使用到的數據複製到緩存中,讓運算能快速進行,當運算結束後再從緩存同步回內存之中沒這樣處理器就無需等待緩慢的內存讀寫了。

基於高速緩存的存儲交互很好地解決了處理器與內存的速度矛盾,但是引入了一個新的問題:緩存一致性(Cache Coherence)。在多處理器系統中,每個處理器都有自己的高速緩存,而他們又共享同一主存,如下圖所示:多個處理器運算任務都涉及同一塊主存,需要一種協議可以保障數據的一致性,這類協議有MSI、MESI、MOSI及Dragon Protocol等。Java虛擬機內存模型中定義的內存訪問操作與硬件的緩存訪問操作是具有可比性的,後續將介紹Java內存模型。

詳細解讀Java 內存模型

除此之外,為了使得處理器內部的運算單元能竟可能被充分利用,處理器可能會對輸入代碼進行亂序執行(Out-Of-Order Execution)優化,處理器會在計算之後將對亂序執行的代碼進行結果重組,保證結果準確性。與處理器的亂序執行優化類似,Java虛擬機的即時編譯器中也有類似的指令重排序(Instruction Recorder)優化。

3Java內存模型

定義Java內存模型並不是一件容易的事情,這個模型必須定義得足夠嚴謹,才能讓Java的併發操作不會產生歧義;但是,也必須得足夠寬鬆,使得虛擬機的實現能有足夠的自由空間去利用硬件的各種特性(寄存器、高速緩存等)來獲取更好的執行速度。經過長時間的驗證和修補,在JDK1.5發佈後,Java內存模型就已經成熟和完善起來了。

主內存與工作內存

Java內存模型的主要目標是定義程序中各個變量的訪問規則,即在虛擬機中將變量存儲到內存和從內存中取出變量這樣底層細節。此處的變量與Java編程時所說的變量不一樣,指包括了實例字段、靜態字段和構成數組對象的元素,但是不包括局部變量與方法參數,後者是線程私有的,不會被共享。

Java內存模型中規定了所有的變量都存儲在主內存中,每條線程還有自己的工作內存(可以與前面的處理器的高速緩存類比),線程的工作內存中保存了該線程使用到的變量到主內存副本拷貝,線程對變量的所有操作(讀取、賦值)都必須在工作內存中進行,而不能直接讀寫主內存中的變量。不同線程之間無法直接訪問對方工作內存中的變量,線程間變量值的傳遞均需要在主內存來完成,線程、主內存和工作內存的交互關係如下圖所示,和上圖很類似。

詳細解讀Java 內存模型

這裡的主內存、工作內存與Java內存區域的Java堆、棧、方法區不是同一層次內存劃分。

內存間交互操作

關於主內存與工作內存之間的具體交互協議,即一個變量如何從主內存拷貝到工作內存、如何從工作內存同步到主內存之間的實現細節,Java內存模型定義了以下八種操作來完成:

  • 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操作)。

重排序

在執行程序時為了提高性能,編譯器和處理器經常會對指令進行重排序。重排序分成三種類型:

  1. 編譯器優化的重排序。編譯器在不改變單線程程序語義放入前提下,可以重新安排語句的執行順序。
  2. 指令級並行的重排序。現代處理器採用了指令級並行技術來將多條指令重疊執行。如果不存在數據依賴性,處理器可以改變語句對應機器指令的執行順序。
  3. 內存系統的重排序。由於處理器使用緩存和讀寫緩衝區,這使得加載和存儲操作看上去可能是在亂序執行。

從Java源代碼到最終實際執行的指令序列,會經過下面三種重排序:

詳細解讀Java 內存模型

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

詳細解讀Java 內存模型

對於volatile型變量的特殊規則

當一個變量定義為volatile之後,它具有兩種特性:

1. 保證此變量對所有線程的可見性:當一條線程修改了這個變量的值時,新值能立即同步到主內存中,因此其他線程可以立即得知該變量修改後的值。

在以下兩種情況下能保證線程的安全性,其餘情況我們仍要通過加鎖來保證原子性:

運行結果並不依賴變量的當前值,或者能夠確保只有單一的線程修改變量的值。

變量不需要與其他狀態變量共同參與不變約束。

如果不滿足以上兩種場景,我們就要選擇所來保證原子性。

2.禁止指令重排序優化

普通的變量僅僅會保證在該方法的執行過程中所有依賴賦值結果的地方都能獲得正確的結果,而不能保證變量賦值操作的順序與程序代碼的執行順序一致。

原子性、可見性與有序性

在併發編程中,我們通常會遇到以下三個問題:原子性問題,可見性問題,有序性問題。我們先看具體看一下這三個概念:

1.原子性

原子性:即一個操作或者多個操作 要麼全部執行並且執行的過程不會被任何因素打斷,要麼就都不執行。

一個很經典的例子就是銀行賬戶轉賬問題:

比如從賬戶A向賬戶B轉1000元,那麼必然包括2個操作:從賬戶A減去1000元,往賬戶B加上1000元。

試想一下,如果這2個操作不具備原子性,會造成什麼樣的後果。假如從賬戶A減去1000元之後,操作突然中止。然後又從B取出了500元,取出500元之後,再執行 往賬戶B加上1000元 的操作。這樣就會導致賬戶A雖然減去了1000元,但是賬戶B沒有收到這個轉過來的1000元。

所以這2個操作必須要具備原子性才能保證不出現一些意外的問題。

2.可見性

可見性是指當多個線程訪問同一個變量時,一個線程修改了這個變量的值,其他線程能夠立即看得到修改的值。

3.有序性

如果在本線程內觀察,所有的操作都是有序的;如果在一個線程中觀察另一個線程,所有的操作都是無序的。前半句是指「線程內表現為串行的語義

」(Within-Thread As-If-Serial Semantics),後半句是指「指令重排序」現象和「工作內存與主內存同步延遲」現象。

happens-befor 原則

我們無法就所有場景來規定某個線程修改的變量何時對其他線程可見,但是我們可以指定某些規則,這規則就是happens-before,從JDK 5 開始,JMM就使用happens-before的概念來闡述多線程之間的內存可見性。

在JMM中,如果一個操作執行的結果需要對另一個操作可見,那麼這兩個操作之間必須存在happens-before關係。

happens-before原則非常重要,它是判斷數據是否存在競爭、線程是否安全的主要依據,依靠這個原則,我們解決在併發環境下兩操作之間是否可能存在衝突的所有問題。下面我們就一個簡單的例子稍微瞭解下happens-before ;

1

2

i = 1; //線程A執行

j = i ; //線程B執行

j 是否等於1呢?假定線程A的操作(i = 1)happens-before線程B的操作(j = i),那麼可以確定線程B執行後j = 1 一定成立,如果他們不存在happens-before原則,那麼j = 1 不一定成立。這就是happens-before原則的威力。

happens-before原則定義如下:

1. 如果一個操作happens-before另一個操作,那麼第一個操作的執行結果將對第二個操作可見,而且第一個操作的執行順序排在第二個操作之前。

2. 兩個操作之間存在happens-before關係,並不意味著一定要按照happens-before原則制定的順序來執行。如果重排序之後的執行結果與按照happens-before關係來執行的結果一致,那麼這種重排序並不非法。

下面是happens-before原則規則:

  1. 程序次序規則:一個線程內,按照代碼順序,書寫在前面的操作先行發生於書寫在後面的操作;
  2. 鎖定規則:一個unLock操作先行發生於後面對同一個鎖額lock操作;
  3. volatile變量規則:對一個變量的寫操作先行發生於後面對這個變量的讀操作;
  4. 傳遞規則:如果操作A先行發生於操作B,而操作B又先行發生於操作C,則可以得出操作A先行發生於操作C;
  5. 線程啟動規則:Thread對象的start()方法先行發生於此線程的每個一個動作;
  6. 線程中斷規則:對線程interrupt()方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生;
  7. 線程終結規則:線程中所有的操作都先行發生於線程的終止檢測,我們可以通過Thread.join()方法結束、Thread.isAlive()的返回值手段檢測到線程已經終止執行;
  8. 對象終結規則:一個對象的初始化完成先行發生於他的finalize()方法的開始;

我們來詳細看看上面每條規則:

程序次序規則:一段代碼在單線程中執行的結果是有序的。注意是執行結果,因為虛擬機、處理器會對指令進行重排序(重排序後面會詳細介紹)。雖然重排序了,但是並不會影響程序的執行結果,所以程序最終執行的結果與順序執行的結果是一致的。故而這個規則只對單線程有效,在多線程環境下無法保證正確性。

鎖定規則:這個規則比較好理解,無論是在單線程環境還是多線程環境,一個鎖處於被鎖定狀態,那麼必須先執行unlock操作後面才能進行lock操作。

volatile變量規則:這是一條比較重要的規則,它標誌著volatile保證了線程可見性。通俗點講就是如果一個線程先去寫一個volatile變量,然後一個線程去讀這個變量,那麼這個寫操作一定是happens-before讀操作的。

傳遞規則:提現了happens-before原則具有傳遞性,即A happens-before B , B happens-before C,那麼A happens-before C

線程啟動規則:假定線程A在執行過程中,通過執行ThreadB.start()來啟動線程B,那麼線程A對共享變量的修改在接下來線程B開始執行後確保對線程B可見。

線程終結規則:假定線程A在執行的過程中,通過制定ThreadB.join()等待線程B終止,那麼線程B在終止之前對共享變量的修改在線程A等待返回後可見。

上面八條是原生Java滿足Happens-before關係的規則,但是我們可以對他們進行推導出其他滿足happens-before的規則:

  1. 將一個元素放入一個線程安全的隊列的操作Happens-Before從隊列中取出這個元素的操作
  2. 將一個元素放入一個線程安全容器的操作Happens-Before從容器中取出這個元素的操作
  3. 在CountDownLatch上的倒數操作Happens-Before CountDownLatch#await()操作
  4. 釋放Semaphore許可的操作Happens-Before獲得許可操作
  5. Future表示的任務的所有操作Happens-Before Future#get()操作
  6. 向Executor提交一個Runnable或Callable的操作Happens-Before任務開始執行操作

這裡再說一遍happens-before的概念:如果兩個操作不存在上述(前面8條 + 後面6條)任一一個happens-before規則,那麼這兩個操作就沒有順序的保障,JVM可以對這兩個操作進行重排序。如果操作A happens-before操作B,那麼操作A在內存上所做的操作對操作B都是可見的。

下面就用一個簡單的例子來描述下happens-before原則:


private int i = 0;

public void write(int j ){

i = j;

}

public int read(){

return i;

}

我們約定線程A執行write(),線程B執行read(),且線程A優先於線程B執行,那麼線程B獲得結果是什麼?;我們就這段簡單的代碼一次分析happens-before的規則(規則5、6、7、8 + 推導的6條可以忽略,因為他們和這段代碼毫無關係):

  1. 由於兩個方法是由不同的線程調用,所以肯定不滿足程序次序規則;
  2. 兩個方法都沒有使用鎖,所以不滿足鎖定規則;
  3. 變量i不是用volatile修飾的,所以volatile變量規則不滿足;
  4. 傳遞規則肯定不滿足;

所以我們無法通過happens-before原則推導出線程A happens-before線程B,雖然可以確認在時間上線程A優先於線程B指定,但是就是無法確認線程B獲得的結果是什麼,所以這段代碼不是線程安全的。那麼怎麼修復這段代碼呢?滿足規則2、3任一即可。

happen-before原則是JMM中非常重要的原則,它是判斷數據是否存在競爭、線程是否安全的主要依據,保證了多線程環境下的可見性。

4

Java與線程

併發不一定要依賴多線程(如PHP中很常見的多進程併發),但是在Java裡面談論併發,大多數都與線程脫不開關係。既然我們探討的話題是Java虛擬機的特性,那講到Java線程,我們就從Java線程在虛擬機中的實現開始講起。

線程的實現

我們知道,線程是比進程更輕量級的調度執行單位,線程的引入,可以把一個進程的資源分配和執行調度分開,各個線程既可以共享進程資源(內存地址、文件I/O等),又可以獨立調度(線程是CPU調度的基本單位)。

主流的操作系統都提供了線程實現,Java語言則提供了在不同硬件和操作系統平臺下對線程操作的統一處理,每個已經執行start()且還未結束的java.lang.Thread類的實例就代表了一個線程。我們注意到Thread類與大部分的Java API有顯著的差別,它的所有關鍵方法都是聲明為Native的。在Java API中,一個Native方法往往意味著這個方法沒有使用或無法使用平臺無關的手段來實現(當然也可能是為了執行效率而使用Native方法,不過,通常最高效率的手段也就是平臺相關的手段)。正因為如此,作者把本節的標題定為“線程的實現”而不是“Java線程的實現”。

實現線程主要有3種方式:使用內核線程實現、使用用戶線程實現和使用用戶線程加輕量級進程混合實現。

一、使用內核線程實現

內核線程(Kernel-Level Thread,KLT)就是直接由操作系統內核(Kernel,下稱內核)支持的線程,這種線程由內核來完成線程切換,內核通過操縱調度器(Scheduler)對線程進行調度,並負責將線程的任務映射到各個處理器上。每個內核線程可以視為內核的一個分身,這樣操作系統就有能力同時處理多件事情,支持多線程的內核就叫做多線程內核(Multi-Threads Kernel)。

程序一般不會直接去使用內核線程,而是去使用內核線程的一種高級接口——輕量級進程(Light Weight Process,LWP),輕量級進程就是我們通常意義上所講的線程,由於每個輕量級進程都由一個內核線程支持,因此只有先支持內核線程,才能有輕量級進程。這種輕量級進程與內核線程之間1:1的關係稱為一對一的線程模型,如圖

詳細解讀Java 內存模型

由於內核線程的支持,每個輕量級進程都成為一個獨立的調度單元,即使有一個輕量級進程在系統調用中阻塞了,也不會影響整個進程繼續工作,但是輕量級進程具有它的侷限性:首先,由於是基於內核線程實現的,所以各種線程操作,如創建、析構及同步,都需要進行系統調用。而系統調用的代價相對較高,需要在用戶態(User Mode)和內核態(Kernel Mode)中來回切換。其次,每個輕量級進程都需要有一個內核線程的支持,因此輕量級進程要消耗一定的內核資源(如內核線程的棧空間),因此一個系統支持輕量級進程的數量是有限的。

二、使用用戶線程實現

從廣義上來講,一個線程只要不是內核線程,就可以認為是用戶線程(User Thread,UT),因此,從這個定義上來講,輕量級進程也屬於用戶線程,但輕量級進程的實現始終是建立在內核之上的,許多操作都要進行系統調用,效率會受到限制。

而狹義上的用戶線程指的是完全建立在用戶空間的線程庫上,系統內核不能感知線程存在的實現。用戶線程的建立、同步、銷燬和調度完全在用戶態中完成,不需要內核的幫助。如果程序實現得當,這種線程不需要切換到內核態,因此操作可以是非常快速且低消耗的,也可以支持規模更大的線程數量,部分高性能數據庫中的多線程就是由用戶線程實現的。這種進程與用戶線程之間1:N的關係稱為一對多的線程模型,如圖所示。

詳細解讀Java 內存模型

使用用戶線程的優勢在於不需要系統內核支援,劣勢也在於沒有系統內核的支援,所有的線程操作都需要用戶程序自己處理。線程的創建、切換和調度都是需要考慮的問題,而且由於操作系統只把處理器資源分配到進程,那諸如“阻塞如何處理”、“多處理器系統中如何將線程映射到其他處理器上”這類問題解決起來將會異常困難,甚至不可能完成。因而使用用戶線程實現的程序一般都比較複雜 [1] ,除了以前在不支持多線程的操作系統中(如DOS)的多線程程序與少數有特殊需求的程序外,現在使用用戶線程的程序越來越少了,Java、Ruby等語言都曾經使用過用戶線程,最終又都放棄使用它。

三、使用用戶線程加輕量級進程混合實現

線程除了依賴內核線程實現和完全由用戶程序自己實現之外,還有一種將內核線程與用戶線程一起使用的實現方式。在這種混合實現下,既存在用戶線程,也存在輕量級進程。用戶線程還是完全建立在用戶空間中,因此用戶線程的創建、切換、析構等操作依然廉價,並且可以支持大規模的用戶線程併發。而操作系統提供支持的輕量級進程則作為用戶線程和內核線程之間的橋樑,這樣可以使用內核提供的線程調度功能及處理器映射,並且用戶線程的系統調用要通過輕量級線程來完成,大大降低了整個進程被完全阻塞的風險。在這種混合模式中,用戶線程與輕量級進程的數量比是不定的,即為N:M的關係,如圖12-5所示,這種就是多對多的線程模型。

許多UNIX系列的操作系統,如Solaris、HP-UX等都提供了N:M的線程模型實現。

詳細解讀Java 內存模型

四、Java線程的實現

Java線程在JDK 1.2之前,是基於稱為“綠色線程”(Green Threads)的用戶線程實現的,而在JDK 1.2中,線程模型替換為基於操作系統原生線程模型來實現。因此,在目前的JDK版本中,操作系統支持怎樣的線程模型,在很大程度上決定了Java虛擬機的線程是怎樣映射的,這點在不同的平臺上沒有辦法達成一致,虛擬機規範中也並未限定Java線程需要使用哪種線程模型來實現。線程模型只對線程的併發規模和操作成本產生影響,對Java程序的編碼和運行過程來說,這些差異都是透明的。

對於Sun JDK來說,它的Windows版與Linux版都是使用一對一的線程模型實現的,一條Java線程就映射到一條輕量級進程之中,因為Windows和Linux系統提供的線程模型就是一對一的。

而在Solaris平臺中,由於操作系統的線程特性可以同時支持一對一(通過Bound Threads或Alternate Libthread實現)及多對多(通過LWP/Thread Based Synchronization實現)的線程模型,因此在Solaris版的JDK中也對應提供了兩個平臺專有的虛擬機參數:-XX:+UseLWPSynchronization(默認值)和-XX:+UseBoundThreads來明確指定虛擬機使用哪種線程模型。

JAVA線程調度

線程調度是指系統為線程分配處理器使用權的過程,主要調度方式有兩種,分別是協同式線程調度(Cooperative Threads-Scheduling)和搶佔式線程調度(Preemptive Threads-Scheduling)。

如果使用協同式調度的多線程系統,線程的執行時間由線程本身來控制,線程把自己的工作執行完了之後,要主動通知系統切換到另外一個線程上。協同式多線程的最大好處是實現簡單,而且由於線程要把自己的事情幹完後才會進行線程切換,切換操作對線程自己是可知的,所以沒有什麼線程同步的問題。Lua語言中的“協同例程”就是這類實現。它的壞處也很明顯:線程執行時間不可控制,甚至如果一個線程編寫有問題,一直不告知系統進行線程切換,那麼程序就會一直阻塞在那裡。很久以前的Windows 3.x系統就是使用協同式來實現多進程多任務的,相當不穩定,一個進程堅持不讓出CPU執行時間就可能會導致整個系統崩潰。

如果使用搶佔式調度的多線程系統,那麼每個線程將由系統來分配執行時間,線程的切換不由線程本身來決定(在Java中,Thread.yield()可以讓出執行時間,但是要獲取執行時間的話,線程本身是沒有什麼辦法的)。在這種實現線程調度的方式下,線程的執行時間是系統可控的,也不會有一個線程導致整個進程阻塞的問題,Java使用的線程調度方式就是搶佔式調度 [1] 。與前面所說的Windows 3.x的例子相對,在Windows 9x/NT內核中就是使用搶佔式來實現多進程的,當一個進程出了問題,我們還可以使用任務管理器把這個進程“殺掉”,而不至於導致系統崩潰。

雖然Java線程調度是系統自動完成的,但是我們還是可以“建議”系統給某些線程多分配一點執行時間,另外的一些線程則可以少分配一點——這項操作可以通過設置線程優先級來完成。Java語言一共設置了10個級別的線程優先級(Thread.MIN_PRIORITY至Thread.MAX_PRIORITY),在兩個線程同時處於Ready狀態時,優先級越高的線程越容易被系統選擇執行。

不過,線程優先級並不是太靠譜,原因是Java的線程是通過映射到系統的原生線程上來實現的,所以線程調度最終還是取決於操作系統,雖然現在很多操作系統都提供線程優先級的概念,但是並不見得能與Java線程的優先級一一對應,如Solaris中有2147483648(2 32 )種優先級,但Windows中就只有7種,比Java線程優先級多的系統還好說,中間留下一點空位就可以了,但比Java線程優先級少的系統,就不得不出現幾個優先級相同的情況了,下面圖顯示了Java線程優先級與Windows線程優先級之間的對應關係,Windows平臺的JDK中使用了除THREAD_PRIORITY_IDLE之外的其餘6種線程優先級。

詳細解讀Java 內存模型

上文說到“線程優先級並不是太靠譜”,不僅僅是說在一些平臺上不同的優先級實際會變得相同這一點,還有其他情況讓我們不能太依賴優先級:優先級可能會被系統自行改變。例如,在Windows系統中存在一個稱為“優先級推進器”(Priority Boosting,當然它可以被關閉掉)的功能,它的大致作用就是當系統發現一個線程執行得特別“勤奮努力”的話,可能會越過線程優先級去為它分配執行時間。因此,我們不能在程序中通過優先級來完全準確地判斷一組狀態都為Ready的線程將會先執行哪一個。

狀態轉換

Java語言定義了5種線程狀態,在任意一個時間點,一個線程只能有且只有其中的一種狀態,這5種狀態分別如下。

1、新建(New):創建後尚未啟動的線程處於這種狀態。

2、運行(Runable):Runable包括了操作系統線程狀態中的Running和Ready,也就是處於此狀態的線程有可能正在執行,也有可能正在等待著CPU為它分配執行時間。

3、無限期等待(Waiting):處於這種狀態的線程不會被分配CPU執行時間,它們要等待被其他線程顯式地喚醒。以下方法會讓線程陷入無限期的等待狀態:

  • 沒有設置Timeout參數的Object.wait()方法。
  • 沒有設置Timeout參數的Thread.join()方法。
  • LockSupport.park()方法。

3、限期等待(Timed Waiting):處於這種狀態的線程也不會被分配CPU執行時間,不過無須等待被其他線程顯式地喚醒,在一定時間之後它們會由系統自動喚醒。以下方法會讓線程進入限期等待狀態:

  • Thread.sleep()方法。
  • 設置了Timeout參數的Object.wait()方法。
  • 設置了Timeout參數的Thread.join()方法。
  • LockSupport.parkNanos()方法。
  • LockSupport.parkUntil()方法。

4、阻塞(Blocked):線程被阻塞了,“阻塞狀態”與“等待狀態”的區別是:“阻塞狀態”在等待著獲取到一個排他鎖,這個事件將在另外一個線程放棄這個鎖的時候發生;而“等待狀態”則是在等待一段時間,或者喚醒動作的發生。在程序等待進入同步區域的時候,線程將進入這種狀態。

5、結束(Terminated):已終止線程的線程狀態,線程已經結束執行。

上述5種狀態在遇到特定事件發生的時候將會互相轉換,它們的轉換關係如圖所示。

詳細解讀Java 內存模型

本公眾號團隊成員由餓了麼、阿里、螞蟻金服等同事組成,關注架構師之巔,可以瞭解最前沿的技術。


分享到:


相關文章: