Java內(nèi)存模型
并發(fā)編程模型中的兩個關(guān)鍵問題
在并發(fā)編程中有兩個關(guān)鍵的問題:
- 線程之間如何通信
- 線程之間如何同步
其中通信是指線程之間以何種機制來交換信息疮胖。在命令式編程中环戈,線程之間的通信機制有兩種:共享內(nèi)存和消息傳遞闷板。Android中的Handler就是屬于消息傳遞。
在共享內(nèi)存的并發(fā)模型里院塞,線程之間共享程序的公共狀態(tài)遮晚,通過寫-讀內(nèi)存的公共狀態(tài)進行隱式通信。在消息傳遞的模型里拦止,線程沒有公共的狀態(tài)县遣,線程之間必須通過發(fā)送消息來進行通信。
同步是指程序中用于控制不同線程操作發(fā)生的相對順序的機制汹族。在共享內(nèi)存并發(fā)模型里萧求,同步是顯示進行的。必須顯示的指定某個方法或某段代碼需要在線程之間互斥執(zhí)行顶瞒。在消息傳遞的模型里夸政,由于消息的傳遞的發(fā)送必須在消息的接收之前,因此同步是隱式進行的榴徐。
Java采用的并發(fā)模型是共享內(nèi)存的模型守问。
Java內(nèi)存模型的抽象結(jié)構(gòu)
在Java中,所有的實例域坑资,靜態(tài)域和數(shù)組元素都存儲在堆內(nèi)存中耗帕,堆內(nèi)存在線程之間共享。而這些被共享的變量一般被稱為共享變量袱贮。局部變量仿便,方法定義的參數(shù)和異常處理器的參數(shù)不會在線程之間共享,它們不會有內(nèi)存可見性問題攒巍,也不受內(nèi)存模型的影響探越。
也就是說:無狀態(tài)的類是安全的。
Java內(nèi)存模型簡稱JMM窑业,Java線程之間的通信由JMM控制钦幔,JMM決定了一個線程對共享變量的寫入何時對另一個線程可見。從抽象的角度來看常柄,JMM定義了線程和主內(nèi)存之間的抽象關(guān)系:
線程的共享變量存儲在主內(nèi)存中(Main Memory)鲤氢,每一個線程都有一個私有的本地內(nèi)存(Local Memory),本地內(nèi)存中存儲了線程以讀/寫共享變量的副本西潘。本地內(nèi)存是一個JMM的抽象概念卷玉,并不真實存在。它涵蓋了緩存喷市、寫緩存區(qū)相种、寄存器以及其他的硬件和編譯器優(yōu)化。Java內(nèi)存模型的抽象示意圖如下:
如果線程A與B之間需要通信的話品姓,必須要經(jīng)歷下面的兩個步驟:
- 線程A把本地內(nèi)存A中更新過的共享變量刷新到主存中去寝并。
- 線程B到主存中去讀取線程A之前已經(jīng)更新過的共享變量箫措。
從源碼到執(zhí)行序列的重排序
在執(zhí)行程序的時候,為了提供性能衬潦,編譯器和處理器常常會對執(zhí)行做重排序斤蔓。重排序分為三種類型。
- 編譯器優(yōu)化重排序镀岛。編譯器在不改變單線程語言的前提下弦牡,可以重新安排語句的執(zhí)行順序。
- 指令級并行的重排序∑颍現(xiàn)代處理器采用了指令級并行技術(shù)(Instruction-Level Parallelism,ILP)來將多條指令重疊執(zhí)行驾锰。如果不存在數(shù)據(jù)依賴性,那么可以改變語句對應機器指令的執(zhí)行順序走越。
- 內(nèi)存系統(tǒng)的重排序椭豫。由于處理機使用緩存讀/寫緩沖區(qū),這使得加載和存儲操作看起來實在亂序執(zhí)行买喧。
從Java源代碼到最終執(zhí)行的指令序列捻悯,會經(jīng)過下面的3重排序匆赃。
1屬于編譯器重排序淤毛,2和3屬于處理器重排序算柳。這些重排序問題可能會導致出現(xiàn)內(nèi)存可見性的問題低淡。
對于編譯器,JMM的編譯器重排序規(guī)則會禁止特定類型的編譯器重排序瞬项。對于處理器重排序囱淋,JMM的處理器重排序規(guī)則會要求Java編譯器在生成指令序列的時猪杭,插入特定的內(nèi)存屏障(Memory Barriers)指令,通過這些內(nèi)存屏障了來禁止特定類型的處理器重排序妥衣。
內(nèi)存屏障是一組計算機指令皂吮,用于實現(xiàn)對內(nèi)存操作的順序限制。
并發(fā)編程的模型分類
現(xiàn)代的處理器使用緩沖區(qū)臨時保存向內(nèi)存寫入的數(shù)據(jù)税手。寫緩存沖可以保證執(zhí)行流水線持續(xù)運行蜂筹,它可以避免由于處理器停頓下來向內(nèi)存寫入數(shù)據(jù)而產(chǎn)生的延遲。同時可以通過批處理的方法刷新緩沖區(qū)芦倒,以及合并寫緩沖區(qū)中對同一地址的多次寫艺挪,減少對內(nèi)存總線的占用。
但是有一個問題兵扬,每個處理器上的寫緩沖區(qū)僅僅對它所在的處理器可見麻裳。這個特性會對內(nèi)存操作的執(zhí)行順序產(chǎn)生影響:處理器對內(nèi)存的讀/寫操作的執(zhí)行順序口蝠,不一定與內(nèi)存發(fā)生的實際的讀寫順序一致。
為了保證可見性掂器,Java編譯器會在生成指令的適當位置會插入內(nèi)存屏障的指令來禁止特定類型的處理器重排序亚皂。JMM吧內(nèi)存屏障分為4類。
屏障類型 | 指令示例 | 說明 |
---|---|---|
LoadLoad屏障 | Load1; LoadLoad; Load2 | 在Load2及后續(xù)讀取操作要讀取的數(shù)據(jù)被訪問前国瓮,保證Load1要讀取的數(shù)據(jù)被讀取完畢灭必。 |
StoreStore屏障 | Store1; StoreStore; Store2 | 在Store2及后續(xù)寫入操作執(zhí)行前,保證Store1的寫入操作對其它處理器可見乃摹。 |
LoadStore屏障 | Load1; LoadStore; Store2 | 在Store2及后續(xù)寫入操作被刷出前禁漓,保證Load1要讀取的數(shù)據(jù)被讀取完畢。 |
StoreLoad屏障 | Store1; StoreLoad; Load2 | 在Load2及后續(xù)所有讀取操作執(zhí)行前孵睬,保證Store1的寫入對所有處理器可見播歼。它的開銷是四種屏障中最大的。 |
StoreLoad屏障是一個“全能型”的屏障掰读。它同時具有其他三個屏障的效果秘狞。
happens-before簡介
從jdk1.5開始,Java使用新的JSR-133內(nèi)存模型蹈集。JSR-133使用happens-before的概念來闡述操作之間的內(nèi)存可見性烁试。在JVM中,如果一個操作執(zhí)行的結(jié)果需要對另一個操作可見拢肆,那么這兩個操作操作之間必須存在happens-before關(guān)系减响,這里提到的兩個操作既可以是在一個線程之內(nèi),也可以是在不同線程之間郭怪。
happens-before規(guī)則如下:
1支示、程序次序規(guī)則:在一個單獨的線程中,按照程序代碼的執(zhí)行流順序鄙才,(時間上)先執(zhí)行的操作happen—before(時間上)后執(zhí)行的操作颂鸿。
2、管理鎖定規(guī)則:一個unlock操作happen—before后面(時間上的先后順序攒庵,下同)對同一個鎖的lock操作嘴纺。
3、volatile變量規(guī)則:對一個volatile變量的寫操作happen—before后面對該變量的讀操作叙甸。
4颖医、線程啟動規(guī)則:Thread對象的start()方法happen—before此線程的每一個動作。
5裆蒸、線程終止規(guī)則:線程的所有操作都happen—before對此線程的終止檢測熔萧,可以通過Thread.join()方法結(jié)束、Thread.isAlive()的返回值等手段檢測到線程已經(jīng)終止執(zhí)行。
6佛致、線程中斷規(guī)則:對線程interrupt()方法的調(diào)用happen—before發(fā)生于被中斷線程的代碼檢測到中斷時事件的發(fā)生贮缕。
7、對象終結(jié)規(guī)則:一個對象的初始化完成(構(gòu)造函數(shù)執(zhí)行結(jié)束)happen—before它的finalize()方法的開始俺榆。
8感昼、傳遞性:如果操作A happen—before操作B,操作B happen—before操作C罐脊,那么可以得出A happen—before操作C定嗓。
一個happens-before規(guī)則對應與一個或多個編譯器和處理器重排序規(guī)則。