Java內(nèi)存模型與線程

除了充分利用計算機處理器的能力外磨确,一個服務端同時對多個客戶端提供服務則是另一個更具體的并發(fā)應用場景仰迁。衡量一個服務性能的好壞高低,每秒事務處理數(shù)(Transactions Per Second, TPS)是最重要的指標之一,它代表著一秒內(nèi)服務端平均能相應的請求總數(shù)由缆,而TPS值與程序的并發(fā)能力又有非常密切的關系。對于計算量相同的任務猾蒂,程序線程并發(fā)協(xié)調(diào)得越有條不紊均唉,效率自然就越高;反之肚菠,線程之間頻繁阻塞甚至死鎖舔箭,將會大大降低程序的并發(fā)能力。

服務端是Java語言最擅長的領域之一蚊逢,這個領域的應用占了Java應用中最大的一塊份額层扶,不過如何寫好并發(fā)應用程序確是服務端程序開發(fā)的難點之一箫章,處理好并發(fā)方面的問題通常需要更多的編碼經(jīng)驗來支持。幸好Java語言和虛擬機提供了許多工具怒医,把并發(fā)變成的門檻降低了不少炉抒。而且各種中間件服務器奢讨、各類框架都努力地替程序員處理盡可能多的多線程并發(fā)細節(jié)稚叹,使得程序員在編碼時能更專注于業(yè)務邏輯,而不花費大量的時間去關服務會被多少人調(diào)用拿诸、如何協(xié)調(diào)硬件資源扒袖。無論語言、中間件和框架如何先進亩码,開發(fā)人員都不能期望它們能獨立完成所有并發(fā)處理的事情季率,了解并發(fā)的內(nèi)幕也是稱為一個高級程序員不可缺少的課程。

硬件的效率與一致性

“讓計算機并發(fā)執(zhí)行若干個運算任務”和“更充分的利用計算機處理器的效能”之間的因果關系描沟,看起來順理成章飒泻,實際上它們之間的關系并沒有想象中的那么簡單,其中一個重要的復雜性來源是絕大多數(shù)的運算任務都不可能只靠處理器計算能完成吏廉,處理器至少要與內(nèi)存交互泞遗,如讀取運算數(shù)據(jù)、存儲運算結果等席覆,這個I/O操作是很難消除的史辙。由于計算器的存儲設備與處理器的運算速度有幾個數(shù)量級的差距,所以現(xiàn)代計算機系統(tǒng)都不得不加入一層讀寫速度盡可能接近處理器運算速度的告訴緩存(Cache)來作為內(nèi)存與處理器之間的緩沖:將運算需要使用到的數(shù)據(jù)復制到緩存中佩伤,讓運算能快速進行聊倔,當運算結束之后再從緩存同步回內(nèi)存之中,這樣處理器就無需等待緩慢的內(nèi)存讀寫了生巡。

基于高速緩存的存儲交互很好的解決了處理器的內(nèi)存與內(nèi)存的速度矛盾耙蔑,但是也為計算機系統(tǒng)帶來了更高的復雜度,因為它引入了一個新的問題:緩存一致性(Cache Coherence)孤荣。在多處理器系統(tǒng)中甸陌,每個處理器都有自己的告訴緩存,而它們又共享同一內(nèi)存(Main Memory)垃环。當多個處理器的運算任務都涉及同一塊主內(nèi)存區(qū)域時邀层,將可能導致各自緩存數(shù)據(jù)不一致,如果真的發(fā)生這種情況遂庄,那同步回到主內(nèi)存時以誰的緩存數(shù)據(jù)為準呢寥院?為了解決一致性的問題,需要各個處理器訪問緩存時都遵循一些協(xié)議涛目,在讀寫時要根據(jù)協(xié)議來進行操作秸谢,這類協(xié)議有MSI凛澎、MESI、MOSI估蹄、Synapse塑煎、Firefly和Dragon Protocol等。多次提到的“內(nèi)存模型”臭蚁,可以理解為在特定的協(xié)議下最铁,對特定的內(nèi)存或告訴緩存進行讀寫訪問的抽象過程。不同結構的物理機器可以擁有不一樣的內(nèi)存模型垮兑,而JVM也有自己的內(nèi)存模型冷尉,并且這里介紹的內(nèi)存訪問操作與硬件的緩存訪問操作具有很高的可比性。

處理器系枪、高速緩存雀哨、主內(nèi)存的交互關系

除了增加高速緩存之外,為了使得處理器內(nèi)部的運算單位能盡量被充分利用私爷,處理器可能會對輸入代碼進行亂序執(zhí)行(Out-Of-Order Execution)優(yōu)化雾棺,處理器會在計算之后將亂序執(zhí)行的結果重組,保證該結果與順序執(zhí)行的結果是一致的衬浑,但并不保證程序中各個語句計算的先后順序與輸入代碼中的順序一致捌浩,因此,如果存在一個計算任務依賴另外一個計算任務的中間結果嚎卫,那么其順序性并不能靠代碼的先后順序來保證嘉栓。與處理器的亂序執(zhí)行優(yōu)化類似,JVM的即時編譯器也有類似的指令重排序(Instruction Reorder)優(yōu)化拓诸。

Java內(nèi)存模型

JVM規(guī)范中試圖定義一種Java內(nèi)存模型(Java Memory Model侵佃,JMM)來屏蔽掉各種硬件和操作系統(tǒng)的內(nèi)存訪問差異,以實現(xiàn)讓Java程序在各種平臺下都達到一致的內(nèi)部才能訪問效果奠支。在此之前馋辈,主流程序語言(C/C++)直接使用物理硬件和操作系統(tǒng)的內(nèi)存模型,因此倍谜,會由于不同平臺上內(nèi)存模型的差異迈螟,有可能導致在一套平臺上并發(fā)完全正常,而在另外一套平臺上并發(fā)訪問卻經(jīng)常出錯尔崔,因此在某些場景就必須針對不同的平臺來編寫程序答毫。

定義Java內(nèi)存模型并非一件容易的事情,這個模型必須定義得足夠嚴謹季春,才能讓Java的并發(fā)內(nèi)存訪問操作不會產(chǎn)生歧義洗搂;但是,也必須定義得足夠?qū)捤桑沟锰摂M機的實現(xiàn)有足夠的自由空間去利用硬件的各種特性(寄存器耘拇、高速緩存和指令集中某些特有指令)來獲取更好的執(zhí)行速度撵颊。經(jīng)過長時間的驗證和修補,在JDK1.5發(fā)布之后惫叛,Java內(nèi)存模型已經(jīng)成熟和完善起來了倡勇。

主內(nèi)存與工作內(nèi)存

Java內(nèi)存模型的主要目標是定義程序中各個變量的訪問規(guī)則,即在虛擬機中將變量存儲到內(nèi)存和從內(nèi)存中取出變量這樣的底層細節(jié)嘉涌。此外的變量(Variables)與Java編程中所說的變量有所區(qū)別妻熊,它包括了實例字段、靜態(tài)字段和構成數(shù)組對象的元素洛心,但不包括局部變量與方法參數(shù)固耘,因為后者是線程私有的,不會被共享词身,自然就不會存在競爭問題。為了獲得較好的執(zhí)行效能番枚,Java內(nèi)存模型并沒有限制執(zhí)行引擎使用處理器的特定寄存器或緩存來和主內(nèi)存進行交互法严,也沒有限制即時編譯器進行調(diào)整代碼執(zhí)行順序這類優(yōu)化措施。

Java內(nèi)存模型規(guī)定了所有的變量都存儲在主內(nèi)存中(此外的內(nèi)存與介紹物理硬件時的主內(nèi)存名字一樣葫笼,兩者也可以互相類比深啤,但此處僅僅是虛擬機內(nèi)存的一部分)赁酝。每條線程還有自己的工作內(nèi)存(Working Memory厂抖,可與前面的高速緩存類比),線程的工作內(nèi)存中保存了被線程使用到的變量的主內(nèi)存副本拷貝帘瞭。這里的副本拷貝并不是把內(nèi)存復制出來洋丐,這個對象的引用呈昔、對象中某個線程訪問到的字段是有可能存在拷貝的,但不會有虛擬機實現(xiàn)成把整個對象拷貝一次友绝。堤尾。線程對變量的所有操作(讀取、賦值等)都必須在工作內(nèi)存中完成迁客,而不是直接讀寫主內(nèi)存中的變量(根據(jù)JVM規(guī)范的規(guī)定郭宝,volatile變量依然有工作內(nèi)存的拷貝,但是由于它特殊的操作順序性規(guī)定掷漱,所以看起來如同直接在主內(nèi)存中讀寫訪問一般粘室,因此這里的描述對于volatile也并不例外)。不同線程之間也無法直接訪問對方工作內(nèi)存中的變量卜范,線程間變量值的傳遞均需要通過主內(nèi)存來完成衔统,線程、主內(nèi)存、工作內(nèi)存三者的關系如下所示


線程缰冤、主內(nèi)存犬缨、工作內(nèi)存三者的交互關系

內(nèi)存間相互操作

關于主內(nèi)存與工作內(nèi)存之間具體的交互協(xié)議,即一個變量如何從主內(nèi)存拷貝到工作內(nèi)存棉浸、如何從工作內(nèi)存同步回主內(nèi)存之類的實現(xiàn)細節(jié)怀薛,Java內(nèi)存模型定義了以下8種操作來完成,虛擬機實現(xiàn)時必須保證下面提及的每一種操作的原子性迷郑、不可再分的(對于double枝恋、long類型的變量來說,load嗡害、store焚碌、read和write操作在某些平臺上允許有例外)。

  • lock(鎖定):作用于主內(nèi)存變量霸妹,它把一個變量標識為一條線程獨占的狀態(tài)十电。
  • unlock(解鎖):作用于主內(nèi)存的變量,它把一個處于鎖定狀態(tài)的變量釋放出來叹螟,釋放后的變量才可以被其他線程鎖定鹃骂。
  • read(讀取):作用域主內(nèi)存的變量罢绽,它把一個變量的值從主內(nèi)存?zhèn)鬏數(shù)骄€程的工作內(nèi)存中畏线,以便隨后的load動作使用。
  • load(載入):作用于工作內(nèi)存的變量良价,它把read操作從主內(nèi)存中得到的變量值放入工作內(nèi)存的變量副本中寝殴。
  • use(使用):作用于工作內(nèi)存的變量,它把工作內(nèi)存中的一個變量的值傳遞給執(zhí)行引擎明垢,每當虛擬機遇到一個需要使用到變量的值的字節(jié)碼指令時將會執(zhí)行這個操作蚣常。
  • assign(賦值):作用于工作內(nèi)存變量,它把一個從執(zhí)行引擎接收到的賦值給工作內(nèi)存的變量袖外,每當虛擬機遇到一個變量賦值的字節(jié)碼時執(zhí)行這個操作史隆。
  • store(存儲):作用于工作內(nèi)存的變量,它把工作內(nèi)存中一個變量的值傳遞到內(nèi)存中曼验,以便隨后的write操作使用泌射。
  • write(寫入):作用于主內(nèi)存的變量,它把store操作從工作內(nèi)存中得到的變量的值放入主內(nèi)存的變量中鬓照。

如果要把一個變量從主內(nèi)存復制到工作內(nèi)存熔酷,那就要順序地執(zhí)行read和load操作,如果要把變量從工作內(nèi)存同步回主內(nèi)存豺裆,就要順序的執(zhí)行store和write操作拒秘。注意号显,Java內(nèi)存模型只要求上述兩個操作必須按順序執(zhí)行,而沒有保證是連續(xù)執(zhí)行躺酒。也就是說押蚤,read與load之間、store與write之間是可以插入其他指令的羹应,如對主內(nèi)存中的變量a揽碘、b進行訪問時,一種可能出現(xiàn)順序是read a园匹、read b雳刺、load b、load a裸违。初次之外掖桦,Java內(nèi)存模型還規(guī)定了在執(zhí)行上述8種基本操作時必須滿足如下規(guī)則:

  • 不允許read和load、store和write操作之一單獨出現(xiàn)供汛,即不允許一個變量從主內(nèi)存讀取了但工作內(nèi)存不接受枪汪,或者從工作內(nèi)存發(fā)起回寫了但主內(nèi)存不接受的情況出現(xiàn)。
  • 不允許一個線程丟棄它的最近的assign操作紊馏,即變量在工作內(nèi)存中改變了之后必須把該變化同步回主內(nèi)存料饥。
  • 不允許一個線程無原因的(沒有發(fā)生過任何assign操作)把數(shù)據(jù)從線程工作內(nèi)存同步回主內(nèi)存中。
  • 一個新的變量只能在主內(nèi)存中誕生朱监,不允許在工作內(nèi)存中直接使用一個未被初始化(load或assign)的變量,換句話說原叮,就是對一個變量實施use赫编、store操作之前,必須先執(zhí)行assign和load操作奋隶。
  • 一個變量在用一個時刻只允許一條線程對其進行l(wèi)ock操作擂送,但lock操作可以被同一條線程重復執(zhí)行多次,多次執(zhí)行l(wèi)ock之后唯欣,只有執(zhí)行相同次數(shù)的unlock操作嘹吨,變量才會解鎖。
  • 如果對一個變量執(zhí)行l(wèi)ock操作境氢,那將會情況工作內(nèi)存中詞變量的值蟀拷,在執(zhí)行引擎使用這個變量之前,需要重新執(zhí)行l(wèi)oad或assign操作初始化變量的值萍聊。
  • 如果一個變量事先沒有被lock操作鎖定问芬,那就不允許對它執(zhí)行unlock操作寿桨,也不允許去unlock一個被其他線程鎖定住的變量此衅。
  • 對一個變量執(zhí)行unlock操作之前,必須先把此變量同步回主內(nèi)存中(執(zhí)行store挡鞍、write操作)。

這8種內(nèi)存訪問操作以及上述規(guī)則限定墨微,再加上稍后介紹的volatile的一些特殊規(guī)定就已經(jīng)完全確定了Java程序中哪些內(nèi)存訪問操作在并發(fā)下是安全的道媚。

對于volatile型變量的特殊規(guī)則

關鍵字volatile可以說是JVM提供的最輕量級的同步機制,但是它并不容易完全被正確欢嘿、完整的理解衰琐,以至于許多程序員動習慣不去使用它,遇到需要處理多線程數(shù)據(jù)競爭的時候一律使用synchronized來進行同步炼蹦。了解volatile變量的語義對了解多線程操作的其他特性很有意義羡宙。

Java內(nèi)存模型對volatile專門定義了一些特殊的訪問規(guī)則,當一個變量定義為volatile之后掐隐,它將具備兩個特性:第一狗热,是保證此變量對所有線程的可見性,這里的“可見性”是指當一條線程修改了這個變量的值虑省,新值對于其他線程來說是可以立即得知的匿刮。而普通變量不能做到這一點,普通變量的值在線程間傳遞均需要通過主內(nèi)存來完成探颈,例如熟丸,線程A修改了一個普通變量的值,然后向主內(nèi)存進行回寫伪节,另外一條線程B在線程A回寫完成之后再從主內(nèi)存進行讀取操作光羞,新變量值才會對線程B可見。

關于volatile變量的可見性怀大,經(jīng)常會被開發(fā)人員誤解纱兑,認為以下描述成立:“volatile變量對所有線程是立即可見的,對volatile變量所有的寫操作都能立即反應到其他線程中化借,換句話說潜慎,volatile變量在各個線程中是一致的,所以基于volatile變量的運算在并發(fā)下是安全的蓖康☆盱牛”這個結論。 volatile變量在各個線程的工作內(nèi)存中不存在一致性問題(在各個線程的工作內(nèi)存中钓瞭,volatile變量也可以存在不一致的情況驳遵,但由于每次使用之前都要先刷新,執(zhí)行引擎看不到不一致的情況山涡,因此變量也可以認為不存在一致性問題)堤结,但是Java里面的運算并非原子操作唆迁,導致volatile變量的運算在并發(fā)下一樣是不安全的,我們可以通過一段簡單的演示來說明:

public class Main {
    public static volatile int race = 0;

    public static void increase() {
        race++;
    }

    private static final int THREADS_COUNT = 20;

    public static void main(String[] args) {
        Thread[] threads = new Thread[THREADS_COUNT];
        for (int i = 0; i < THREADS_COUNT; i++) {
            threads[i] = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 10000; i++) {
                        increase();
                    }
                }
            });
            threads[i].start();
        }
        while (Thread.activeCount() > 2) {
            Thread.yield();
        }
        System.out.println(race);
    }
}

這段代碼發(fā)起了20個線程竞穷,每個線程對race變量進行10000次自增操作唐责,如果這段代碼能夠正確并發(fā)的話,最后輸出的結果應該是200000瘾带。但是在運行完這段代碼之后并沒有得到這個預期的結果鼠哥,而且會發(fā)現(xiàn)每次運行程序,輸出的結果都不一樣看政,都是小于200000的數(shù)字朴恳。問題出在了自增運算“race++”上,我們用javap反編譯這段代碼后會得到如下代碼清單:

  public static void increase();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=0, args_size=0
         0: getstatic     #2                  // Field race:I
         3: iconst_1
         4: iadd
         5: putstatic     #2                  // Field race:I
         8: return
      LineNumberTable:
        line 11: 0
        line 12: 8

發(fā)現(xiàn)只有一行代碼的increase方法在Class文件中是由4條字節(jié)碼指令構成的允蚣,從字節(jié)碼層面上很容易就分析出并發(fā)失敗的原因:當getstatic指令把race的值取到操作數(shù)棧的棧頂時于颖,volatile關鍵字保證了race的值在此時是正確的,但是在執(zhí)行iconst_1嚷兔、iadd這些指令的時候森渐,其他線程可能已經(jīng)把race的值增大了,而在操作棧頂?shù)闹稻妥兂闪诉^期數(shù)據(jù)冒晰,所以putstatic指令執(zhí)行后就可能把較小的race同步回主內(nèi)存之中同衣。

由于volatile變量只能保證可見性,在不符合以下兩條規(guī)則的運算場景中壶运,我們?nèi)匀灰ㄟ^加鎖(使用synchronized或者java.util.concurrent中的原子類)來保證原子性耐齐。

  • 運算結果并不依賴變量的當前值,或者能確保只有單一的線程修改變量的值
  • 變量不需要與其他的狀態(tài)共同參與不變約束

如下的代碼清單中的場景就很適合使用volatile變量來控制并發(fā)蒋情,當shutdown方法被調(diào)用的時候蚪缀,能確保所有線程中執(zhí)行的doWork方法都立即停下來。

volatile boolean shutdownRequseted;

public void shutdown() {
    shutdownRequseted = true;
}

public void doWork(){
    whilr (!shutdownRequseted){
        //do something
    }
}

使用volatile變量的第二語義是禁止指令重排序優(yōu)化恕出,普通的變量僅僅會保證在該方法執(zhí)行過程中所有依賴賦值結果的地方都能獲取到正確的結果,而不能保證變量賦值操作的順序與程序代碼中的執(zhí)行順序一致违帆。因為在一個線程的方法執(zhí)行過程中無法感知到這點浙巫,這也就是Java內(nèi)存模型中描述的所謂的“線程內(nèi)表現(xiàn)為串行的語義”(Within-Thead As-If-Serial Semantics)。

指令重排序是并發(fā)編程中最容易讓開發(fā)人員產(chǎn)生疑惑的地方刷后,比如的畴,一段標準的DCL單例代碼,可以觀察到加入了volatile和未加入volatile關鍵字時所產(chǎn)生匯編代碼的差別

public class Singleton {
    private volatile static Singleton instance;

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }

    public static void main(String[] args) {
        Singleton singleton = Singleton.getInstance();
    }
}

編譯后尝胆,這段代碼對instance變量賦值部分如下:

0x01a3de0f:mov  $0x3375cdb0,%esi        ;...beb0cd75  33
                                        ;    {oop('Singleton')}
0x01a3de14:mov  %eax,0x150(%esi)        ;...89865001 0000
0x01a3de1a:shr  $0x9,%esi               ;...clee09
0x01a3de1d:movb $0x0,0x1104800 (%esi)   ;...c6860048 100100
0x01a3de24:lock addl $0x0,(%esp)        ;...f0830424 00
                                        ;*putstatic intstance
                                        ;-
Singleton::getInstance@24

通過對比就會發(fā)現(xiàn)丧裁,關鍵變化在于有volatile修飾的變量,賦值后多執(zhí)行了“l(fā)ock addl 0x0,(%esp) ”操作含衔,這個操作相當于一個內(nèi)存屏障(Memory Barrier或Memory Fence煎娇,指重排序時不能把后面的指令重排序到內(nèi)存屏障之前的位置)二庵,只有一個CPU訪問內(nèi)存時,并不需要內(nèi)存屏障缓呛;但如果有兩個或多個CPU訪問內(nèi)存時催享,切其中有一個在觀測另一個,就需要內(nèi)存屏障來保證一致性了哟绊。這句指令中的“addl0x0,(%esp)”(把ESP寄存器的值加0)顯然是一個空操作因妙,關鍵在于lock的前綴,它的作用是使得本CPU的Cache寫入了內(nèi)存票髓,該寫入動作也會引起別的CPU或者別的內(nèi)核無效化(Invalidate)其Cache攀涵,這種操作相當于對Cache中的變量做了一次前面介紹Java內(nèi)存模式中所說的“store和write”操作。所以通過這樣的空操作洽沟,可讓volatile變量的修改對其他CPU立即可見以故。

那為什么說它禁止指令重排序呢?從硬件架構上講玲躯,指令重排序是指CPU采用了允許講多條指令不按程序規(guī)定的順序分開發(fā)送給各相應電路單元處理据德。但并不是說指令任意重排,CPU需要能正確處理指令依賴情況以保障程序能得出正確的執(zhí)行結果跷车。在本內(nèi)CPU中棘利,重排序看起來依然是有序的。因此“l(fā)ock addl $0x0,(%esp)”指令把修改同步到內(nèi)存時朽缴,意味著所有之前的操作都已經(jīng)執(zhí)行完成善玫,這樣便形成了“指令重排序無法通過內(nèi)存屏障”的效果。

解決了volatile的語義問題密强,再來看看在眾多保障并發(fā)安全的工具中選用volatile的意義:它能讓我們的代碼比使用其他同步工具更快嗎茅郎?在某些情況下,volatile的同步機制的性能確實要優(yōu)于鎖或渤,但是由于虛擬機對鎖實行的許多消除和優(yōu)化系冗,使得我們很難量化的認為volatile就會比synchronized塊多少。如果讓volatile自己與自己比較的話薪鹦,可以確定一個原則掌敬,volatile變量的讀操作的性能消耗與普通變量幾乎沒有什么差別,但是寫操作則會慢一些池磁,因為它需要在本地代碼中插入許多內(nèi)存屏障指令來保證處理器不發(fā)生亂序執(zhí)行奔害。不過即便如此,大多數(shù)場景下volatile的總開銷仍然要比鎖要低地熄,我們在volatile與鎖之中選擇的唯一依據(jù)僅僅是volatile的語義更能滿足使用場景的需求华临。

最后再看看Java內(nèi)存模型中對volatile變量定義的特殊規(guī)則。假定T表示一個線程端考,V和W分別表示兩個volatile型的變量雅潭,那么在進行read揭厚、load、use寻馏、assign棋弥、store和write操作時需要滿足如下規(guī)則:

  • 只有當線程T對變量V執(zhí)行的前一個動作是load的時候,線程T才能對變量V執(zhí)行use動作诚欠;并且顽染,只有當線程T對變量V執(zhí)行的后一個動作是use的時候,線程T才能對變量V執(zhí)行l(wèi)oad動作轰绵。線程T對變量V的use動作可以認為是和線程T對變量V的load粉寞、read動作相關聯(lián),必須連續(xù)一起出現(xiàn)(這條規(guī)則要求在工作線程中左腔,每次使用V前都必須從主內(nèi)存刷新最新的值唧垦,用于保證能看見其他線程對變量V所做的修改后的值)。
  • 只有當線程T對變量V執(zhí)行的前一個動作是assign的時候液样,線程T才能對變量V執(zhí)行store動作振亮;并且,只有當線程T對變量V執(zhí)行的后一個動作是store的時候鞭莽,線程T才能對變量V執(zhí)行assign動作坊秸。線程T對變量V的assign動作認為是和線程T對變量V的store、write動作相關聯(lián)澎怒,必須一起出現(xiàn)(這條規(guī)則要求在工作線程中褒搔,每次修改V后都必須立刻同步回主內(nèi)存中,用于確保其他線程可以看到自己對變量V所做的修改)喷面。
  • 假定動作A是線程T對變量V實施的use或assign動作星瘾,假定動作F是和動作A相關聯(lián)的load或store動作,假定動作P是和動作F相應的對變量V的read或者write動作惧辈;類似的琳状,假定動作B是線程T對變量W實施的use或者assign動作,假定動作G是和動作B相關聯(lián)的load或者store動作盒齿,假定動作Q是和動作G相應的對變量W的read或write動作算撮。如果A先于B,那么P先于Q(這條規(guī)則要求volatile修飾的變量不會被指令重排序優(yōu)化县昂,保證代碼是執(zhí)行順序與程序的順序相同)。

對于long和double型變量的特殊規(guī)則

Java內(nèi)存模型要求lock陷舅、unlock倒彰、read、load莱睁、assign待讳、use芒澜、store、write這8個操作都具有原子性创淡,但是對于64位的數(shù)據(jù)類型(long和double)痴晦,在模型中特別定義了一條相對寬松的規(guī)定:允許虛擬機將沒有被volatile修飾的64位數(shù)據(jù)的讀寫操作劃分為兩次32位的操作來進行,即允許虛擬機實現(xiàn)選擇可以不保證64位數(shù)據(jù)類型的load琳彩、store誊酌、read和write這4個操作的原子性,這點就是所謂的long和double的非原子性協(xié)定(Nonatomic Treatment of double and long Variables)露乏。

如果有多個線程共享一個并未聲明為volatile的long或double類型的變量碧浊,并且同時對它們讀取和修改操作,那么某些線程可能會讀取到一個既非原值瘟仿,也不是其他線程修改值的代表了“半個變量”的數(shù)值箱锐。

不過這種讀取到“半個變量”的情況非常罕見(在目前商用Java虛擬機中不會出現(xiàn)),因為Java內(nèi)存模型雖然允許虛擬機不把long和double變量的讀寫實現(xiàn)成原子操作劳较,但允許虛擬機選擇把這些操作實現(xiàn)為具有原子性的操作驹止,而且還“強烈建議”虛擬機這樣實現(xiàn)。在實際開發(fā)中观蜗,目前各種平臺下的商用虛擬機幾乎都選擇把64位數(shù)據(jù)的讀寫操作作為原子操作來對待臊恋,因此我們在編寫代碼時一般不需要把用到的long和double變量專門聲明為volatile。

原子性嫂便、可見性和有序性

  • 原子性(Atomicity):由Java內(nèi)存模型來直接保證的原子性操作包括read捞镰、load、assign毙替、use岸售、store和write,我們大致可以認為基本數(shù)據(jù)類型的訪問讀寫是具備原子性的(例外就是long和double的非原子性協(xié)定厂画,讀者只要知道這件事就可以了凸丸,無須太過在意這些幾乎不會發(fā)生的例外情況)。如果應用場景需要一個更大范圍的原子性保證袱院,Java內(nèi)存模型還提供了lock和unlock操作來滿足這種需求屎慢,盡管虛擬機未把lock和unlock操作直接開放給用戶使用,但是卻提供了更高層次的字節(jié)碼指令monitorenter和monitorexit來隱式地使用這兩個操作忽洛,這兩個字節(jié)碼指令反映到Java代碼中就是同步塊——synchronized關鍵字腻惠,因此在synchronized塊之間的操作也具備原子性。
  • 可見性(Visibility):可見性是指當一個線程修改了共享變量的值欲虚,其他線程就能夠立刻得知這個修改集灌。Java內(nèi)存模型是通過在變量修改后將新值同步回主內(nèi)存,在變量讀取前從主內(nèi)存刷新變量值這種依賴主內(nèi)存作為傳遞媒介的方式來實現(xiàn)可見性的复哆,無論是普通變量還是volatile變量都是如此欣喧,普通變量與volatile變量的區(qū)別是腌零,volatile的特殊規(guī)則保證了新值能立即同步到主內(nèi)存,以及每次使用前立即從主內(nèi)存刷新唆阿。因此益涧,可以說volatile保證了多線程操作時變量的可見性,而普通變量則不能保證這一點驯鳖。除了volatile之外闲询,Java還有兩個關鍵字能實現(xiàn)可見性,即synchronized和final臼隔。同步塊的可見性是由“對一個變量執(zhí)行unlock操作之前嘹裂,必須先把此變量同步回主內(nèi)存中(執(zhí)行store、write操作)”這條規(guī)則獲得的摔握,而final關鍵字的可見性是指:被final修飾的字段在構造器中一旦初始化完成寄狼,并且構造器沒有把“this”的引用傳遞出去(this引用逃逸是一件很危險的事情,其他線程有可能通過這個引用訪問到“初始化一半”的對象)氨淌,那在其他線程中就能看到final字段的值泊愧。
  • 有序性(Ordering):Java內(nèi)存模型的有序性在volatile也討論過,Java程序中天然的有序性可以總結為一句話:如果在本線程中觀察盛正,所有的操作都是有序的删咱;如果在一個線程中觀察另一個線程,所有的操作都是無序的豪筝。前半句是指“線程內(nèi)表現(xiàn)為串行的語義”(Within-Thread As-If-Serial Semantics)痰滋,后半句是指“指令重排序”現(xiàn)象和“工作內(nèi)存與主內(nèi)存同步延遲”現(xiàn)象。Java語言提供了volatile和synchronized兩個關鍵字來保證線程之間操作的有序性续崖,volatile關鍵字本身就包含禁止指令重排序的語義敲街,而synchronized則是由“一個變量在同一個時刻只允許一條線程對其進行l(wèi)ock操作”這條規(guī)則獲得的,這條規(guī)則決定了持有同一個鎖的兩個同步塊只能串行地進入严望。

我們可以看到多艇,synchronized關鍵字在需要這3種特性的時候就可以作為其中一種解決方案?看起來很“萬能”像吻。的確峻黍,大部分的并發(fā)控制操作都能使用synchronized來完成,synchronized的萬能也造成了我們的濫用拨匆,越“萬能”的并發(fā)控制姆涩,通常會伴隨著越大的性能影響。

先行發(fā)生原則

如果Java內(nèi)存模型中所有的有序性都僅僅依靠volatile和synchronized來完成惭每,那么有一些操作將會變得很繁瑣阵面,但是我們在編寫Java并發(fā)代碼的時候并沒有感覺到這一點,這是因為Java語言中有一個“先行發(fā)生”(happens-before)的原則。這個原則非常重要样刷,它是判斷數(shù)據(jù)是否存在競爭、線程是否安全的主要依據(jù)览爵,依靠這個原則置鼻,我們可以通過幾條規(guī)則一攬子的解決并發(fā)環(huán)境下兩個操作之間是否可能存在沖突的所有問題。

先行發(fā)生時Java內(nèi)存模型中定義的兩項操作之間的偏序關系蜓竹,如果說操作A先行發(fā)生于操作B箕母,其實就是說在發(fā)生操作B之前,操作A產(chǎn)生的影響能被操作B觀察到俱济,“影響”包括修改了內(nèi)存中共享變量的值嘶是、發(fā)送了消息、調(diào)用了方法等蛛碌。舉個例子:

//以下操作在線程A中執(zhí)行
i = 1;

//以下操作在線程B中執(zhí)行
j = i;

//以下操作在線程C中執(zhí)行
i = 2;

假設線程A中的操作“i=1”先行發(fā)生于線程B的操作“j=1”聂喇,那么可以確定在線程B的操作執(zhí)行后,變量j的值一定等于1蔚携,得出這個結論的依據(jù)有兩個:一是根據(jù)先行發(fā)生原則希太,“i=1”的結果可以被觀察到;二是線程C還沒“登場”酝蜒,線程A操作結束之后沒有其他線程會修改變量i的值√芑裕現(xiàn)在再來考慮線程C,我們依然保持線程A和線程B之間的先行發(fā)生關系亡脑,而線程C出現(xiàn)在線程A和線程B的操作之間堕澄,但是線程C與線程B沒有先行發(fā)生關系,那j的值會是多少呢霉咨?答案是不確定蛙紫。1和2都有可能,因為線程C對變量i的影響可能會被線程B觀察到躯护,也可能不會惊来,這個時候線程B就存在讀取到過期數(shù)據(jù)的風險,不具備多線程安全性棺滞。

下面是Java內(nèi)存模型下一些“天然的”先行發(fā)生關系裁蚁,這些先行發(fā)生關系無須任何同步器協(xié)助就已經(jīng)存在,可以在編碼中直接使用继准。如果兩個操作之間的關系不在此列枉证,并且無法從下列規(guī)則中推導出來的話,它們就沒有順序性保障移必,虛擬機可以對它們隨意地進行重排序室谚。

  • 程序次序規(guī)則:在一個線程中,按照程序代碼順序,書寫在前面的操作先行發(fā)生于書寫在后面的操作秒赤。準確的說猪瞬,應該是控制流順序而不是程序代碼順序,因為要考慮分支入篮、循環(huán)等結構兜畸。
  • 管程鎖定規(guī)則:一個unlock操作先行發(fā)生于后面對同一個鎖的lock操作盹沈。這里必須強調(diào)的是同一個鎖港柜,而“后面”是指時間上的先后順序动壤。
  • volatile變量規(guī)則:對一個volatile變量的寫操作先行發(fā)生于后面對這個變量的讀操作,這里的“后面”同樣是指時間上的先后順序酥诽。
  • 線程啟動規(guī)則:Thread對象的start()方法先行發(fā)生于此線程的每一個動作鞍泉。
  • 線程中斷規(guī)則:線程中的所有操作都先行發(fā)生于對此線程的終止檢測,我們可以通過Thread.join()方法結束肮帐、Thread.isAlive()的返回值等手段檢測到線程已經(jīng)終止執(zhí)行咖驮。
  • 對象終結規(guī)則:一個對象的初始化完成(構造函數(shù)執(zhí)行結束)先行發(fā)生于它的finalize()方法的開始。
  • 傳遞性:如果操作A先行發(fā)生于操作B泪姨,操作B先行發(fā)生于操作C游沿,那就可以得出操作A先行發(fā)生于操作C的結論。

Java語言無須任何同步手段保障就能成立的先行發(fā)生規(guī)則就只有上面這些了肮砾。

private int value = 0;

public void setValue(int value) {
    this.value = value;
}

public int getValue() {
    return value;
}

代碼中展示的不過是getter/setter方法诀黍,假設存在線程A和線程B,線程A先調(diào)用了“setValue(1)”仗处,然后B線程調(diào)用了同一個對象的“getValue()”眯勾,那么線程B收到的返回值是什么?

我們依次分析一下發(fā)生原則中的各項規(guī)則婆誓,由于兩個方法分別由線程A和線程B調(diào)用吃环,不在一個線程中,所以程序次序規(guī)則在這里不適用洋幻;由于沒有同步塊郁轻,自然就不會發(fā)生lock和unlock的操作,所以管程鎖定規(guī)則也不是適用文留;由于value變量沒有被volatile關鍵字修飾好唯,所以volatile變量規(guī)則不適用;后面的線程啟動燥翅、終止骑篙、中斷規(guī)則和對象終結規(guī)則也和這里完全沒關系。因為沒有一個適用的先行發(fā)生規(guī)則森书,所以最后一天傳遞性也無從談起靶端,因此我們可以判斷盡管線程A在操作時間上先于線程B谎势,但是無法確定線程B中“getValue”方法的返回結果,換句話說杨名,這里面的操作不是線程安全的脏榆。

那怎么修復這個問題呢?我們至少有兩種比較簡單的方案可以選擇台谍,要么把getter/setter方法都定義為synchronized方法姐霍,這樣就可以套用管理鎖定規(guī)則;要么把value定義volatile變量典唇,由于setter方法對于value的修改不依賴value的原值,滿足volatile關鍵字使用場景胯府,這樣就可以套用volatile變量規(guī)則來實現(xiàn)先行發(fā)生關系介衔。

通過上面的例子,我們可以得出結論:一個操作“時間上的先發(fā)生”不代表這個操作會是“先行發(fā)生”骂因,那如果一個操作“先行發(fā)生”是否就能推導出這個操作必定是“時間上的先發(fā)生”呢炎咖?也不成立,一個典型的例子就是多次提到的“指令重排序”

//以下操作在同一個線程中執(zhí)行
int i= 1;
int j = 2;

這兩條語句都是在同一個線程之中寒波,根據(jù)程序次序規(guī)則乘盼,“int i=1”的操作先行發(fā)生于“int j=2”,但是“int j=2”的代碼完全可能先被處理器執(zhí)行俄烁,這并不影響先行發(fā)生原則的正確性绸栅,因為我們在這條線程之中沒有辦法感知到這點。

綜合上面兩個例子我們可以得出結論:時間先后順序與先行發(fā)生原則之間基本沒有太大關系页屠,所以我們衡量并發(fā)安全問題的時候不要受到時間順序的干擾粹胯,一切必須以先行發(fā)生原則為準。

Java與線程

并發(fā)不一定要依賴多線程(如PHP中很常見的多線程并發(fā))辰企,但是在Java里面談論并發(fā)风纠,大多數(shù)都與線程脫不開關系。

線程的實現(xiàn)

線程是比進程更輕量級的調(diào)度執(zhí)行單位牢贸,線程的引入竹观,可以把一個線程的資源分配和執(zhí)行調(diào)度分開,各個線程可以共享進程資源(內(nèi)存地址潜索、文件IO等)臭增,又可以獨立調(diào)度(線程是CPU調(diào)度的基本單位)。

主流的操作系統(tǒng)都提供了線程實現(xiàn)帮辟,Java語言則提供了在不同硬件和操作系統(tǒng)平臺下對線程操作的統(tǒng)一處理速址,每個已經(jīng)執(zhí)行start()且未結束的java.lang.Thread類的實現(xiàn)就代表了一個線程。我們注意到Thread類與的大部分JavaAPI有顯著差別由驹,它的所有關鍵方法都是聲明為native的芍锚。在JavaAPI中昔园,一個native方法往往意味著這個方法沒有使用或無法使用平臺無關的手段來實現(xiàn)(當然也可能是為了執(zhí)行效率而使用native方法,不過并炮,通常最高效的手段也就是平臺相關的手段)默刚。

實現(xiàn)線程主要有3種方式:使用內(nèi)核線程實現(xiàn)、使用用戶線程實現(xiàn)和使用用戶線程+輕量級進程混合實現(xiàn)逃魄。

使用內(nèi)核實現(xiàn)

內(nèi)核線程(Kernel-Level Thread荤西,KLT)就是直接由操作系統(tǒng)內(nèi)核(Kernel,下面統(tǒng)稱內(nèi)核)支持的線程伍俘,這種線程由內(nèi)核來完成線程切換邪锌,內(nèi)核通過操縱調(diào)度器(Scheduler)對線程進行調(diào)度,并負責將線程的任務映射到各個處理器上癌瘾。每個內(nèi)核線程可以視為內(nèi)核的一個分身觅丰,這樣操作系統(tǒng)就有能力同時處理多件事情,支持多線程的內(nèi)核就叫做多線程內(nèi)核(Multi-Threads Kernel)妨退。

程序一般不會直接去使用內(nèi)核線程妇萄,而是去使用內(nèi)核線程的一種高級接口——輕量級進程(Light Weight Process,LWP)咬荷,輕量級進程就是我們通常意義上所講的線程冠句,由于每個輕量級進程都由一個內(nèi)核線程支持,因此只有先支持內(nèi)核線程幸乒,才能有輕量級進程懦底。這種輕量級進程與內(nèi)核線程之間1:1的關系稱為一對一的線程模型,如下圖所示:


輕量級進程與內(nèi)核線程之間1:1的關系

由于內(nèi)核線程的支持逝变,每個輕量級進程都成為了一個獨立的調(diào)度單元基茵,即使有一個輕量級進程在系統(tǒng)調(diào)用中阻塞了,也不會影響整個進程繼續(xù)工作壳影,但是輕量級進程有它的局限性:

  • 首先拱层,由于是基于內(nèi)核線程實現(xiàn)的,所以各種線程操作宴咧,如創(chuàng)建根灯、析構和同步,都需要進行系統(tǒng)調(diào)用掺栅。而系統(tǒng)調(diào)用的待嫁相對較高烙肺,需要在用戶態(tài)和內(nèi)核態(tài)中來回切換。
  • 其次氧卧,每個輕量級進程都需要有一個內(nèi)核線程的支持桃笙,因此輕量級進程要消耗一定的內(nèi)核資源(如內(nèi)核線程的棧空間)沙绝,因此一個系統(tǒng)支持輕量級進程的數(shù)量是有限的搏明。

使用用戶線程實現(xiàn)

從廣義上講鼠锈,一個線程只要不是內(nèi)核線程,就可以認為是用戶線程星著,因此從這個定義上來講购笆,輕量級進程也屬于用戶線程,但輕量級進程的實現(xiàn)始終是建立在內(nèi)核之上的虚循,許多操作都要進行系統(tǒng)代用同欠,效率會受到限制。

而狹義的用戶線程指的是完全建立在用戶空間的線程庫上横缔,系統(tǒng)內(nèi)核不能感知線程存在的實現(xiàn)铺遂。用戶線程的創(chuàng)建、同步茎刚、銷毀和調(diào)度完全在用戶狀態(tài)中完成娃循,不需要內(nèi)個的幫助。如果程序?qū)崿F(xiàn)得當斗蒋,這種線程不需要切換到內(nèi)核態(tài),因此操作可以是非车阎剩快速且低消耗的泉沾,也可以支持規(guī)模更大的線程數(shù)量,部分高性能數(shù)據(jù)庫中的多線程就是由用戶線程實現(xiàn)的妇押。這種線程與用戶線程之間1:N的關系稱為一對多的線程模型跷究,如圖所示

進程與用戶線程1:N的關系

使用用戶線程的優(yōu)勢在于不需要系統(tǒng)內(nèi)核支援,劣勢也在于沒有系統(tǒng)內(nèi)核的支援敲霍,所有的線程操作都需要用戶程序自己處理俊马。線程的創(chuàng)建、切換和調(diào)度都是需要考慮的問題肩杈,而且由于操作系統(tǒng)只把處理器資源分配到進程柴我,那注入“阻塞如何處理”、“多處理器系統(tǒng)中如何將線程映射到其他處理器上”這類問題解決起來就會異常困難扩然,甚至不可能完成艘儒。因而使用用戶線程實現(xiàn)的程序一般都比較復雜,除了以前在不支持多線程的操作系統(tǒng)中的多線程和少數(shù)特殊需求的程序外夫偶,現(xiàn)在使用用戶線程的程序越來越少界睁,Java、Ruby等語言都曾經(jīng)使用過用戶線程兵拢,但最終又都放棄了翻斟。

使用用戶線程+輕量級進程混合實現(xiàn)

線程除了依賴內(nèi)核線程實現(xiàn)和完全由用戶程序自己實現(xiàn)之外,還有一種將內(nèi)核線程與用戶線程一起使用的實現(xiàn)方式说铃。在這種混合實現(xiàn)下访惜,既存在用戶線程嘹履,也存在輕量級進程。用戶線程還是完全建立在用戶空間中疾牲,因此用戶線程的創(chuàng)建植捎、切換、析構等操作依然廉價阳柔,并且可以支持大規(guī)模的用戶線程并發(fā)焰枢。而操作系統(tǒng)提供支持的輕量級進程則作為用戶線程和內(nèi)核線程之間的橋梁,這樣可以使用內(nèi)核提供的線程調(diào)度功能及處理器映射舌剂,并且用戶線程的系統(tǒng)調(diào)度要通過輕量級進程來完成济锄,大大降低了整個進程被阻塞的風險。在這種混合模式中霍转,用戶線程與輕量級進程的數(shù)量比是不一定的荐绝,即為N:M的關系。許多UNIX系列的操作系統(tǒng)都提供了N:M的線程模型實現(xiàn)避消。

用戶線程與輕量級線程N:M的關系

Java線程的實現(xiàn)

Java線程在JDK1.2之前低滩,是基于稱為“綠色線程”的用戶線程實現(xiàn)的,而在JDK1.2中岩喷,線程模型替換成基于操作系統(tǒng)原生線程模型來實現(xiàn)恕沫。因此在目前的JDK版本中,操作系統(tǒng)支持怎樣的線程模型纱意,在很大程度上決定了Java虛擬機的線程是怎樣的映射婶溯,這點不同平臺沒有辦法達成一致,虛擬機規(guī)范中也并未限定Java線程需要使用哪些線程模型來實現(xiàn)偷霉。線程模型只對線程的并發(fā)規(guī)模和操作成本產(chǎn)生影響迄委,對Java程序的編碼和運行過程來說,這些差異都是透明的类少。

對于Sun JDK來說叙身,它的Windows版和Linux版都是使用一對一的模型實現(xiàn)的,一條Java線程就映射到一條輕量級進程中硫狞,因為Windows和Linux系統(tǒng)提供的線程模型就是一對一的曲梗。

但是在Solaris平臺,由于操作系統(tǒng)的線程特性可以同時支持一對一和多對多的線程模型妓忍,因此在Solaris版的JDK也對應提供了兩個平臺專有的的虛擬機參數(shù):-XX:UseLWPSynchronization(默認值)和-XX:UseBoundThreads來明確指定虛擬機適用哪種線程模型虏两。

Java線程調(diào)度

線程調(diào)度是指系統(tǒng)為線程分配處理器使用權的過程,主要調(diào)度方式有兩種世剖,分別是協(xié)同式線程調(diào)度(Cooperative )和搶占式線程調(diào)度(Preemptive Threads-Scheduling)定罢。

如果使用協(xié)同式調(diào)度的多線程系統(tǒng),線程的執(zhí)行時間由線程本身來控制旁瘫,線程把自己的工作執(zhí)行完了之后祖凫,要主動通知系統(tǒng)切換到另外一個線程上琼蚯。協(xié)同式多線程的最大好處是實現(xiàn)簡單,而且由于線程要把自己的事情干完后才會進行線程切換惠况,切換操作對線程自己是可知的遭庶,所以沒有什么線程同步的問題。Lua語言中的“協(xié)同例程”就是這類實現(xiàn)稠屠。它的壞處也很明顯:線程執(zhí)行時間不可控制峦睡,甚至如果一個線程編寫的問題,一直不告知系統(tǒng)進行線程切換权埠,那么程序就會一直阻塞在那里榨了。很久以前的Windows3.x系統(tǒng)就是使用協(xié)同式來實現(xiàn)多線程多任務的,相當不穩(wěn)定攘蔽,一個進程堅持不讓出CPU執(zhí)行時間就可能會導致整個系統(tǒng)崩潰龙屉。

如果使用搶占式調(diào)度的多線程系統(tǒng),那么每個線程將由系統(tǒng)來分配執(zhí)行時間满俗,線程的切換不由線程本身來決定(在Java中转捕,Thread.yield()可以讓出執(zhí)行時間,但是要獲取執(zhí)行時間的話唆垃,線程本身是沒有什么辦法的)瓜富。在這種實現(xiàn)線程調(diào)度的方式下,線程的執(zhí)行時間是系統(tǒng)可控的降盹,也不會有一個線程導致整個進程阻塞的問題,Java使用的線程調(diào)度方式就是搶占式調(diào)度谤辜。與前面所說的Windows3.x的例子相對蓄坏,在Windows9x/NT內(nèi)核中就是使用搶占式來實現(xiàn)多進程的,當一個進程除了問題丑念,我們還可以使用任務管理器把這個進程殺掉涡戳,而不至于導致系統(tǒng)崩潰。

雖然Java線程調(diào)度是系統(tǒng)自動完成的脯倚,但是我們還是可以“建議”系統(tǒng)給默寫線程多分配一點執(zhí)行時間渔彰,另外的一些線程則可以少分配一點,這項操作可以通過設置線程優(yōu)先級來完成推正。Java語言一共設置了10個級別的線程優(yōu)先級(Thread.MIN_PRIORITY至Thread.MAX_PRIORITY)恍涂,這兩個線程同時處于Ready狀態(tài)時,優(yōu)先級越高的線程越容易被系統(tǒng)選擇執(zhí)行植榕。

不過優(yōu)先級并不是太靠譜再沧,原因是Java線程是通過映射到系統(tǒng)的原生線程上來實現(xiàn)的,所以線程調(diào)度最終還是取決于操作系統(tǒng)尊残,雖然現(xiàn)在很多操作系統(tǒng)都提供線程優(yōu)先級的概念炒瘸,但是并不能和Java線程的優(yōu)先級一一對應淤堵,如Solaris有2^32種概念,但win只有7種顷扩。并且一些平臺中不同的優(yōu)先級實際上也會變得相同拐邪,優(yōu)先級會被系統(tǒng)自行改變,例如隘截,在win中存在一個稱謂“優(yōu)先級推進去”的功能扎阶,它的大致作用就是當系統(tǒng)發(fā)現(xiàn)一個線程執(zhí)行得特別勤奮的話,可能會越過優(yōu)先級去為它分配執(zhí)行時間技俐。

狀態(tài)轉換

Java中定義了5種線程狀態(tài)乘陪,在任意一個時間點,一個線程只能有且僅有一種狀態(tài)雕擂,如下:

  • 新建(New):創(chuàng)建后尚未啟動的線程
  • 運行(Runable):Runable包括了操作系統(tǒng)線程狀態(tài)中的Running和Ready啡邑,也就是處于此狀態(tài)的線程有可能正在執(zhí)行,也有可能賑災等待CPU為它分配執(zhí)行時間井赌。
  • 無限等待(Waiting):處于這種狀態(tài)的線程不會被分配CPU執(zhí)行時間谤逼,它們要等待被其他線程顯式的喚醒。以下方法會讓線程陷入無限等待狀態(tài):
    • 沒有設置Timeout參數(shù)的Object.wait()方法
    • 沒有設置Timeout參數(shù)的Thread.join()方法
    • LockSupport.park()方法
  • 限期等待(Timed Waiting):處于這種狀態(tài)的線程也不會被分配CPU執(zhí)行時間仇穗,不過無須等待被其他線程顯示的喚醒流部,在一定時間后它們會由系統(tǒng)自動喚醒。以下方法會讓線程進入限期等待狀態(tài):
    • Thread.sleep()方法
    • 設置了Timeout參數(shù)的Object.wait()方法
    • 設置了Timeout參數(shù)的Thread.join()方法
    • LockSupport.parkNanos()方法
    • LockSupport.parkUnit()方法
  • 阻塞(Blocked):線程被阻塞了纹坐,“阻塞狀態(tài)”和“等待狀態(tài)”的區(qū)別在于:“阻塞狀態(tài)”在等待著獲取到一個排他鎖枝冀,這個時間將在另外一個線程放棄這個鎖的時候發(fā)生;而“等待狀態(tài)”則是在等待一段時間耘子,或者喚醒動作的發(fā)生果漾。在程序等待進入同步區(qū)域的時候,線程將進入這種狀態(tài)谷誓。
  • 結束(Terminated):已終止線程的線程狀態(tài)绒障,線程已經(jīng)結束執(zhí)行。
線程狀態(tài)切換
最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末捍歪,一起剝皮案震驚了整個濱河市户辱,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌糙臼,老刑警劉巖庐镐,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異变逃,居然都是意外死亡焚鹊,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來末患,“玉大人研叫,你說我怎么就攤上這事¤嫡耄” “怎么了嚷炉?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長探橱。 經(jīng)常有香客問我申屹,道長,這世上最難降的妖魔是什么隧膏? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任哗讥,我火速辦了婚禮,結果婚禮上胞枕,老公的妹妹穿的比我還像新娘杆煞。我一直安慰自己,他們只是感情好腐泻,可當我...
    茶點故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布决乎。 她就那樣靜靜地躺著,像睡著了一般派桩。 火紅的嫁衣襯著肌膚如雪构诚。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天铆惑,我揣著相機與錄音范嘱,去河邊找鬼。 笑死员魏,一個胖子當著我的面吹牛丑蛤,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播逆趋,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼晒奕!你這毒婦竟也來了闻书?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤脑慧,失蹤者是張志新(化名)和其女友劉穎魄眉,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體闷袒,經(jīng)...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡坑律,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了囊骤。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片晃择。...
    茶點故事閱讀 38,059評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡冀值,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出宫屠,到底是詐尸還是另有隱情列疗,我是刑警寧澤,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布浪蹂,位于F島的核電站抵栈,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏坤次。R本人自食惡果不足惜古劲,卻給世界環(huán)境...
    茶點故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望缰猴。 院中可真熱鬧产艾,春花似錦、人聲如沸洛波。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽蹬挤。三九已至缚窿,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間焰扳,已是汗流浹背倦零。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留吨悍,地道東北人扫茅。 一個月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像育瓜,于是被迫代替她去往敵國和親葫隙。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,792評論 2 345

推薦閱讀更多精彩內(nèi)容