




版權(quán)說明:本文檔由用戶提供并上傳,收益歸屬內(nèi)容提供方,若內(nèi)容存在侵權(quán),請進(jìn)行舉報(bào)或認(rèn)領(lǐng)
文檔簡介
Java技術(shù)系Java并發(fā)編程的方騰飛ISBN:978-7-111-50824-本書紙版由機(jī)械工業(yè)于2015年,由華章分社(華章圖文信息,奧維博世)全球范圍內(nèi)制作與。:+86-10-68995265信箱::騰訊華章( 前為什么要寫這本記得第一次寫并發(fā)編程的文章時(shí)還是在2012年,當(dāng)時(shí)花了幾個(gè)星期的時(shí)間寫了一篇文的心態(tài)投向了InfoQ,慶幸的是后得到InfoQ主編采納的回復(fù),高興之情無以言表。這也是我第一次在專業(yè)上文章,而后在InfoQ編輯的不斷鼓勵和支持下,我陸續(xù)在川兄找到我,問有沒有寫一本書,當(dāng)時(shí)覺得自己資歷尚淺,婉言了。后來和福川兄一術(shù)點(diǎn)非常多且深,所以陸續(xù)又邀請了同事和朋友一起參與到本書的編寫當(dāng)中。第5章由編寫,其他7騰飛編寫。本書本書結(jié)合JDK的源碼介紹了Java并發(fā)框架、線程池的實(shí)現(xiàn)原理,幫助讀者做到知其所然本書結(jié)合線上應(yīng)用,給出了一些并發(fā)編程實(shí)戰(zhàn)技巧,以及線上處理并發(fā)問題的步驟和路讀者對·Java開發(fā)工程·架構(gòu)·并發(fā)編程·開設(shè)相關(guān)課程的大專院校師如何閱讀本果你是一名并發(fā)編程初學(xué)者,建議按照順序閱讀本書,并按照例子進(jìn)行編碼和實(shí)戰(zhàn)。如第3章介紹深入介紹了Java的內(nèi)存模型。Java線程之間的通信對程序員完全透明,內(nèi)見性問題很容易困擾Java程序員,本章試圖揭開Java內(nèi)存模型的神秘面紗第4章從介紹多線程技術(shù)帶來的好處開始,講述了如何啟動和終止線程以及線程的狀態(tài)詳細(xì)闡述了多線程之間進(jìn)行通信的基本方式和等待/通知經(jīng)典范第5章介紹Java并發(fā)包中與鎖相關(guān)的API和組件,以及這些API和組件的使用方式與實(shí)現(xiàn)節(jié)第7章介紹了Java中的原子操作類,并給出一些實(shí)例第10章介紹了Executor框架的整體結(jié)構(gòu)和成員組件第11章介紹幾個(gè)并發(fā)編程的實(shí)戰(zhàn),以及排查并發(fā)編程造成問題勘誤和支由于筆者的水平有限,編寫時(shí)間倉促,書中難免會出現(xiàn)一些錯誤或者確的地方,懇請讀者批評指正。為此,特意創(chuàng)建一個(gè)支持與應(yīng)急方案的站點(diǎn) 將錯誤發(fā)布在勘誤表頁面中,同時(shí)如果你遇到任何問題,也可以Q&A頁面,我將盡量上為讀者提供最滿意的解答。全部源文件除可以從華章[1]外,還可以從并發(fā)編程[2],我也會將相應(yīng)的功能更新及時(shí)發(fā)布出來。如果你有的寶貴意見,也 致感謝機(jī)械工業(yè)華章公司的、、,在這一年多的時(shí)間中始終支寫作,的鼓勵和幫助引導(dǎo)我順利完成全部書稿感謝方正電子的,是他帶我進(jìn)入了面象的世界最后感謝爸媽、岳父母和老婆,感謝的支持,并時(shí)時(shí)刻刻為我灌輸信心和力量望能生活!方騰參見華章?!庉嫛!幷叩?章并發(fā)編程的并發(fā)編程的目的是為了讓程序運(yùn)行得更快,但是,并不是啟動的線程就能讓程序最非常多的,比如上下文切換的問題、死鎖的問題,以及受限于硬件和的資源限制問題,本章會介紹幾種并發(fā)編程的以及解決方案。上下文切即使是單核處理器也支持多線行代碼,CPU通過給每個(gè)線程分配CPU時(shí)間片來實(shí)現(xiàn)換線行,讓感覺多個(gè)線程是同時(shí)執(zhí)行的,時(shí)間片一般是幾十毫秒(ms)。多線程一定快publicclassConcurrencyTestprivatestaticfinallongcount=publicstaticvoidmain(String[]args)throws{concurrency();}privatestaticvoidconcurrency()throws{longstart=System.currentTimeMillis();Threadthread=newThread(newRunnable(){publicvoidrun()inta=for(longi=0;i<count;{a+=}}intb=0;for(longi=0;i<count;{b--}longtime=System.currentTimeMillis()-start;System.out.println("concurrency:"+}privatestaticvoidserial()longstart=System.currentTimeMillis();inta=0;for(longi=0;i<count;{a+=}intb=for(longi=0;i<count;{b--}longtime=System.currentTimeMillis()-start;System.out.println("serial:"+time+"ms,b="+b+",a="+a);}}上述問題的答案是“不一定”,如表1-1所示 測試上下文切換次數(shù)和下面來看看有什么工具可以度量上下文切換帶來的消耗·使用Lmbench3[1]可以測量上下文切換的時(shí)長·使用vmstat可以測量上下文切換的次數(shù)下面是利用vmstat測量上下文切換次數(shù)的示$vmstat1
r cache csussyidwa0 0127876398928 2009900 0127868398928 05951171019900 0127868398928 059011801010000 0127868398928 0567113501990[1]Lmbench3是一個(gè)性能分析工如何減少上下文切減少上下文切換的方法有無鎖并發(fā)編程、CAS算法、使用最少線程和使用協(xié)程·CAS算法。Java的Atomic包使用CAS算法來更新數(shù)據(jù),而不需要加鎖樣會造成大量線處于等待狀態(tài)?!f(xié)單線實(shí)現(xiàn)多任務(wù)的調(diào)度,并在單線維持多個(gè)任務(wù)間的切換減少上下文切換實(shí)本節(jié)將通過減少線上大量WAITING的線程,來減少上下文切換次數(shù)第一步:用jstack命令dump線程信息,看看pid為3117的進(jìn)的線在做什么sudo-uadmin/opt/ifeve/java/bin/jstack31177>[tengfei.fangtf@ifeve~]$grepjava.lang.Thread.Statedump17|awk'{print|sort|uniq-392163053程基本全是JBOSS的工作線await。說明JBOSS線程池里線程接收到的任務(wù)太少,大量線"http--7001-97"daemonprio=10tid=0x000000004f6a8000nid=0x555einObject.wait()[0x0000000052423000]java.lang.Thread.State:WAITING(onobjectmonitor)atjava.lang.Object.wait(NativeMethod)waitingon<0x00000007969b2280>(.AprEndpoint$Worker)atjava.lang.Object.wait(Object.java:485)atlocked<0x00000007969b2280>(.AprEndpoint$Worker)at.AprEndpoint$Worker.run(AprEndpoint.java:1489)at第四步:減少JBOSS的工作線程數(shù),找到JBOSS的線程池配置信息,將maxThreads降100 emptySessionPath="false"minSpareThreads="40"maxSpareThreads="75"maxPostSize="512000"enableLookups="false"redirectPort="8443"acceptCount="200"bufferSize="16384"connectionTimeout="15000"disableUploadTimeout="false"useBodyEncodingForURI="true">[tengfei.fangtf@ifeve~]$grepjava.lang.Thread.Statedump17|awk'{print|sort|uniq-442291301死用。讓先來看一段代碼,這段代碼會引起死鎖,使線程t1和線程t2互相等待對方鎖。publicclass{privatstaticStringA=privatestaticStringB=publicstaticvoidmain(String[]{new}privatevoiddeadLock()Threadt1=newThread(new{@Overridepublicvoidrun(){synchronized(A)try{}catch(InterruptedException{}synchronized(B)}}}Threadt2=newThread(new{@Overridepublicvoidrun(){synchronized(B)synchronized(A)}}}}}這段代碼只是演示死鎖的場景,在現(xiàn)實(shí)中你可能不會寫出這樣的代碼。但是,在一些更復(fù)雜的場景中,你可能會遇到這樣的問題,比如t1拿到鎖之后,因?yàn)橐恍┊惓G闆r沒有(死循環(huán))。又或者是t1拿到一個(gè)數(shù)據(jù)庫鎖,鎖的時(shí)候拋出了異常,沒掉"Thread-2"prio=5tid=7fc0458d1000nid=0x116c1c000waitingformonitorentry[116c1b00java.lang.Thread.State:BLOCKED(onobjectmonitor)atwaitingtolock<7fb2f3ec0>(alocked<7fb2f3ef8>(ajava.lang.String)atjava.lang.Thread.run(Thread.java:695)"Thread-1"prio=5tid=7fc0430f6800nid=0x116b19000waitingformonitorentry[116b1800java.lang.Thread.State:BLOCKED(onobjectmonitor)atwaitingtolock<7fb2f3ef8>(alocked<7fb2f3ec0>(ajava.lang.String)atjava.lang.Thread.run(Thread.java:695)現(xiàn)在介紹避免死鎖的幾個(gè)常見方法·避免一個(gè)線程同時(shí)獲取多個(gè)鎖·避免一個(gè)線鎖內(nèi)同時(shí)占用多個(gè)資源,盡量保證每個(gè)鎖只占用一個(gè)資源·嘗試使用定時(shí)鎖,使用lock.tryLock(timeout)來替代使用鎖機(jī)制·對于數(shù)據(jù)庫鎖,加鎖和必須在一個(gè)數(shù)據(jù)庫連接里,否則會出現(xiàn)失敗的情況資源限制的(1)資源限資源限制是指在進(jìn)行并發(fā)編程時(shí),程序的執(zhí)行速度受限于計(jì)算機(jī)硬件資源或資源。源,速度不會變成10Mb/s,所以在進(jìn)行并發(fā)編程時(shí),要考慮這些資源的限制。硬件資源限制有帶寬的上傳/速度、硬盤讀寫速度和CPU的處理速度。資源限制有數(shù)據(jù)庫的連接資源限制的問序使用多線辦公網(wǎng)并發(fā)載和處理數(shù)據(jù)時(shí),導(dǎo)致CPU利用率達(dá)到100%,幾個(gè)小時(shí)都不如何解決資源限制的問的數(shù)據(jù)??梢酝ㄟ^“數(shù)據(jù)ID%機(jī)器數(shù)”,計(jì)算得到一個(gè)機(jī)器,然后由對應(yīng)的機(jī)器處理這連接復(fù)用,或者在調(diào)用對方webservice接口獲取數(shù)據(jù)時(shí),只建立接在資源限制情況下進(jìn)行并發(fā)編程序的并發(fā)度,比如文件程序依賴于兩個(gè)資源——帶寬和硬盤讀寫速度。有數(shù)據(jù)庫操作本章小問題,因?yàn)檫@些類都已經(jīng)通過了充分的測試和優(yōu)化,均可解決了本章提到的幾個(gè)。第2章Java并發(fā)機(jī)制的底層實(shí)現(xiàn)CPU的指令。本章深入底層一起探索下Java并發(fā)機(jī)制的底層實(shí)現(xiàn)原理。volatile的應(yīng)線發(fā)編scroiedvolailevolaile輕量級的scroied處理器開發(fā)證變“見”見線改一個(gè)共享變量時(shí),另外一個(gè)線程能讀到這個(gè)修改的值。如果volaile變量修飾符使用恰當(dāng)?shù)脑?,scroied執(zhí)為線換調(diào)層處實(shí)現(xiàn)volaile過深入分析幫助volaile變先從了解volatile的定義開始volatile的定義與實(shí)現(xiàn)原Jaa語規(guī)3對volaile定義:Java編語許線程變?yōu)樽兙€應(yīng)該過鎖單獲這變Java語volaile鎖更個(gè)被volaileJava線線這變值表2-1CPU的術(shù)語定volatile是如何來保證可見性的呢?讓在X86處理器下通過工具獲取JIT編譯器生成匯編指令來查看對volatile進(jìn)行寫操作時(shí),CPU會做什Java代碼如下instance=new //instance是volatile變轉(zhuǎn)變成匯編代碼,如下0x01a3de1d:movb$0×0,0×1104800(%esi);0x01a3de24:lockaddl構(gòu)開發(fā)者手冊可知,Lock前綴的指令在多核處理器下會了兩件事情[1]。將當(dāng)前處理器緩存行的數(shù)據(jù)寫回到系統(tǒng)內(nèi)存這個(gè)寫回內(nèi)存的操作會使在其他CPU里緩存了該內(nèi)存地址的數(shù)為了提高處理速度,處理器不直接和內(nèi)存進(jìn)行通信,而是先將系統(tǒng)內(nèi)存的數(shù)據(jù)讀到緩(12或其他)后進(jìn)時(shí)寫對了volaile的變進(jìn)J處發(fā)oc綴這變緩統(tǒng)緩值還執(zhí)行計(jì)問題處為證處緩實(shí)現(xiàn)緩協(xié)議處理器通過總線上來檢查緩值過處緩設(shè)下面來具體講解volatile的兩條實(shí)現(xiàn)原則 的內(nèi)存區(qū)域已經(jīng)緩存在處理器,則不會聲言LOCK#信號。相反,它會鎖定這塊內(nèi)存區(qū)定”,緩存一致性機(jī)制會同時(shí)修改由兩個(gè)以上處理器緩存的內(nèi)存區(qū)域數(shù)據(jù)。 64處理器能其他處理器系統(tǒng)內(nèi)存和它們的緩存。處理器使用技術(shù)保證它的緩存、系統(tǒng)內(nèi)存和其他處理器的器來檢測其他處理器打算寫內(nèi)存地址,而這個(gè)地址當(dāng)前處于共享狀態(tài),那么正在的處理器將使它的緩存行無效,在下次相同內(nèi)存地址時(shí),強(qiáng)制執(zhí)行緩存行填充。volatile的使用優(yōu)著名的Java并發(fā)編程大師Douglea在JDK7的并發(fā)包里新增一個(gè)隊(duì)列集合類Linked-/**隊(duì)列中的頭部節(jié)點(diǎn)privatetransientf?inalPaddedAtomicReference<QNode>/**隊(duì)列中的尾部節(jié)點(diǎn)privatetransientf?inalPaddedAtomicReference<QNode>staticf?inalclassPaddedAtomicReference<T>extendsAtomicReferenceT>//使用很多4個(gè)字節(jié)的追加到64個(gè)字Objectp0,p1,p2,p3,p4,p5,p6,p7,p8,p9,pa,pb,pc,pd,pe;PaddedAtomicReference(Tr){}}publicclassAtomicReference<V>implements{privatevolatileV//省略其他代}頭節(jié)點(diǎn)(head)和尾節(jié)點(diǎn)(tail),而這個(gè)類PaddedAtomicReference相對于父類象的占4個(gè)字節(jié),它追加了15個(gè)變量(共占60個(gè)字節(jié)),再加上父類的value變量,一共64個(gè)其他處理器不能自己高速緩存中的尾節(jié)點(diǎn),而隊(duì)列的入隊(duì)和出隊(duì)操作則需要不停修改頭不過這種追加字節(jié)的方式在Java7下可能不生效,因?yàn)镴ava7變得更加智慧,它會淘汰或因?yàn)樗鼤i住總線,導(dǎo)致其他CPU不能總線,不能總線就意味著不能系統(tǒng)內(nèi)synchronized的實(shí)現(xiàn)原理與應(yīng)詳細(xì)介紹JavaSE1.6中為了減少獲得鎖和鎖帶來的性能消耗而引入的偏向鎖和輕量級鎖,以及鎖的結(jié)構(gòu)和升級過程?!τ谄胀ㄍ椒椒ǎi是當(dāng)前實(shí)例對象·對于靜態(tài)同步方法,鎖是當(dāng)前類的Class對·對于同步方法塊,鎖是Synchonized括號里配置的對那么鎖到底存在哪里呢?鎖里面會什么信息呢?束處和異常處,JVM要保證每個(gè)monitorenter必須有對應(yīng)的monitorexit與之配對。任何對指令時(shí),將會嘗試獲取對象所對應(yīng)的monitor的所,即嘗試獲得對象的鎖。Java對象synchronized用的鎖是存在Java對象頭里的。如果對象是數(shù)組類型,則虛擬機(jī)用3個(gè)字表2-2Java對象頭的長 Word里默認(rèn)對象的HashCode、分代和鎖標(biāo)記位。32位JVM Word的默認(rèn)結(jié)構(gòu)如表2-3所示。表2-3Java對象頭的結(jié)表2-4MarkWord的狀態(tài)變在64位虛擬機(jī)下 Word是64bit大小的,其結(jié)構(gòu)如表2-5所示表2-5MarkWord的結(jié)鎖的升級與對JavaSE1.6為了減少獲得鎖和鎖帶來的性能消耗,引入了“偏向鎖”和“輕量級鎖”,在JavaSE1.6中,鎖一共有4種狀態(tài),級別從低到高依次是:無鎖狀態(tài)、偏向鎖狀態(tài)、輕量級鎖狀獲得鎖和鎖的效率,下文會詳細(xì)分析。偏向opo[]經(jīng)過發(fā)現(xiàn)鎖僅線競總線獲為讓線獲鎖向鎖線程步塊獲鎖時(shí)對象頭棧幀鎖記錄里鎖的線該線進(jìn)塊時(shí)進(jìn)行鎖和簡單地測試對頭arkord否線鎖測試線經(jīng)獲鎖測試敗則測試arkord鎖標(biāo)識設(shè)1(鎖):設(shè)競鎖;如果設(shè)則嘗試對頭鎖線偏向鎖的撤偏向鎖使用了一種等到競爭出現(xiàn)才鎖的機(jī)制,所以當(dāng)其他線程嘗試競爭偏向鎖時(shí),圖2-1偏向鎖關(guān)閉偏向輕量輕量級鎖加線執(zhí)行同步塊之前,JVM會先在當(dāng)前線程的中創(chuàng)建用于鎖記錄的空間,并輕量級鎖輕量級時(shí),會使用原子的CAS操作將DisplacedMarkWord替換回到對象頭,如果成圖2-2爭奪鎖導(dǎo)致的鎖膨脹流程為為(獲鎖線)鎖級級鎖輕級鎖態(tài)鎖處于這態(tài)線試圖獲鎖時(shí),都塞持鎖線程鎖喚這線喚線進(jìn)一輪奪鎖鎖的優(yōu)缺點(diǎn)對表2-6是鎖的優(yōu)缺點(diǎn)的對表2-6鎖的優(yōu)缺點(diǎn)的對 本節(jié)一些內(nèi)容參考了HotSpot源碼、對象頭源碼markOop.hpp、偏向鎖源biasedLocking.cpp,以及其他源碼ObjectMonitor.cpp和BasicLock.cpp原子操作的實(shí)現(xiàn)原子(atomic)本意是“不能被進(jìn)一步分割的最小粒子”,而原子操作(atomicoperation)意一起來聊一聊在In處理器和Java里是如何實(shí)現(xiàn)原子操作的。術(shù)語定在了解原子操作的實(shí)現(xiàn)原理前,先要了解一下相關(guān)的術(shù)語,如表2-7所示表2-7CPU術(shù)語定處理器如何實(shí)現(xiàn)原子操32位-32處理器使用基于對緩存加鎖或總線加鎖的方式來實(shí)現(xiàn)多處理器之間的原子操作處理器會自動證本操子處理器證統(tǒng)中節(jié)原意一處器節(jié)時(shí)處理器不能這節(jié)存地eim6和的處動證單處器對緩進(jìn)16/32/6作是雜處動保證總線寬使用總線鎖保證原子第一個(gè)機(jī)制是通過總線鎖保證原子性。如果多個(gè)處理器同時(shí)對共享變量進(jìn)行讀改寫操兩次i++操作,期望的結(jié)果是3,但是有可能結(jié)果是2,如圖2-3所示。圖2-3結(jié)果對使用緩存鎖保證原子第二個(gè)機(jī)制是通過緩存鎖定來保證原子性。在同一時(shí)刻,只需保證對某個(gè)內(nèi)存地址處理器緩存中進(jìn)行,并不需要總線鎖,在Pentium6和目前的處理器中可以使用“緩存言LOCK#信號,而是修改的內(nèi)存地址,并允許它的緩存一致性機(jī)制來保證操作的原子性,因?yàn)榫彺嬉恢滦詸C(jī)制會同時(shí)修改由兩個(gè)以上處理器緩存的內(nèi)存區(qū)域數(shù)據(jù),當(dāng)其他處但是有兩種情況下處理器不會使用緩存鎖定第一種情況是:當(dāng)操作的數(shù)據(jù)不能被緩存在處理器,或操作的數(shù)據(jù)跨多個(gè)緩存 line)時(shí),則處理器會調(diào)用總線鎖定第二種情況是:有些處理器不支持緩存鎖定。對于In486和Pentium處理器,就算鎖定的針對以上兩個(gè)機(jī)制,通過In處理器提供了很多Lock前綴的指令來實(shí)現(xiàn)。例如,位測令(如ADD、OR)等,被這些指令操作的內(nèi)存區(qū)域就會加鎖,導(dǎo)致其他處理器不能同時(shí)它。Java如何實(shí)現(xiàn)原子在Java中可以通過鎖和循環(huán)CAS的方式來實(shí)現(xiàn)原子操使用循環(huán)CAS實(shí)現(xiàn)原子操privateAtomicIntegeratomicI=newAtomicInteger(0);privateinti=0;publicstaticvoidmain(String[]{finalCountercas=newCounter();List<Thread>ts=newArrayList<Thread>(600);longstart=System.currentTimeMillis();for(intj=0;j<100;j++)Threadt=newThread(new{publicvoidrun()for(inti=0;i<10000;{cas.count();}}}for(Threadt:{}//等待所有線行完for(Threadt:{try}catch(InterruptedException{}}System.out.println(System.currentTimeMillis()-start);} *使用CAS實(shí)現(xiàn)線程安全計(jì)數(shù)器 privatevoidsafeCount(){for(;;)inti=booleansuc= pareAndSet(i,++i);if(suc){}}
}*非線程安全計(jì)數(shù)privatevoid{}}CAS實(shí)現(xiàn)原子操作的三大類的compareAndSet方法的作用是首先檢查當(dāng)前是否等于預(yù)期,并且檢查當(dāng)前標(biāo)志是否等于預(yù)期標(biāo)志,如果全部相等,則以原子方式將該和該標(biāo)志的值設(shè)置為給定的更新值。publicboolean //預(yù)期 //更新后的 //預(yù)期標(biāo) //更新后的標(biāo))循環(huán)時(shí)間長開銷大。自旋CAS如果長時(shí)間不成功,會給CPU帶來非常大的執(zhí)行開銷。J處pasepase遲線執(zhí)(de-pipelie)過執(zhí)資遲時(shí)間實(shí)現(xiàn)處遲時(shí)間環(huán)時(shí)順序(eoryrderiolaio)而引線(Uipeliels)U執(zhí)證變對一個(gè)共享變執(zhí)時(shí),循環(huán)證對變時(shí)環(huán)證這個(gè)時(shí)候就可以用鎖。還有一個(gè)取巧的辦法,就是把多個(gè)共享變量合并成一個(gè)共享變量來操作。比變i2jaij2aijJava1.,J提供了AtomicReference類來保證對象之間的原子性,就可以把多個(gè)變量放在一個(gè)對象里來行CAS操作使用鎖機(jī)制實(shí)現(xiàn)本章小本章一起研究了volatile、synchronized和原子操作的實(shí)現(xiàn)原理。Java中的大部分容器第3章JavaJava內(nèi)存模型的基并發(fā)編程模型的兩個(gè)關(guān)鍵問發(fā)編處理兩個(gè)關(guān)鍵問題:線間線間(這線發(fā)執(zhí)動實(shí))線間換編線間傳遞。同步是指程序中用于控制不同線程間操作發(fā)生相對順序的機(jī)制。在共享內(nèi)存并發(fā)Java內(nèi)存模型的抽象結(jié)在Java中,所有實(shí)例域、靜態(tài)域和數(shù)組元素都在堆內(nèi)存中,堆內(nèi)存程之間共法定義參數(shù)(Java語言規(guī)范稱之為FormalMethodParameters)和異常處理器參數(shù)(ExceptionHandlerParameters)不會程之間共享,它們不會有內(nèi)存可見性問題,也不受內(nèi)存模型的影Java線程之間的通信由Java內(nèi)存模型(本文簡稱為JMM)控制,JMM決定一個(gè)線共享內(nèi)存(LocalMemory),本地內(nèi)存中了該線程以讀/寫共享變量的副本。本地內(nèi)存是JMM的圖3-1Java內(nèi)存模型的抽象結(jié)構(gòu)示意從圖3-1來看,如果線程A與線程B之間要通信的話,必須要經(jīng)歷下面2個(gè)步驟線程A把本地內(nèi)存A中更新過的共享變量刷新到主內(nèi)線程B到主內(nèi)存中去線程A之前已更新過的共享變量圖3-2線程之間的通信如圖3-2所示,本地內(nèi)存A和本地內(nèi)存B由主內(nèi)存享變量x的副本。假設(shè)初始時(shí),這3個(gè)存中,此時(shí)主內(nèi)存中的x值變?yōu)榱?。隨后,線程B到主內(nèi)存中去線程A更新后的x值,此時(shí)從源代碼到指令序列的重排在執(zhí)行程序時(shí),為了提高性能,編譯器和處理器常常會對指令做重排序。重排序分3種型從Java源代碼到最終實(shí)際執(zhí)行的指令序列,會分別經(jīng)歷下面3種重排序,如圖3-3所示圖3-3從源碼到最終執(zhí)行的指令序列的示意出現(xiàn)內(nèi)存可見性問題。對于編譯器,JMM的編譯器重排序規(guī)則會特定類型的編譯器重排序(不是所有的編譯器重排序都要)。對于處理器重排序,JMM的處理器重排序規(guī)則會要求Java編譯器在生成指令序列時(shí),特定類型的內(nèi)存屏障(MemoryBarriers,In MemoryFence)指令,通過內(nèi)存屏障指令來特定類型的處理器重排序JMM屬于語言級的內(nèi)存模型,它確保在不同的編譯器和不同的處理器平臺之上,通過止特定類型的編譯器重排序和處理器重排序,為程序員提供一致的內(nèi)存可見性保證并發(fā)編程模型的分表3-1處理器操作內(nèi)存的執(zhí)行結(jié)圖3-4處現(xiàn)代的處理器都會允寫-讀操作進(jìn)行重排序。表3-2處理器的重排序注·sparc-TSO是指以 Order)內(nèi)存模型運(yùn)行時(shí)sparc處理器的特性·表3-2中的X86包括X64及AMD64·由于ARM處理器的內(nèi)存模型與PowerPC處理器的內(nèi)存模型非常類似,本文將忽略它·數(shù)據(jù)依賴性后文會專門說明表3-3內(nèi)存屏障類貴,因?yàn)楫?dāng)前處理器通常要把寫緩沖區(qū)中的數(shù)據(jù)全部刷新到內(nèi)存中(BufferFullyFlush)。happens-before簡與程序員密切相關(guān)的happens-before規(guī)則如下·程序順序規(guī)則:一個(gè)線程中的每個(gè)操作,happens-before于該線程中的任意后續(xù)操作·監(jiān)視器鎖規(guī)則:對一個(gè)鎖的,happens-before于隨后對這個(gè)鎖的加鎖·volatile變量規(guī)則:對一個(gè)volatile域的寫,happens-before于任意后續(xù)對這個(gè)volatile域讀·傳遞性:如果Ahappens-beforeB,且Bhappens-beforeC,那么Ahappens-beforeC注意兩個(gè)操作之間具有happens-before關(guān)系,并不意味著前一個(gè)操作必須要在后一個(gè) isvisibletoandorderedbeforethesecond)。happens-before與JMM的關(guān)系如圖3-5所示重排重排序是指編譯器和處理器為了優(yōu)化程序性能而對指令序列進(jìn)行重新排序的一種數(shù)據(jù)依賴表3-4數(shù)據(jù)依賴類上面3種情況,只要重排序兩個(gè)操作的執(zhí)行順序,程序的執(zhí)行結(jié)果就會被改變as-if-serialdoublepi= //doubler =1.0; //Bdoublearea=pi*r* //上面3個(gè)操作的數(shù)據(jù)依賴關(guān)系如圖3-6所示圖3-63個(gè)操作之間的依賴最終執(zhí)行的指令序列中,C不能被重排序到A和B的前面(C排到A和B的前面,程序的結(jié)果將被改變)。但A和B之間沒有數(shù)據(jù)依賴關(guān)系,編譯器和處理器可以重排序A和B之間的執(zhí)行順序圖3-7程序的兩種執(zhí)行順程序順序1)Ahappens-beforeB。2)Bhappens-beforeC。3)Ahappens-beforeC。這里的第3個(gè)happens-before關(guān)系,是根據(jù)happens-before的傳遞導(dǎo)出來的這里Ahappens-beforeB,但實(shí)際執(zhí)行時(shí)B卻可以排在A之前執(zhí)行(看上面的重排序后的執(zhí)按happens-before順序執(zhí)行的結(jié)果一致。在這種情況下,JMM會認(rèn)為這種重排序并不(not盡可能提高并行度。編譯器和處理器遵從這一目標(biāo),從happens-before的定義可以看出,重排序?qū)Χ嗑€程的影現(xiàn)在讓來看看,重排序是否會改變多線程程序的執(zhí)行結(jié)果。請看下面的示例代碼classReorderExampleinta=booleanflag=false;publicvoidwriter(){a= //flag= //}Publicvoidreader()if(f?lag){ //3inti=a* //}}}答案是:不一定操作3和操作4沒有數(shù)據(jù)依賴關(guān)系,編譯器和處理器也可以對這兩個(gè)操作重排序。讓先來圖3-8程序執(zhí)行時(shí)序注 本 用虛箭線標(biāo)識錯誤的讀操作,用實(shí)箭線標(biāo)識正確的讀操作圖3-9程序的執(zhí)行時(shí)序計(jì)算結(jié)果臨時(shí)保存到一個(gè)名為重排序緩沖(ReorderBuffer,ROB)的硬件緩存中。當(dāng)操作3的條順序一順序一致性內(nèi)存模型是一個(gè)理論參考模型,在設(shè)計(jì)的時(shí)候,處理器的內(nèi)存模型和編程言的內(nèi)存模型都會以順序一致性內(nèi)存模型作為參照數(shù)據(jù)競爭與順序一致當(dāng)程序未正確同步時(shí),就可能會存在數(shù)據(jù)競爭。Java內(nèi)存模型規(guī)范對數(shù)據(jù)競爭的定義下而且寫和讀沒有通過同步JMM對正確同步的多線程程序的內(nèi)存一致性做了如下保證序的執(zhí)行結(jié)果與該程序在順序一致性內(nèi)存模型中的執(zhí)行結(jié)果相同。馬上就會看到,這對(synchronized、volatile和final)的正順序一致性內(nèi)存模一個(gè)線程中的所有操作必須按照程序的順序來執(zhí)行順序一致性內(nèi)存模型為程序員提供的視圖如圖3-10所示圖3-10順序一致性內(nèi)存模型的順單這過擺動連線時(shí)線須順執(zhí)讀/時(shí)間線連線程并發(fā)執(zhí)時(shí),圖線讀/(順間)。為了更好進(jìn)行理解,下面通過兩個(gè)示意圖來對順序一致性模型的特性做進(jìn)一步的說明假設(shè)有兩個(gè)線程A和B并發(fā)執(zhí)行。其中A線程有3個(gè)操作,它們在程序中的順A1→A2→A3。B線程也有3個(gè)操作,它們在程序中的順序是:B1→B2→B3假設(shè)這兩個(gè)線程使用監(jiān)視器鎖來正確同步:A線程的3個(gè)操作執(zhí)行后監(jiān)視器鎖,隨后B線程獲取同一個(gè)監(jiān)視器鎖順序一致性模型中的執(zhí)行效果將如圖311所示。圖3-11順序一致性模型的一種執(zhí)現(xiàn)在再假設(shè)這兩個(gè)線程沒有做同步,下面是這個(gè)未同步程序在順序一致性模型中執(zhí)行示意圖,如圖3-12所示圖3-12順序一致性模型中的另一種執(zhí)J這證J執(zhí)順線作執(zhí)順線過緩地內(nèi)這僅對線見線觀前線線過刷主內(nèi)這對線見這線線執(zhí)順同步程序的順序一致性效請看下面的示例代碼class{inta=booleanflag=publicsynchronizedvoidwriter //獲取a=flag= //publicsynchronizedvoidreader //獲取if(flag)inti= //}}在上面示例代碼中,假設(shè)A線行writer()方法后,B線行reader()方法。這是一個(gè)正順序一致性模型中,所有操作完全按程序的順序串行執(zhí)行。而在JMM中,臨界區(qū)內(nèi)的代可以重排序(但JMM不允許臨界區(qū)內(nèi)的代碼“逸出”到臨界區(qū)之外,那樣會破壞監(jiān)視器的圖3-13兩個(gè)內(nèi)存模型中的執(zhí)行時(shí)序?qū)Ρ刃薪Y(jié)果的前提下,盡可能地為編譯器和處理器的優(yōu)化打開方便。未同步程序的執(zhí)對于未同步或未正確同步的多線程程序,JMM只提供最小安全性:線行時(shí)到的到的值不會無中生有(OutOfThinAir)的冒出來。為了實(shí)現(xiàn)最小安全性,JVM在堆上分配對象此,在已清零的內(nèi)存空間(Pre-zeroedMemory)分配對象時(shí),域的默認(rèn)初始化已經(jīng)完成了。如果想要保證執(zhí)行結(jié)果一致,JMM需要大量的處理器和編譯器的優(yōu)化,這對程序的執(zhí)行JMM不保證對64位的long型和double型變量的寫操作具有原子性,而順序一致性證對所有的內(nèi)存讀/寫操作都具有原子性驟稱之為總線事務(wù)(BusTransaction)。總線事務(wù)包括讀事務(wù)(ReadTransaction)和寫事務(wù)(Write務(wù)。在一個(gè)處理器執(zhí)行總線事務(wù)期間,總線會其他的處理器和I/O設(shè)備執(zhí)行內(nèi)存的讀/寫。下面,讓通過一個(gè)示意圖來說明總線的工作機(jī)制,如圖3-14所示。圖3-14總線圖設(shè)處時(shí)總線發(fā)總線務(wù)這時(shí)總線(srbiraio)對競這設(shè)總線處競獲勝(總線處的)時(shí)處繼續(xù)的總線務(wù)處器則處總線事務(wù)執(zhí)存設(shè)處執(zhí)總線務(wù)間(不管這個(gè)總線務(wù)讀事務(wù)還務(wù))處總線發(fā)總線務(wù)時(shí)處請總線??偩€的這些工作機(jī)制可以把所有處理器對內(nèi)存的以串行化的方式來執(zhí)行。在任意時(shí)間點(diǎn),最多只能有一個(gè)處理器可以內(nèi)存。這個(gè)特性確保了單個(gè)總線事務(wù)之中的內(nèi)存讀/寫當(dāng)單個(gè)內(nèi)存操作不具有原子性時(shí),可能會產(chǎn)生意想不到。請看示意圖,如圖3-15示圖3-15總線事務(wù)執(zhí)行的時(shí)序volatile的內(nèi)存當(dāng)共享變量為volatile后,對這個(gè)變量的讀/寫將會很特別。為了揭開volatile的神秘紗,下面將介紹volatile的內(nèi)存語義及volatile內(nèi)存語義的實(shí)現(xiàn)volatile的特classVolatileFeaturesExamplevolatilelongvl=0L; //使用volatile64位的long型變量publicvoidset(longl){vl= //單個(gè)volatile變量的}publicvoidgetAndIncrement() //復(fù)合(多個(gè))volatile變量的讀/}publiclongget()return //單個(gè)volatile變量的}}假設(shè)有多個(gè)線程分別調(diào)用上面程序的3個(gè)方法,這個(gè)程序在語義上和下面程序等classVolatileFeaturesExamplelongvl= //64位的long型普通變publicsynchronizedvoidset(longl){//對單個(gè)的普通變量的寫用同一個(gè)鎖vl=}publicvoidgetAndIncrement //普通方法調(diào)longtemp= //調(diào)用已同步的讀temp+= //普通寫操 //調(diào)用已同步的寫}publicsynchronizedlongget //對單個(gè)的普通變量的讀用同一個(gè)鎖同return}}鎖的happens-before規(guī)則保證鎖和獲取鎖的兩個(gè)線程之間的內(nèi)存可見性,這意味著一個(gè)volatile變量的讀,總是能看到(任意線這個(gè)volatile變量最后的寫入簡而言之,volatile變量自身具有下·可見性。對一個(gè)volatile變量的讀,總是能看到(任意線這個(gè)volatile變量最后的入volatile寫-讀建立的happens-before關(guān)比volatile自身的特性更為重要,也更需要去關(guān)注。從JSR-133開始(即從JDK5開始),volatile變量的寫-讀可以實(shí)現(xiàn)線程之間的通信從內(nèi)存語義的角度來說,volatile的寫-讀與鎖的-獲取有相同的內(nèi)存效果:volatile寫鎖的有相同的內(nèi)存語義;volatile讀與鎖的獲取有相同的內(nèi)存語義請看下面使用volatile變量的示例代碼classVolatileExample a=volatilebooleanflag=false;publicvoidwriter(){a=1; //1flag=true; //2}publicvoid{if(flag){ //3inti=a; //4}}}假設(shè)線程A執(zhí)行writer()方法之后,線程B執(zhí)行reader()方法。根據(jù)happens-before規(guī)則,這過程建立的happens-before關(guān)系可以分為3類2)根據(jù)volatile規(guī)則,2happens-before3。3)根據(jù)happens-before的傳遞性規(guī)則,1happens-before4。圖3-16happens-before在上圖中,每一個(gè)箭頭的兩個(gè)節(jié)點(diǎn),代表了一個(gè)happens-before關(guān)系。黑色箭頭表示程注注本volatile寫-讀的內(nèi)存volatile寫的內(nèi)存語義如下當(dāng)寫一個(gè)volatile變量時(shí),JMM會把該線應(yīng)的本地內(nèi)存中的共享變量值刷新到主存圖3-17共享變量的狀態(tài)示意volatile讀的內(nèi)存語義如下內(nèi)存中共享變量。圖3-18為線程B讀同一個(gè)volatile變量后,共享變量的狀態(tài)示意圖果把volailevolaile讀兩個(gè)步驟綜的話讀線讀volaile變量線這volaile變見變值變對讀線見。下面對volatile寫和volatile讀的內(nèi)存語義做個(gè)總結(jié)·線程A寫一個(gè)volatile變量,實(shí)質(zhì)上是線程A向接下來將要讀這個(gè)volatile變量的某個(gè)線發(fā)出了(其對共享變量所做修改的)·線程B讀一個(gè)volatile變量,實(shí)質(zhì)上是線程B接收了之前某個(gè)線程發(fā)出的(在寫這個(gè)變量之前對共享變量所做修改的)消息·線程A寫一個(gè)volatile變量,隨后線程B讀這個(gè)volatile變量,這個(gè)過程實(shí)質(zhì)上是線程A通主內(nèi)存向線程B發(fā)送消息圖3-18共享變量的狀態(tài)示意volatile內(nèi)存語義的下面來看看JMM如何實(shí)現(xiàn)volatile寫/讀的內(nèi)存語義表3-5volatile重排序規(guī)則從表3-5可以看出·當(dāng)?shù)谝粋€(gè)操作是volatile寫,第二個(gè)操作是volatile讀時(shí),不能為了實(shí)現(xiàn)volatile的內(nèi)存語義,編譯器在生成字節(jié)碼時(shí),會在指令序列中內(nèi)存屏障特定類型的處理器重排序。對于編譯器來說,發(fā)現(xiàn)一個(gè)最優(yōu)布置來最小化屏障的總數(shù)幾乎不可能。為此,JMM采取保守策略。下面是基于保守策略的JMM內(nèi)存屏障策略。·在每個(gè)volatile寫操作的前面一個(gè)StoreStore屏障·在每個(gè)volatile寫操作的后面一個(gè)StoreLoad屏障·在每個(gè)volatile讀操作的后面一個(gè)LoadLoad屏障·在每個(gè)volatile讀操作的后面一個(gè)LoadStore屏障下面是保守策略下,volatile寫內(nèi)存屏障后生成的指令序列示意圖,如圖3-19所示圖3-19指令序列示意內(nèi)存讀的前面一個(gè)StoreLoad屏障。從整體執(zhí)行效率的角度考慮,JMM最終選擇了在每個(gè)volatile寫的后面一個(gè)StoreLoad屏障。因?yàn)関olatile寫-讀內(nèi)存語義的常見使用模式是:一個(gè)下面是在保守策略下,volatile讀內(nèi)存屏障后生成的指令序列示意圖,如圖3-20所示圖3-20指令序列示意圖3-20中的LoadLoad屏障用來處理器把上面的volatile讀與下面的普通讀重排序LoadStore屏障用來處理器把上面的volatile讀與下面的普通寫重排序class{intvolatileintv1=1;volatileintv2=2;voidreadAndWrite(){inti= //第一個(gè)volatileintj= //第二個(gè)volatilea=i+ //普通v1=i+ //第一個(gè)volatilev2=j* //第二個(gè)volatile} //其他}針對readAndWrite()方法,編譯器在生成字節(jié)碼時(shí)可以做如下的優(yōu)化圖3-21指令序列示意oreoad為第二個(gè)volailerer時(shí)編譯volaile讀為見編譯這里oreoad前面保守策略下的volatile讀和寫,在X86處理器平臺可以優(yōu)化成如圖3-22所示在volatile寫后面一個(gè)StoreLoad屏障即可正確實(shí)現(xiàn)volatile寫-讀的內(nèi)存語義。這意味著在圖3-22指令序列示意JSR-133為什么要增強(qiáng)volatile的內(nèi)存圖3-23線行時(shí)序限制編譯器和處理器對volatile變量與普通變量的重排序,確保volatile的寫-讀和鎖的-獲取具有相同的內(nèi)存語義。從編譯器重排序規(guī)則和處理器內(nèi)存屏障策略來看,只要volatile序規(guī)則和處理器內(nèi)存屏障策略。性能上,volatile更有優(yōu)勢。如果讀者想在程序中用volatile代替鎖,請一定謹(jǐn)慎,具體參閱BrianGoetz的文章《Java理論與實(shí)踐:正確使用Volatile變量》。鎖的內(nèi)存鎖的-獲取建立的happens-before關(guān)下面是鎖-獲取的示例代碼class{inta=publicsynchronizedvoidwriter(){ //1 //2 //publicsynchronizedvoidreader(){ //4inti=a; //5 //}假設(shè)線程A執(zhí)行writer()方法,隨后線程B執(zhí)行reader()方法。根據(jù)happens-before規(guī)則,這過程包含的happens-before關(guān)系可以分為3類根據(jù)程序次序規(guī)則,1happens-before2,2happens-before3;4happens-before5,5happens-before6。 上述happens-before關(guān)系的圖形化表現(xiàn)形式如圖3-24所示圖3-24happens-before關(guān)系5。因此,線程A在鎖之前所有可見的共享變量,程B獲取同一個(gè)鎖之后,將立刻變得對B線程可見鎖的和獲取的內(nèi)存語面的MonitorExample程序?yàn)槔?,A線程鎖后,共享數(shù)據(jù)的狀態(tài)示意圖如圖3-25所示。圖3-25共享數(shù)據(jù)的狀態(tài)示意當(dāng)線程獲取鎖時(shí)JMM會把該線應(yīng)的本地內(nèi)存置為無效從而使得被監(jiān)視器保護(hù)的臨界區(qū)代碼必須從主內(nèi)存中共享變量圖326是鎖獲取的狀態(tài)示意圖。圖3-26鎖獲取的狀態(tài)示意下面對鎖和鎖獲取的內(nèi)存語義做個(gè)總結(jié)·線程A一個(gè)鎖,實(shí)質(zhì)上是線程A向接下來將要獲取這個(gè)鎖的某個(gè)線程發(fā)出了(線程對共享變量所做修改的)消息·線程A鎖,隨后線程B獲取這個(gè)鎖,這個(gè)過程實(shí)質(zhì)上是線程A通過主內(nèi)存向線程B送消息鎖內(nèi)存語義的本文將借助ReentrantLock的源代碼,來分析鎖內(nèi)存語義的具體實(shí)現(xiàn)機(jī)制請看下面的示例代碼class{inta=ReentrantLocklock=newReentrantLock();publicvoidwriter(){ //獲取{}finallylock.unlock();//}}publicvoidreader 獲取tryinti=}finallylock.unlock();//}}}在ReentrantLock中,調(diào)用lock()方法獲取鎖;調(diào)用unlock()方法鎖 圖3-27是ReentrantLock的類圖(僅畫出與本文相關(guān)的部分)圖3-27ReentrantLock的類圖使用公平鎖時(shí),加鎖方ock()調(diào)用軌跡如下。1)FairSync:lock()QueuedSynchronizer:acquire(intarg)ReentrantLock:tryAcquire(intacquires)在第4步真正開始加鎖,下面是該方法的源代碼protectedfinalbooleantryAcquire(int{finalThreadcurrent=intc=getState(); //獲取鎖的開始,首先讀volatile變量stateif(c==0){if(is(current)&&compareAndSetState(0,acquires)){returntrue;}}elseif(current=={intnextc=c+acquires;if(nextc<0)thrownewError("umlockcountreturntrue;}return}在使用公平鎖時(shí),方法unlock()調(diào)用軌跡如下。1)ReentrantLock:unlock()。2) arg)。3)Sync:tryRelease(intreleases)。在第3步真正開始鎖,下面是該方法的源代碼protectedfinalbooleantryRelease(int{intc=getState()-if(Thread.currentThread()!=getExclusiveOwnerThread())thrownewIllegalMonitorStateException();booleanfree=false;if(c==0){free=true;} //鎖的最后,寫volatile變量statereturnfree;}從上面的源代碼可以看出,在鎖的最后寫volatile變量stateNonfairSync:lock() pareAndSetState(intexpect,intupdate)。protectedfinalbooleancompareAndSetState(intexpect,int{return pareAndSwapInt(this,stateOffset,expect,}該方法以原子操作的方式更新state變量,本文把的pareAndSet()方法調(diào)用簡稱為前文提到過,編譯器不會對volatile讀與volatile讀后面的任意內(nèi)存操作重排序;編下面 類 pareAndSwapInt()方法的源代碼publicfinalnativebooleancompareAndSwapInt(Objecto,longWindows操作系統(tǒng),X86處理器)。下面是對應(yīng)于inX86處理器的源代碼的片段。inlineexchange_value,volatile{//alternativefor intmp=os::is_MP();asmmovedx,movecx,exchange_valuemoveax,compare_valuecmpxchgdwordptr[edx],}}果程序是在單處理器上運(yùn)行,就省略lock前綴(單處理器自身會單處理器內(nèi)的順序一致性,不需要lock前綴提供的內(nèi)存屏障效果) 冊對lock前綴的說明如下帶來昂貴的開銷。從Pentium4、InXeon及P6處理器開始,In使用緩存鎖定(CacheLocking)2)該指令,與之前和之后的讀和寫指令重排序3)把寫緩沖區(qū)中的所有數(shù)據(jù)刷新到內(nèi)上面的第2點(diǎn)和第3點(diǎn)所具有的內(nèi)存屏障效果,足以同時(shí)實(shí)現(xiàn)volatile讀和volatile寫的內(nèi)語義經(jīng)過上面的分析,現(xiàn)在終于能明白為什么JDK文檔說CAS同時(shí)具有volatile讀volatile寫的內(nèi)存語義了現(xiàn)在對公平鎖和非公平鎖的內(nèi)存語義做個(gè)總結(jié)·公平鎖和非公平鎖時(shí),最后都要寫一個(gè)volatile變量state·公平鎖獲取時(shí),首先會去讀volatile變量·非公平鎖獲取時(shí),首先會用CAS更新volatile變量,這個(gè)操作同時(shí)具有volatile讀和寫的內(nèi)存語義利用volatile變量的寫-讀所具有的內(nèi)存語義利用CAS所附帶的volatile讀和volatile寫的內(nèi)存語義concurrent包的A線程寫volatile變量,隨后B線程讀這個(gè)volatile變量A線程寫volatile變量,隨后B線程用CAS更新這個(gè)volatile變量A線程用CAS更新一個(gè)volatile變量,隨后B線程用CAS更新這個(gè)volatile變量A線程用CAS更新一個(gè)volatile變量,隨后B線程讀這個(gè)volatile變量的基石。如果仔細(xì)分析concurrent包的源代碼實(shí)現(xiàn),會發(fā)現(xiàn)一個(gè)通用化的實(shí)現(xiàn)模式。首先,共享變量為volatile然后,使用CAS的原子條件更新來實(shí)現(xiàn)線程之間的同包中的基礎(chǔ)類都是使用這種模式來實(shí)現(xiàn)的,而concurrent包中的類又是依賴于這些基礎(chǔ)類圖3-28concurrent包的實(shí)現(xiàn)示意final域的內(nèi)存與前面介紹的鎖和volatile相比,對final域的讀和寫更像是普通的變量。下面將介final域的內(nèi)存語義final域的重排序?qū)τ趂inal域,編譯器和處理器要遵守兩個(gè)重排序規(guī)則在構(gòu)造函數(shù)內(nèi)對一個(gè)fina域的寫入,與隨后把這個(gè)被構(gòu)造對象的賦值給一個(gè)變這兩個(gè)操作之間不能重排序。下面通過一些示例性的代碼來分別說明這兩個(gè)規(guī)則publicclassFinalExampleint //普通變finalintj; final變量staticFinalExampleobj;publicFinalExample //構(gòu)造i= //寫普j= //寫final}publicstaticvoidwriter //寫線程A執(zhí)obj=newFinalExample}publicstaticvoidreader(){//讀線程B執(zhí)行FinalExampleobject=obj;//讀對象inta=object.i;//讀普通域intb=object.j;//讀final}}寫final域的重排序JMM編譯器把final域的寫重排序到構(gòu)造函數(shù)之外編譯器會在final域的寫之后,構(gòu)造函數(shù)return之前,一個(gè)StoreStore屏障。這個(gè)屏 把這個(gè)對象的賦值給變量obj。假設(shè)線程B讀對象與讀對象的成員域之間沒有重排序(馬上會說明為什么需要這個(gè)設(shè)),圖3-29是一種可能的執(zhí)行時(shí)序在圖3-29中,寫普通域的操作被編譯器重排序到了構(gòu)造函數(shù)之外,讀線程B錯誤地了之內(nèi),讀線程B正確地了final變量初始化之后的值。正確初始化過了,而普通域不具有這個(gè)保障。以上圖為例,在讀線程B“看到”對象obj時(shí),圖3-29線行時(shí)序讀final域的重排序讀final域的重排序規(guī)則是,在一個(gè)線程中,初次讀對象與初次讀該對象包含的final域操作的前面一個(gè)LoadLoad屏障。讀對象次讀該對ial這兩個(gè)操作之間間賴于編譯間賴編譯這處間賴這操有處理器允間賴(比如alpha處理器),這個(gè)規(guī)則就是專門用來針對這種處理器的reader()方法包含3個(gè)操作·初次讀變量obj·初次讀變量obj指象的普通域j·初次讀變量obj指象的final域i現(xiàn)在假設(shè)寫線程A沒有發(fā)生任何重排序,同時(shí)程序在間接依賴的處理器上執(zhí)行,3-30所示是一種可能的執(zhí)行時(shí)序圖3-30線行時(shí)序域還沒有被寫線程A寫入,這是一個(gè)錯誤的操作。而讀final域的重排序規(guī)則會把讀對象確的操作。域的對象的。在這個(gè)示例程序中,如果該不為null,那么對象的final域一定已經(jīng)final域?yàn)轭惿厦婵吹降膄inal域是基礎(chǔ)數(shù)據(jù)類型,如果final域是類型,將會有什么效果?請看publicclassFinalReferenceExamplefinalint[] //final是類staticFinalReferenceExamplepublicFinalReferenceExample //構(gòu)造intArray=new //intArray[0]= //}publicstaticvoidwriterOne //寫線程A執(zhí)obj=newFinalReferenceExample();//}publicstaticvoidwriterTwo //寫線程B執(zhí)Array[0]= //}publicstaticvoidreader //讀線程C執(zhí)if(obj!=null){ //5inttemp1= //}}}本例final域?yàn)橐粋€(gè)類型,它一個(gè)int型的數(shù)組對象。對于類型,寫final域的重排序規(guī)則對編譯器和處理器增加了如下約束:在構(gòu)造函數(shù)內(nèi)對一個(gè)final的對象的成員域的寫入,與隨后在構(gòu)造函數(shù)外把這個(gè)被構(gòu)造對象的賦值給一個(gè)變量,這兩個(gè)操作之對上面的示例程序,假設(shè)首先線程A執(zhí)行writerOne()方法,執(zhí)行完后線程B執(zhí)writerTwo()方法,執(zhí)行完后線程C執(zhí)行reader()方法。圖3-31是一種可能的線行時(shí)序構(gòu)造的對象的賦值給某個(gè)變量。這里除了前面提到的1不能和3重排序外,2和3也不JMM可以確保讀線程C至少能看到寫線程A在構(gòu)造函數(shù)中對final對象的成員域的寫為什么final不能從構(gòu)造函數(shù)內(nèi)“溢出前面提到過,寫final域的重排序規(guī)則可以確保:在變量為任意線程可見之前,該還需要一個(gè)保證:在構(gòu)造函數(shù),不能讓這個(gè)被構(gòu)造對象的為其他線程所見,也就是對象不能在構(gòu)造函數(shù)中“逸出”。為了說明問題,讓來看下面的示例代碼。publicclass{finalintstaticFinalReferenceEscapeExampleobj;publicFinalReferenceEscapeExample(){i= //1寫finalobj= //2this在此"逸出}publicstaticvoidwriter()newFinalReferenceEscapeExample}publicstaticvoidreader()if(obj!=null){ //3inttemp= //}}}圖3-31型final的執(zhí)行時(shí)序假設(shè)一個(gè)線程A執(zhí)行writer()方法,另一個(gè)線程B執(zhí)行reader()方法。這里的操作2使得對排在操作1后面,執(zhí)行read()方法的線程仍然可能無法看到final域被初始化后的值,因?yàn)檫@里操作1和操作2之間可能被重排序。實(shí)際的執(zhí)行時(shí)序可能如圖3-32所示圖3-32多線行時(shí)序從圖3-32可以看出:在構(gòu)造函數(shù)返回前,被構(gòu)造對象的不能為其他線程所見,因?yàn)榇薴inal語義在處理器中的現(xiàn)在以X86處理器為例,說明final語義在處理器中的具體實(shí)現(xiàn)上面提到,寫final域的重排序規(guī)則會要求編譯器在final域的寫之后,構(gòu)造函數(shù)return之前一個(gè)StoreStore障屏。讀final域的重排序規(guī)則要求編譯器在讀final域的操作前面中,final域的讀/寫不會任何內(nèi)存屏障!JSR-133為什么要增強(qiáng)final的為了修補(bǔ)這個(gè),JSR-133組增強(qiáng)了final的語義。通過為final域增加寫和讀重排序規(guī)則,可以為Java程序員提供初始化安全保證:只要對象是正確構(gòu)造的(被構(gòu)造對象的在happens-before是JMM最的概念。對應(yīng)Java程序員來說,理解happens-before是理JMM的關(guān)鍵JMM的由于這兩個(gè)因素互相,所以JSR-133組在設(shè)計(jì)JMM時(shí)的目標(biāo)就是找到一個(gè)器的限制要盡可能地放松。下面讓來看JSR-133是如何實(shí)現(xiàn)這一目標(biāo)的。doublepi=3.14; //Adoubler =1.0; //Bdoublearea=pi*r*r;//C上面計(jì)算圓的面積的示例代碼存在3個(gè)happens-before關(guān)系,如下·Ahappens-beforeB·Ahappens-beforeC要求的重排序分為了下面兩類。·會改變程序執(zhí)行結(jié)果的·不會改變程序執(zhí)行結(jié)果的重排序JMM對這兩種不同性質(zhì)的重排序,采取了不同的策略,如·對于會改變程序執(zhí)行結(jié)果的重排序,JMM要求編譯器和處理器必須這種重排序圖3-33是JMM的設(shè)計(jì)示意圖圖3-33JMM的設(shè)計(jì)示意從圖3-33可以看出兩點(diǎn),如下并不一定真實(shí)存在,比如上面的Ahappens- B)J對編譯處的經(jīng)J實(shí)則變執(zhí)結(jié)果(單線線編譯處優(yōu)編譯經(jīng)過細(xì)認(rèn)鎖單線程這個(gè)鎖編譯經(jīng)過細(xì)認(rèn)volaile變量只會單線程編譯這volaile變量當(dāng)作一個(gè)普變對這些優(yōu)變執(zhí)結(jié)執(zhí)行效率。happens-before的定happens-before的概念最初由LeslieLamport在其一篇影響深遠(yuǎn)的(《Time,Clocksand分布式系統(tǒng)中事件之間的偏序關(guān)系(partialordering)。LeslieLamport在這篇中給出了一個(gè)《JSR-133:JavaMemoryModelandThreadSpecification》對happens-before關(guān)系的定義如下來執(zhí)行的結(jié)果一致,那么這種重排序并不(也就是說,JMM允許這種重排序)。上面的2)是JMM對編譯器和處理器重排序的約束原如前面所言,JMM其實(shí)是在遵happens-before《JSR-133:JavaMemoryModelandThreadSpecification》定義了如下happens-before規(guī)則。2)監(jiān)視器鎖規(guī)則:對一個(gè)鎖的,happens-before于隨后對這個(gè)鎖的加鎖。volatile變量規(guī)則:對一個(gè)volatile域的寫,happens-before于任意后續(xù)對這個(gè)volatile域讀傳遞性:如果Ahappens-beforeB,且Bhappens-beforeC,那么Ahappens-beforeCstart()規(guī)則:如果線程A執(zhí)行操作ThreadB.start()(啟動線程B),那么A線程ThreadB.start()操作happens-before于線程B中的任意操join()規(guī)則:如果線程A執(zhí)行操作ThreadB.join()并成功返回,那么線程Bhappens-before于線程A從ThreadB.join()操作成功返圖3-34happens-before關(guān)系的示意圖·1happens-before2和3happens-before4由程序順序規(guī)則產(chǎn)生。由于編譯器和處理器都要 3是由volatile規(guī)則產(chǎn)生。前面提到過,對一個(gè)volatile變量的讀,總是能到(任意線程)之前對這個(gè)volatile變量最后的寫入。因此,volatile的這個(gè)特性可以保證實(shí)volatile規(guī)則下面來看start()規(guī)則。假設(shè)線程A在執(zhí)行的過程中,通過執(zhí)行ThreadB.start()來啟動線圖3-35happens-before關(guān)系的示意在圖3-35中,1happens-before2由程序順序規(guī)則產(chǎn)生。2happens-before4由start()規(guī)則產(chǎn)變量所做的修改,接下來程B開始執(zhí)行后都將確保對線程B可見。下面來看join()規(guī)則。假設(shè)線程A在執(zhí)行的過程中,通過執(zhí)行ThreadB.join()來等待線圖3-36happens-before關(guān)系的示意在圖3-36中,2happens-before4由join()規(guī)則產(chǎn)生;4happens-before5由程序順序規(guī)則產(chǎn)生。根據(jù)傳遞性規(guī)則,將有2happens-before5。這意味著,線程A執(zhí)行操作ThreadB.join()并成功返雙重檢查鎖定與延遲初始Java線時(shí)遲類創(chuàng)對銷檢查鎖見遲術(shù)錯誤檢查鎖錯誤線遲雙重檢查鎖定的publicclass{privatestaticInstanceinstance;publicstaticInstancegetInstance(){if(instance== //1:A instance=new //2:B return}}程A可能會看到instance的對象還沒有完成初始化(出現(xiàn)這種情況的原因見3.8.2節(jié))。publicclass{privatestaticInstancepublicsynchronizedstaticInstance{if(instance==null)instance=newInstance();return}}由于對getInstance()方法做了同步處理,synchronized將導(dǎo)致性能開銷。如果getInstance()publicclassDoubleCheckedLocking{ //1privatestaticInstance //publicstaticInstancegetInstance() //if(instance==null){ 4:第一次檢查synchronized(DoubleCheckedLocking.class){//5:加鎖if(instance==null) //6:第二次檢查instance=newInstance(); //7:問題的根源出在這里 // //return // //}·多個(gè)線程試圖在同一時(shí)間創(chuàng)建對象時(shí),會通過加鎖來保證只有一個(gè)線程能創(chuàng)建對象取到instance不為null時(shí),instance的對象有可能還沒有完成初始化問題的根memoryallocate();//1:分配對象的內(nèi)存空間ctorInstance(memory);//2:初始化對象instance= //3:設(shè)置instance指向剛分配的內(nèi)存地發(fā)生的,詳情見參考文獻(xiàn)1的“Out-of-orderwrites”部分)。2和3之間重排序之后的執(zhí)行時(shí)序如memory=allocate();//1:分配對象的內(nèi)存空instance= //3:設(shè)置instance指向剛分配的內(nèi)存地//注意,此時(shí)對象還沒有被初始ctorInstance(memory);//2:初始化對根據(jù)《TheJavaLanguageSpecification,JavaSE7Edition》(后文簡稱為Java語言規(guī)范),所有線執(zhí)行Java程序時(shí)必須要遵守ra-threadsemantics保證重排序不會改變單線程內(nèi)的程序執(zhí)行結(jié)果。換句話說,intra-threadsemantics允許那些在單線程內(nèi),不會改造對象后,立即這個(gè)對象)。下面,再讓查看多線程并發(fā)執(zhí)行的情況。如圖3-示圖3-37線行時(shí)序圖3-38多線行時(shí)序instance所的對象,但此時(shí)這個(gè)對象可能還沒有被A線程初始化!表3-6是這個(gè)場景的表3-6多線行時(shí)序B在B1處判斷出instance不為空,線程B接下來將instance的對象。此時(shí),線程B將會訪在知曉了問題發(fā)生的根源之后,可以想出兩個(gè)辦法來實(shí)現(xiàn)線程安全的延遲初始化不允許2和3重排序基于volatile的解決方對檢查鎖實(shí)現(xiàn)遲(指obleeceoci示例代碼)(isacevolaile型)實(shí)現(xiàn)線延遲請碼。publicclass{privatevolatilestaticInstanceinstance;publicstaticInstancegetInstance(){if(instance==null)synchronized{if(instance==instance=new //instance為volatile,現(xiàn)在沒問}}return}}注意這個(gè)解決方案需要JDK5或更高版本(因?yàn)閺腏DK5開始使用新的JSR-133內(nèi)存模當(dāng)對象的為volatile后,3.8.2節(jié)中的3行偽代碼中的2和3之間的重排序,在多線程環(huán)境中將會被。上面示例代碼將按如下的時(shí)序執(zhí)行,如圖3-39所示。圖3-39多線行時(shí)序這個(gè)方案本質(zhì)上是通過圖3-39中的2和3之間的重排序,來保證線程安全的延遲初化基于類初始化的解決方JVM在類的初始化階段(即在Class被加載后,且被線程使用之前),會執(zhí)行類的初始化。執(zhí)行類的初始化期間,JVM會去獲取一個(gè)鎖。這個(gè)鎖可以同步多個(gè)線同一個(gè)類的初始化基于這個(gè)特性,可以實(shí)現(xiàn)另一種線程安全的延遲初始化方案(這個(gè)方案被稱之InitializationOnDemandHolderidiom)publicclassInstanceFactoryprivatestaticclassInstanceHolderpublicstaticInstanceinstance=new}publicstaticInstancegetInstance()returnInstanceHolder.instance;//這里將導(dǎo)致InstanceHolder類被初始}}假設(shè)兩個(gè)線程并發(fā)執(zhí)行g(shù)etInstance()方法,下面是執(zhí)行的示意圖,如圖3-40所示圖3-40兩個(gè)線程并發(fā)執(zhí)行的示意這個(gè)方案的實(shí)質(zhì)是:允許3.8.2節(jié)中的3行偽代碼中的2和3重排序,但不允許非構(gòu)造線程(里指線程B)“看到”這個(gè)重排序T是一個(gè)類,而且一個(gè)T類型的實(shí)例被創(chuàng)建T是一個(gè)類,且T中的一個(gè)靜態(tài)方法被調(diào)用T中的一個(gè)靜態(tài)字段被賦值T中的一個(gè)靜態(tài)字段被使用,而且這個(gè)字段不是一個(gè)常量字段 Class,見Java語言規(guī)范的§7.6),而且一個(gè)斷言語句嵌套在T圖3-41類初始化——第1階段表3-7類初始化——第1階段的執(zhí)行時(shí)第2階段:線程A執(zhí)行類的初始化,同時(shí)線程B在初始化鎖對應(yīng)的condition上等待表3-8類初始化——第2階段的執(zhí)行時(shí)圖3-42類初始化——第2階第3階段:線程A設(shè)置state=initialized,然后喚醒在condition中等待的所有線程圖3-43類初始化——第3階段表3-9類初始化——第3階段的執(zhí)行時(shí)第4階段:線程B結(jié)束類的初始化處圖3-44類初始化——第4階段表3-10類初始化——第4階段的執(zhí)行時(shí)圖3-45多線行時(shí)序化和初始化類中的靜態(tài)字段),線程B一定能看到。第5階段:線程C執(zhí)行類的初始化的處理圖3-46類初始化——第5階段表3-11類初始化——第5階段的執(zhí)行時(shí)單一些(前面的線程A和B的類初始化處理過經(jīng)歷了兩次鎖獲取-鎖,而線程C的類初始化處理只需要經(jīng)歷一次鎖獲取-鎖)。線程A在第2階段的A1執(zhí)行類的初始化,并在第3階段的A4鎖;線程C在第5階段的獲取同一個(gè)鎖,并在在第5階段的C4之后才開始這個(gè)類。根據(jù)Java內(nèi)存模型規(guī)范的鎖規(guī)則將存在如下的happens-before關(guān)系這個(gè)happens-before關(guān)系將保證:線程A執(zhí)行類的初始化時(shí)的寫入操作,線程C一定能到注意這里的condition和state標(biāo)記是本文虛構(gòu)出來的。Java語言規(guī)范并沒有硬性規(guī)定一注 Java語言規(guī)范允許Java的具體實(shí)現(xiàn),優(yōu)化類的初始化處理 這里的第5段做優(yōu)化),具體細(xì)節(jié)參見Java語言規(guī)范的12.4.2節(jié)圖3-47多線行時(shí)序遲化類創(chuàng)實(shí)開銷了遲銷時(shí)優(yōu)遲實(shí)對實(shí)線遲請紹volaile遲實(shí)對態(tài)字線遲請紹類Java內(nèi)存模型綜處理器的內(nèi)存模化都要被,這對執(zhí)行性能將會有很大的影響。根據(jù)對不同類型的讀/寫操作組合的執(zhí)行順序的放松,可以把常見處理器的內(nèi)存模型劃為如下幾種類型·放松
溫馨提示
- 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請下載最新的WinRAR軟件解壓。
- 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請聯(lián)系上傳者。文件的所有權(quán)益歸上傳用戶所有。
- 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁內(nèi)容里面會有圖紙預(yù)覽,若沒有圖紙預(yù)覽就沒有圖紙。
- 4. 未經(jīng)權(quán)益所有人同意不得將文件中的內(nèi)容挪作商業(yè)或盈利用途。
- 5. 人人文庫網(wǎng)僅提供信息存儲空間,僅對用戶上傳內(nèi)容的表現(xiàn)方式做保護(hù)處理,對用戶上傳分享的文檔內(nèi)容本身不做任何修改或編輯,并不能對任何下載內(nèi)容負(fù)責(zé)。
- 6. 下載文件中如有侵權(quán)或不適當(dāng)內(nèi)容,請與我們聯(lián)系,我們立即糾正。
- 7. 本站不保證下載資源的準(zhǔn)確性、安全性和完整性, 同時(shí)也不承擔(dān)用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。
最新文檔
- 2025年度門面房出租與租賃期限調(diào)整合同
- 二零二五年度診所負(fù)責(zé)人安全責(zé)任免除合同
- 服務(wù)器采購合同共
- 無人機(jī)研發(fā)制造投資合同
- 水利設(shè)施施工合同
- 高考語文復(fù)習(xí)-文言文專題訓(xùn)練-《遼史》
- 高考語文復(fù)習(xí):文言文霍去病專練
- 農(nóng)業(yè)產(chǎn)業(yè)孵化項(xiàng)目合作協(xié)議書
- 業(yè)務(wù)流程外包服務(wù)協(xié)議內(nèi)容詳訂
- 數(shù)字媒體設(shè)計(jì)技能考核點(diǎn)
- 六年級上冊心理健康課件6《健康上網(wǎng)快樂多》(27張PPT)
- 改進(jìn)維持性血液透析患者貧血狀況PDCA
- 城市軌道交通工程施工組織設(shè)計(jì)與概預(yù)算PPT全套完整教學(xué)課件
- 某高速公路江蘇段施工組織設(shè)計(jì)
- 全國青少年機(jī)器人技術(shù)等級(機(jī)器人二級)考試復(fù)習(xí)題庫(含真題)
- 學(xué)習(xí)弘揚(yáng)雷鋒精神課件
- 行政區(qū)域代碼表Excel
- 精神病醫(yī)院管理制度
- 化工廠中控DCS系統(tǒng)崗位職責(zé)
- 唯物史觀指導(dǎo)初中歷史教學(xué)
- 2023年同等學(xué)力研究生考試教育學(xué)試卷附詳細(xì)答案
評論
0/150
提交評論