1衅金、基礎(chǔ)與概念
(1)、共享性簿煌、互斥性典挑、原子性、可見性啦吧、有序性您觉。
http://www.cnblogs.com/paddix/p/5374810.html
(2)、JMM內(nèi)存模型——描述線程本地內(nèi)存和主內(nèi)存之間的抽象關(guān)系授滓。線程A和線程B之間通訊琳水,需要通過主內(nèi)存肆糕。
JMM屬于語言級的內(nèi)存模型,它確保在不同的編譯器和不同的處理器平臺之上在孝,通過禁止特定類型的編譯器重排序和處理器重排序诚啃,為程序員提供一致的內(nèi)存可見性保證。
注意私沮,線程本地內(nèi)存只是一個抽象概念始赎,它涵蓋了緩存、寫緩沖區(qū)仔燕、寄存器以及其他的硬件和編譯器優(yōu)化造垛。
2、重排序
在執(zhí)行程序時晰搀,為了提高性能五辽,編譯器和處理器常常會對指令做重排序。
重排序分類
(1)外恕、編譯器優(yōu)化的重排序:編譯器在不改變單線程程序語義的前提下杆逗,可以重新安排語句的執(zhí)行順序。
(2)鳞疲、指令級并行的重排序:現(xiàn)代處理器采用了指令級并行技術(shù)(Instruction-Level Parallelism罪郊,ILP)來將多條指令重疊執(zhí)行。如果不存在數(shù)據(jù)依賴性尚洽,處理器可以改變語句對應(yīng)機(jī)器指令的執(zhí)行順序悔橄。
(3)、內(nèi)存系統(tǒng)的重排序:由于處理器使用緩存和讀/寫緩沖區(qū)翎朱,這使得加載和存儲操作看上去可能是在亂序執(zhí)行橄维。如下圖:
重排序的原則:as-if-serial語義
as-if-serial語義的意思是:不管怎么重排序(編譯器和處理器為了提高并行度),(單線程)程序的執(zhí)行結(jié)果不能被改變拴曲。編譯器争舞、runtime和處理器都必須遵守as-if-serial語義。
為了遵守as-if-serial語義澈灼,編譯器和處理器不會對存在數(shù)據(jù)依賴關(guān)系的操作做重排序竞川。數(shù)據(jù)依賴關(guān)系如下圖所示:
as-if-serial語義只能保證單線程下,重排序引起的問題叁熔。在多線程情況下委乌,不存在數(shù)據(jù)依賴關(guān)系的重排序也會破壞程序的意圖。
單線程情況下荣回,控制依賴關(guān)系的重排序遭贸,不影響最終結(jié)果。多線程情況下心软,則可能會破壞程序的意圖壕吹。
JMM禁止重排序的措施:
(1)著蛙、對于編譯器,JMM的編譯器重排序規(guī)則會禁止特定類型的編譯器重排序(不是所有的編譯器重排序都要禁止)耳贬。
(2)踏堡、對于處理器重排序,JMM的處理器重排序規(guī)則會要求Java編譯器在生成指令序列時咒劲,插入特定類型的內(nèi)存屏障(Memory Barriers顷蟆,Intel稱之為MemoryFence)指令,通過內(nèi)存屏障指令來禁止特定類型的處理器重排序腐魂。
從上圖可以看出:常見的處理器都允許Store-Load重排序帐偎;常見的處理器都不允許對存在數(shù)據(jù)依賴的操作做重排序。sparc-TSO和X86擁有相對較強(qiáng)的處理器內(nèi)存模型挤渔,它們僅允許對寫-讀操作做重排序(因為它們都使用了寫緩沖區(qū))肮街。
內(nèi)存屏障如上圖4種類型风题,StoreLoad Barriers是一個“全能型”的屏障判导,它同時具有其他3個屏障的效果。現(xiàn)代的多處理器大多支持該屏障(其他類型的屏障不一定被所有處理器支持)沛硅。執(zhí)行該屏障開銷會很昂貴眼刃,因為當(dāng)前處理器通常要把寫緩沖區(qū)中的數(shù)據(jù)全部刷新到內(nèi)存中(Buffer Fully Flush)。
3摇肌、happen-before
JDK5之后擂红,采用JSR-133版本的JMM內(nèi)存模型,使用hap-pens-before的概念來闡述操作之間的內(nèi)存可見性围小。在JMM中昵骤,如果一個操作執(zhí)行的結(jié)果需要對另一個操作可見,那么這兩個操作之間必須要存在happens-before關(guān)系肯适。這里提到的兩個操作既可以是在一個線程之內(nèi)变秦,也可以是在不同線程之間。happens-before規(guī)則如下:
程序順序規(guī)則:一個線程中的每個操作框舔,happens-before于該線程中的任意后續(xù)操作蹦玫。?
監(jiān)視器鎖規(guī)則:對一個鎖的解鎖,happens-before于隨后對這個鎖的加鎖刘绣。?
volatile變量規(guī)則:對一個volatile域的寫樱溉,happens-before于任意后續(xù)對這個volatile域的讀。?
傳遞性:如果A happens-before B纬凤,且B happens-beforeC福贞,那么A happens-before C。
start()規(guī)則:如果線程A執(zhí)行操作ThreadB.start()(啟動線程B)停士,那么A線程的ThreadB.start()操作happens-before于線程B中的任意操作挖帘。
join()規(guī)則:如果線程A執(zhí)行操作ThreadB.join()并成功返回绢馍,那么線程B中的任意操作happens-before于線程A從ThreadB.join()操作成功返回。
對于Java程序員來說肠套,happens-before規(guī)則簡單易懂舰涌,它避免Java程序員為了理解JMM提供的內(nèi)存可見性保證而去學(xué)習(xí)復(fù)雜的重排序規(guī)則以及這些規(guī)則的具體實現(xiàn)方法。
4你稚、順序一致性
順序一致性內(nèi)存模型是一個理論參考模型瓷耙。順序一致性內(nèi)存模型有兩大特性:
1)一個線程中的所有操作必須按照程序的順序來執(zhí)行。
2)(不管程序是否同步)所有線程都只能看到一個單一的操作執(zhí)行順序刁赖。在順序一致性內(nèi)存模型中搁痛,每個操作都必須原子執(zhí)行且立刻對所有線程可見。
5宇弛、volatile
5-1鸡典、volatile實現(xiàn)原理
1)將當(dāng)前處理器緩存行的數(shù)據(jù)寫回到系統(tǒng)內(nèi)存。(Lock前綴指令.“緩存鎖定”枪芒,阻止兩個或以上處理器同時修改被緩存的內(nèi)存區(qū)域)
2)這個寫回內(nèi)存的操作會彻况,其他處理器“嗅探”到,使在其他CPU里緩存了該內(nèi)存地址的數(shù)據(jù)無效舅踪。
5-2纽甘、volatile特性
可見性:對一個volatile變量的讀,總是能看到(任意線程)對這個volatile變量最后的寫入抽碌。
原子性:對任意單個volatile變量的讀/寫具有原子性悍赢,但類似于volatile++這種復(fù)合操作不具有原子性。
5-3、volatile寫-讀建立的happens-before關(guān)系
volatile的寫-讀與鎖的釋放-獲取有相同的內(nèi)存效果。
這里A線程寫一個volatile變量后准验,B線程讀同一個volatile變量冠句。A線程在寫volatile變量之前所有可見的共享變量,在B線程讀同一個volatile變量后,將立即變得對B線程可見。
5-4、volatile寫-讀的內(nèi)存語義
volatile寫的內(nèi)存語義如下:當(dāng)寫一個volatile變量時瀑梗,JMM會把該線程對應(yīng)的本地內(nèi)存中的共享變量值刷新到主內(nèi)存。
volatile讀的內(nèi)存語義如下:當(dāng)讀一個volatile變量時裳扯,JMM會把該線程對應(yīng)的本地內(nèi)存置為無效抛丽。線程接下來將從主內(nèi)存中讀取共享變量。
注:關(guān)于volatile變量重排序饰豺,嚴(yán)格限制編譯器和處理器對volatile變量與普通變量的重排序亿鲜,確保volatile的寫-讀和鎖的釋放-獲取具有相同的內(nèi)存語義。
5-5、volatile和鎖的區(qū)別
由于volatile僅僅保證對單個volatile變量的讀/寫具有原子性蒿柳,而鎖的互斥執(zhí)行的特性可以確保對整個臨界區(qū)代碼的執(zhí)行具有原子性饶套。在功能上,鎖比volatile更強(qiáng)大垒探;在可伸縮性和執(zhí)行性能上妓蛮,volatile更有優(yōu)勢。
volatile的不能完全取代Synchronized的位置圾叼,只有在一些特殊的場景下蛤克,才能適用volatile∫奈茫總的來說构挤,必須同時滿足下面兩個條件才能保證在并發(fā)環(huán)境的線程安全:
(1)對變量的寫操作不依賴于當(dāng)前值。如i++不符合
(2)該變量沒有包含在具有其他變量的不變式中惕鼓。
6筋现、鎖
6-1、鎖的獲取和釋放 建立的happens-before關(guān)系
6-2箱歧、鎖的釋放和獲取的內(nèi)存語義
MM會把該線程對應(yīng)的本地內(nèi)存置為無效矾飞。從而使得被監(jiān)視器保護(hù)的臨界區(qū)代碼必須從主內(nèi)存中讀取共享變量。鎖釋放與volatile寫有相同的內(nèi)存語義叫胁;鎖獲取與volatile讀有相同的內(nèi)存語義凰慈。
線程A釋放一個鎖汞幢,實質(zhì)上是線程A向接下來將要獲取這個鎖的某個線程發(fā)出了(線程A對共享變量所做修改的)消息驼鹅。
線程B獲取一個鎖,實質(zhì)上是線程B接收了之前某個線程發(fā)出的(在釋放這個鎖之前對共享變量所做修改的)消息森篷。
線程A釋放鎖输钩,隨后線程B獲取這個鎖,這個過程實質(zhì)上是線程A通過主內(nèi)存向線程B發(fā)送消息仲智。
7买乃、java concurrent包的通用化的實現(xiàn)模式
分析concurrent包的源代碼實現(xiàn),會發(fā)現(xiàn)一個通用化的實現(xiàn)模式钓辆。
首先剪验,聲明共享變量為volatile。
然后前联,使用CAS的原子條件更新來實現(xiàn)線程之間的同步功戚。
同時,配合以volatile的讀/寫和CAS所具有的volatile讀和寫的內(nèi)存語義來實現(xiàn)線程之間的通信似嗤。
7啸臀、final
8、雙重檢查和延遲優(yōu)化
上面代碼表面上看起來烁落,似乎兩全其美:在多個線程試圖在同一時間創(chuàng)建對象時乘粒,會通過加鎖來保證只有一個線程能創(chuàng)建對象豌注。在對象創(chuàng)建好之后,執(zhí)行g(shù)etInstance()將不需要獲取鎖灯萍,直接返回已創(chuàng)建好的對象轧铁。
雙重檢查鎖定看起來似乎很完美,但這是一個錯誤的優(yōu)化旦棉!在線程執(zhí)行到第4行代碼讀取到instance不為null時属桦,instance引用的對象有可能還沒有完成初始化。
問題的根源:
前面的雙重檢查鎖定示例代碼的第7行(instance = new Singleton();)創(chuàng)建一個對象他爸。這一行代碼可以分解為如下的三行偽代碼:
memory = allocate();? //1:分配對象的內(nèi)存空間
ctorInstance(memory);? //2:初始化對象
instance = memory;? ? //3:設(shè)置instance指向剛分配的內(nèi)存地址
上面三行偽代碼中的2和3之間聂宾,可能會被重排序(在一些JIT編譯器上,這種重排序是真實發(fā)生的诊笤,詳情見參考文獻(xiàn)1的“Out-of-order writes”部分)系谐。2和3之間重排序之后的執(zhí)行時序如下:
memory = allocate();? //1:分配對象的內(nèi)存空間
instance = memory;? ? //3:設(shè)置instance指向剛分配的內(nèi)存地址
//注意,此時對象還沒有被初始化讨跟!
ctorInstance(memory);? //2:初始化對象
在知曉了問題發(fā)生的根源之后纪他,我們可以想出兩個辦法來實現(xiàn)線程安全的延遲初始化。
1)不允許2和3重排序晾匠。 ? ? ? ? 2)允許2和3重排序茶袒,但不允許其他線程“看到”這個重排序。
當(dāng)聲明對象的引用為volatile后凉馆,“問題的根源”的三行偽代碼中的2和3之間的重排序薪寓,在多線程環(huán)境中將會被禁止。(注意澜共,這個解決方案需要JDK5或更高版本向叉,因為從JDK5開始使用新的JSR-133內(nèi)存模型規(guī)范,這個規(guī)范增強(qiáng)了volatile的語義嗦董。)
另外一種方式:通過內(nèi)部類的加載來實現(xiàn)