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

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

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

硬件的效率與一致性

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

基于高速緩存的存儲(chǔ)交互很好的解決了處理器的內(nèi)存與內(nèi)存的速度矛盾,但是也為計(jì)算機(jī)系統(tǒng)帶來了更高的復(fù)雜度,因?yàn)樗肓艘粋€(gè)新的問題:緩存一致性(Cache Coherence)。在多處理器系統(tǒng)中,每個(gè)處理器都有自己的告訴緩存,而它們又共享同一內(nèi)存(Main Memory)。當(dāng)多個(gè)處理器的運(yùn)算任務(wù)都涉及同一塊主內(nèi)存區(qū)域時(shí),將可能導(dǎo)致各自緩存數(shù)據(jù)不一致,如果真的發(fā)生這種情況,那同步回到主內(nèi)存時(shí)以誰的緩存數(shù)據(jù)為準(zhǔn)呢?為了解決一致性的問題,需要各個(gè)處理器訪問緩存時(shí)都遵循一些協(xié)議,在讀寫時(shí)要根據(jù)協(xié)議來進(jìn)行操作,這類協(xié)議有MSI、MESI、MOSI、Synapse、Firefly和Dragon Protocol等。多次提到的“內(nèi)存模型”,可以理解為在特定的協(xié)議下,對(duì)特定的內(nèi)存或告訴緩存進(jìn)行讀寫訪問的抽象過程。不同結(jié)構(gòu)的物理機(jī)器可以擁有不一樣的內(nèi)存模型,而JVM也有自己的內(nèi)存模型,并且這里介紹的內(nèi)存訪問操作與硬件的緩存訪問操作具有很高的可比性。

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

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

Java內(nèi)存模型

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

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

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

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

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


線程、主內(nèi)存、工作內(nèi)存三者的交互關(guān)系

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

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

  • lock(鎖定):作用于主內(nèi)存變量,它把一個(gè)變量標(biāo)識(shí)為一條線程獨(dú)占的狀態(tài)。
  • unlock(解鎖):作用于主內(nèi)存的變量,它把一個(gè)處于鎖定狀態(tài)的變量釋放出來,釋放后的變量才可以被其他線程鎖定。
  • read(讀?。鹤饔糜蛑鲀?nèi)存的變量,它把一個(gè)變量的值從主內(nèi)存?zhèn)鬏數(shù)骄€程的工作內(nèi)存中,以便隨后的load動(dòng)作使用。
  • load(載入):作用于工作內(nèi)存的變量,它把read操作從主內(nèi)存中得到的變量值放入工作內(nèi)存的變量副本中。
  • use(使用):作用于工作內(nèi)存的變量,它把工作內(nèi)存中的一個(gè)變量的值傳遞給執(zhí)行引擎,每當(dāng)虛擬機(jī)遇到一個(gè)需要使用到變量的值的字節(jié)碼指令時(shí)將會(huì)執(zhí)行這個(gè)操作。
  • assign(賦值):作用于工作內(nèi)存變量,它把一個(gè)從執(zhí)行引擎接收到的賦值給工作內(nèi)存的變量,每當(dāng)虛擬機(jī)遇到一個(gè)變量賦值的字節(jié)碼時(shí)執(zhí)行這個(gè)操作。
  • store(存儲(chǔ)):作用于工作內(nèi)存的變量,它把工作內(nèi)存中一個(gè)變量的值傳遞到內(nèi)存中,以便隨后的write操作使用。
  • write(寫入):作用于主內(nèi)存的變量,它把store操作從工作內(nèi)存中得到的變量的值放入主內(nèi)存的變量中。

如果要把一個(gè)變量從主內(nèi)存復(fù)制到工作內(nèi)存,那就要順序地執(zhí)行read和load操作,如果要把變量從工作內(nèi)存同步回主內(nèi)存,就要順序的執(zhí)行store和write操作。注意,Java內(nèi)存模型只要求上述兩個(gè)操作必須按順序執(zhí)行,而沒有保證是連續(xù)執(zhí)行。也就是說,read與load之間、store與write之間是可以插入其他指令的,如對(duì)主內(nèi)存中的變量a、b進(jìn)行訪問時(shí),一種可能出現(xiàn)順序是read a、read b、load b、load a。初次之外,Java內(nèi)存模型還規(guī)定了在執(zhí)行上述8種基本操作時(shí)必須滿足如下規(guī)則:

  • 不允許read和load、store和write操作之一單獨(dú)出現(xiàn),即不允許一個(gè)變量從主內(nèi)存讀取了但工作內(nèi)存不接受,或者從工作內(nèi)存發(fā)起回寫了但主內(nèi)存不接受的情況出現(xiàn)。
  • 不允許一個(gè)線程丟棄它的最近的assign操作,即變量在工作內(nèi)存中改變了之后必須把該變化同步回主內(nèi)存。
  • 不允許一個(gè)線程無原因的(沒有發(fā)生過任何assign操作)把數(shù)據(jù)從線程工作內(nèi)存同步回主內(nèi)存中。
  • 一個(gè)新的變量只能在主內(nèi)存中誕生,不允許在工作內(nèi)存中直接使用一個(gè)未被初始化(load或assign)的變量,換句話說,就是對(duì)一個(gè)變量實(shí)施use、store操作之前,必須先執(zhí)行assign和load操作。
  • 一個(gè)變量在用一個(gè)時(shí)刻只允許一條線程對(duì)其進(jìn)行l(wèi)ock操作,但lock操作可以被同一條線程重復(fù)執(zhí)行多次,多次執(zhí)行l(wèi)ock之后,只有執(zhí)行相同次數(shù)的unlock操作,變量才會(huì)解鎖。
  • 如果對(duì)一個(gè)變量執(zhí)行l(wèi)ock操作,那將會(huì)情況工作內(nèi)存中詞變量的值,在執(zhí)行引擎使用這個(gè)變量之前,需要重新執(zhí)行l(wèi)oad或assign操作初始化變量的值。
  • 如果一個(gè)變量事先沒有被lock操作鎖定,那就不允許對(duì)它執(zhí)行unlock操作,也不允許去unlock一個(gè)被其他線程鎖定住的變量。
  • 對(duì)一個(gè)變量執(zhí)行unlock操作之前,必須先把此變量同步回主內(nèi)存中(執(zhí)行store、write操作)。

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

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

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

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

關(guān)于volatile變量的可見性,經(jīng)常會(huì)被開發(fā)人員誤解,認(rèn)為以下描述成立:“volatile變量對(duì)所有線程是立即可見的,對(duì)volatile變量所有的寫操作都能立即反應(yīng)到其他線程中,換句話說,volatile變量在各個(gè)線程中是一致的,所以基于volatile變量的運(yùn)算在并發(fā)下是安全的?!边@個(gè)結(jié)論。 volatile變量在各個(gè)線程的工作內(nèi)存中不存在一致性問題(在各個(gè)線程的工作內(nèi)存中,volatile變量也可以存在不一致的情況,但由于每次使用之前都要先刷新,執(zhí)行引擎看不到不一致的情況,因此變量也可以認(rèn)為不存在一致性問題),但是Java里面的運(yùn)算并非原子操作,導(dǎo)致volatile變量的運(yùn)算在并發(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個(gè)線程,每個(gè)線程對(duì)race變量進(jìn)行10000次自增操作,如果這段代碼能夠正確并發(fā)的話,最后輸出的結(jié)果應(yīng)該是200000。但是在運(yùn)行完這段代碼之后并沒有得到這個(gè)預(yù)期的結(jié)果,而且會(huì)發(fā)現(xiàn)每次運(yùn)行程序,輸出的結(jié)果都不一樣,都是小于200000的數(shù)字。問題出在了自增運(yùn)算“race++”上,我們用javap反編譯這段代碼后會(huì)得到如下代碼清單:

  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é)碼指令構(gòu)成的,從字節(jié)碼層面上很容易就分析出并發(fā)失敗的原因:當(dāng)getstatic指令把race的值取到操作數(shù)棧的棧頂時(shí),volatile關(guān)鍵字保證了race的值在此時(shí)是正確的,但是在執(zhí)行iconst_1、iadd這些指令的時(shí)候,其他線程可能已經(jīng)把race的值增大了,而在操作棧頂?shù)闹稻妥兂闪诉^期數(shù)據(jù),所以putstatic指令執(zhí)行后就可能把較小的race同步回主內(nèi)存之中。

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

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

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

volatile boolean shutdownRequseted;

public void shutdown() {
    shutdownRequseted = true;
}

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

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

指令重排序是并發(fā)編程中最容易讓開發(fā)人員產(chǎn)生疑惑的地方,比如,一段標(biāo)準(zhǔn)的DCL單例代碼,可以觀察到加入了volatile和未加入volatile關(guān)鍵字時(shí)所產(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();
    }
}

編譯后,這段代碼對(duì)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

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

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

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

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

  • 只有當(dāng)線程T對(duì)變量V執(zhí)行的前一個(gè)動(dòng)作是load的時(shí)候,線程T才能對(duì)變量V執(zhí)行use動(dòng)作;并且,只有當(dāng)線程T對(duì)變量V執(zhí)行的后一個(gè)動(dòng)作是use的時(shí)候,線程T才能對(duì)變量V執(zhí)行l(wèi)oad動(dòng)作。線程T對(duì)變量V的use動(dòng)作可以認(rèn)為是和線程T對(duì)變量V的load、read動(dòng)作相關(guān)聯(lián),必須連續(xù)一起出現(xiàn)(這條規(guī)則要求在工作線程中,每次使用V前都必須從主內(nèi)存刷新最新的值,用于保證能看見其他線程對(duì)變量V所做的修改后的值)。
  • 只有當(dāng)線程T對(duì)變量V執(zhí)行的前一個(gè)動(dòng)作是assign的時(shí)候,線程T才能對(duì)變量V執(zhí)行store動(dòng)作;并且,只有當(dāng)線程T對(duì)變量V執(zhí)行的后一個(gè)動(dòng)作是store的時(shí)候,線程T才能對(duì)變量V執(zhí)行assign動(dòng)作。線程T對(duì)變量V的assign動(dòng)作認(rèn)為是和線程T對(duì)變量V的store、write動(dòng)作相關(guān)聯(lián),必須一起出現(xiàn)(這條規(guī)則要求在工作線程中,每次修改V后都必須立刻同步回主內(nèi)存中,用于確保其他線程可以看到自己對(duì)變量V所做的修改)。
  • 假定動(dòng)作A是線程T對(duì)變量V實(shí)施的use或assign動(dòng)作,假定動(dòng)作F是和動(dòng)作A相關(guān)聯(lián)的load或store動(dòng)作,假定動(dòng)作P是和動(dòng)作F相應(yīng)的對(duì)變量V的read或者write動(dòng)作;類似的,假定動(dòng)作B是線程T對(duì)變量W實(shí)施的use或者assign動(dòng)作,假定動(dòng)作G是和動(dòng)作B相關(guān)聯(lián)的load或者store動(dòng)作,假定動(dòng)作Q是和動(dòng)作G相應(yīng)的對(duì)變量W的read或write動(dòng)作。如果A先于B,那么P先于Q(這條規(guī)則要求volatile修飾的變量不會(huì)被指令重排序優(yōu)化,保證代碼是執(zhí)行順序與程序的順序相同)。

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

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

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

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

原子性、可見性和有序性

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

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

先行發(fā)生原則

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

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

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

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

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

假設(shè)線程A中的操作“i=1”先行發(fā)生于線程B的操作“j=1”,那么可以確定在線程B的操作執(zhí)行后,變量j的值一定等于1,得出這個(gè)結(jié)論的依據(jù)有兩個(gè):一是根據(jù)先行發(fā)生原則,“i=1”的結(jié)果可以被觀察到;二是線程C還沒“登場”,線程A操作結(jié)束之后沒有其他線程會(huì)修改變量i的值?,F(xiàn)在再來考慮線程C,我們依然保持線程A和線程B之間的先行發(fā)生關(guān)系,而線程C出現(xiàn)在線程A和線程B的操作之間,但是線程C與線程B沒有先行發(fā)生關(guān)系,那j的值會(huì)是多少呢?答案是不確定。1和2都有可能,因?yàn)榫€程C對(duì)變量i的影響可能會(huì)被線程B觀察到,也可能不會(huì),這個(gè)時(shí)候線程B就存在讀取到過期數(shù)據(jù)的風(fēng)險(xiǎn),不具備多線程安全性。

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

  • 程序次序規(guī)則:在一個(gè)線程中,按照程序代碼順序,書寫在前面的操作先行發(fā)生于書寫在后面的操作。準(zhǔn)確的說,應(yīng)該是控制流順序而不是程序代碼順序,因?yàn)橐紤]分支、循環(huán)等結(jié)構(gòu)。
  • 管程鎖定規(guī)則:一個(gè)unlock操作先行發(fā)生于后面對(duì)同一個(gè)鎖的lock操作。這里必須強(qiáng)調(diào)的是同一個(gè)鎖,而“后面”是指時(shí)間上的先后順序。
  • volatile變量規(guī)則:對(duì)一個(gè)volatile變量的寫操作先行發(fā)生于后面對(duì)這個(gè)變量的讀操作,這里的“后面”同樣是指時(shí)間上的先后順序。
  • 線程啟動(dòng)規(guī)則:Thread對(duì)象的start()方法先行發(fā)生于此線程的每一個(gè)動(dòng)作。
  • 線程中斷規(guī)則:線程中的所有操作都先行發(fā)生于對(duì)此線程的終止檢測,我們可以通過Thread.join()方法結(jié)束、Thread.isAlive()的返回值等手段檢測到線程已經(jīng)終止執(zhí)行。
  • 對(duì)象終結(jié)規(guī)則:一個(gè)對(duì)象的初始化完成(構(gòu)造函數(shù)執(zhí)行結(jié)束)先行發(fā)生于它的finalize()方法的開始。
  • 傳遞性:如果操作A先行發(fā)生于操作B,操作B先行發(fā)生于操作C,那就可以得出操作A先行發(fā)生于操作C的結(jié)論。

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

private int value = 0;

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

public int getValue() {
    return value;
}

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

我們依次分析一下發(fā)生原則中的各項(xiàng)規(guī)則,由于兩個(gè)方法分別由線程A和線程B調(diào)用,不在一個(gè)線程中,所以程序次序規(guī)則在這里不適用;由于沒有同步塊,自然就不會(huì)發(fā)生lock和unlock的操作,所以管程鎖定規(guī)則也不是適用;由于value變量沒有被volatile關(guān)鍵字修飾,所以volatile變量規(guī)則不適用;后面的線程啟動(dòng)、終止、中斷規(guī)則和對(duì)象終結(jié)規(guī)則也和這里完全沒關(guān)系。因?yàn)闆]有一個(gè)適用的先行發(fā)生規(guī)則,所以最后一天傳遞性也無從談起,因此我們可以判斷盡管線程A在操作時(shí)間上先于線程B,但是無法確定線程B中“getValue”方法的返回結(jié)果,換句話說,這里面的操作不是線程安全的。

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

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

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

這兩條語句都是在同一個(gè)線程之中,根據(jù)程序次序規(guī)則,“int i=1”的操作先行發(fā)生于“int j=2”,但是“int j=2”的代碼完全可能先被處理器執(zhí)行,這并不影響先行發(fā)生原則的正確性,因?yàn)槲覀冊谶@條線程之中沒有辦法感知到這點(diǎn)。

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

Java與線程

并發(fā)不一定要依賴多線程(如PHP中很常見的多線程并發(fā)),但是在Java里面談?wù)摬l(fā),大多數(shù)都與線程脫不開關(guān)系。

線程的實(shí)現(xiàn)

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

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

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

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

內(nèi)核線程(Kernel-Level Thread,KLT)就是直接由操作系統(tǒng)內(nèi)核(Kernel,下面統(tǒng)稱內(nèi)核)支持的線程,這種線程由內(nèi)核來完成線程切換,內(nèi)核通過操縱調(diào)度器(Scheduler)對(duì)線程進(jìn)行調(diào)度,并負(fù)責(zé)將線程的任務(wù)映射到各個(gè)處理器上。每個(gè)內(nèi)核線程可以視為內(nèi)核的一個(gè)分身,這樣操作系統(tǒng)就有能力同時(shí)處理多件事情,支持多線程的內(nèi)核就叫做多線程內(nèi)核(Multi-Threads Kernel)。

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


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

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

  • 首先,由于是基于內(nèi)核線程實(shí)現(xiàn)的,所以各種線程操作,如創(chuàng)建、析構(gòu)和同步,都需要進(jìn)行系統(tǒng)調(diào)用。而系統(tǒng)調(diào)用的待嫁相對(duì)較高,需要在用戶態(tài)和內(nèi)核態(tài)中來回切換。
  • 其次,每個(gè)輕量級(jí)進(jìn)程都需要有一個(gè)內(nèi)核線程的支持,因此輕量級(jí)進(jìn)程要消耗一定的內(nèi)核資源(如內(nèi)核線程的棧空間),因此一個(gè)系統(tǒng)支持輕量級(jí)進(jìn)程的數(shù)量是有限的。

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

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

而狹義的用戶線程指的是完全建立在用戶空間的線程庫上,系統(tǒng)內(nèi)核不能感知線程存在的實(shí)現(xiàn)。用戶線程的創(chuàng)建、同步、銷毀和調(diào)度完全在用戶狀態(tài)中完成,不需要內(nèi)個(gè)的幫助。如果程序?qū)崿F(xiàn)得當(dāng),這種線程不需要切換到內(nèi)核態(tài),因此操作可以是非??焖偾业拖牡?,也可以支持規(guī)模更大的線程數(shù)量,部分高性能數(shù)據(jù)庫中的多線程就是由用戶線程實(shí)現(xiàn)的。這種線程與用戶線程之間1:N的關(guān)系稱為一對(duì)多的線程模型,如圖所示

進(jìn)程與用戶線程1:N的關(guān)系

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

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

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

用戶線程與輕量級(jí)線程N(yùn):M的關(guān)系

Java線程的實(shí)現(xiàn)

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

對(duì)于Sun JDK來說,它的Windows版和Linux版都是使用一對(duì)一的模型實(shí)現(xiàn)的,一條Java線程就映射到一條輕量級(jí)進(jìn)程中,因?yàn)閃indows和Linux系統(tǒng)提供的線程模型就是一對(duì)一的。

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

Java線程調(diào)度

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

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

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

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

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

狀態(tài)轉(zhuǎn)換

Java中定義了5種線程狀態(tài),在任意一個(gè)時(shí)間點(diǎn),一個(gè)線程只能有且僅有一種狀態(tài),如下:

  • 新建(New):創(chuàng)建后尚未啟動(dòng)的線程
  • 運(yùn)行(Runable):Runable包括了操作系統(tǒng)線程狀態(tài)中的Running和Ready,也就是處于此狀態(tài)的線程有可能正在執(zhí)行,也有可能賑災(zāi)等待CPU為它分配執(zhí)行時(shí)間。
  • 無限等待(Waiting):處于這種狀態(tài)的線程不會(huì)被分配CPU執(zhí)行時(shí)間,它們要等待被其他線程顯式的喚醒。以下方法會(huì)讓線程陷入無限等待狀態(tài):
    • 沒有設(shè)置Timeout參數(shù)的Object.wait()方法
    • 沒有設(shè)置Timeout參數(shù)的Thread.join()方法
    • LockSupport.park()方法
  • 限期等待(Timed Waiting):處于這種狀態(tài)的線程也不會(huì)被分配CPU執(zhí)行時(shí)間,不過無須等待被其他線程顯示的喚醒,在一定時(shí)間后它們會(huì)由系統(tǒng)自動(dòng)喚醒。以下方法會(huì)讓線程進(jìn)入限期等待狀態(tài):
    • Thread.sleep()方法
    • 設(shè)置了Timeout參數(shù)的Object.wait()方法
    • 設(shè)置了Timeout參數(shù)的Thread.join()方法
    • LockSupport.parkNanos()方法
    • LockSupport.parkUnit()方法
  • 阻塞(Blocked):線程被阻塞了,“阻塞狀態(tài)”和“等待狀態(tài)”的區(qū)別在于:“阻塞狀態(tài)”在等待著獲取到一個(gè)排他鎖,這個(gè)時(shí)間將在另外一個(gè)線程放棄這個(gè)鎖的時(shí)候發(fā)生;而“等待狀態(tài)”則是在等待一段時(shí)間,或者喚醒動(dòng)作的發(fā)生。在程序等待進(jìn)入同步區(qū)域的時(shí)候,線程將進(jìn)入這種狀態(tài)。
  • 結(jié)束(Terminated):已終止線程的線程狀態(tài),線程已經(jīng)結(jié)束執(zhí)行。
線程狀態(tài)切換
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容