從一個簡單的Java單例示例談談并發(上)
一個簡單的單例示例
單例模式可能是大家經常接觸和使用的一個設計模式,你可能會這么寫
public?class?UnsafeLazyInitiallization?{? ????private?static?UnsafeLazyInitiallization?instance;? ????private?UnsafeLazyInitiallization()?{?}? ????public?static?UnsafeLazyInitiallization?getInstance(){? ????????if(instance==null){? ????????????//1:A線程執行? ????????????instance=new?UnsafeLazyInitiallization();? ????????????//2:B線程執行? ????????}? ????????return?instance;? ????}? }
上面代碼大家應該都知道,所謂的線程不安全的懶漢單例寫法。在UnsafeLazyInitiallization類中,假設A線程執行代碼1的同時,B線程執行代碼2,此時,線程A可能看到instance引用的對象還沒有初始化。
你可能會說,線程不安全,我可以對getInstance()方法做同步處理保證安全啊,比如下面這樣的寫法
public?class?SafeLazyInitiallization?{? ?????private?static?SafeLazyInitiallization?instance;? ?????private?SafeLazyInitiallization()?{?}? ?????public?synchronized?static?SafeLazyInitiallization?getInstance(){? ?????????if(instance==null){? ?????????????instance=new?SafeLazyInitiallization();? ?????????}? ?????????return?instance;? ?????}? }
這樣的寫法是保證了線程安全,但是由于getInstance()方法做了同步處理,synchronized將導致性能開銷。如getInstance()方法被多個線程頻繁調用,將會導致程序執行性能的下降。反之,如果getInstance()方法不會被多個線程頻繁的調用,那么這個方案將能夠提供令人滿意的性能。
那么,有沒有更優雅的方案呢?前人的智慧是偉大的,在早期的JVM中,synchronized存在巨大的性能開銷,因此,人們想出了一個“聰明”的技巧——雙重檢查鎖定。人們通過雙重檢查鎖定來降低同步的開銷。下面來讓我們看看
public?class?DoubleCheckedLocking?{? ????//1? ????private?static?DoubleCheckedLocking?instance;? ????//2? ????private?DoubleCheckedLocking()?{?}? ????public?static?DoubleCheckedLocking?getInstance()?{? ????????//3? ????????if?(instance?==?null)?{? ????????????//4:第一次檢查? ????????????synchronized?(DoubleCheckedLocking.class)?{? ????????????????//5:加鎖? ????????????????if?(instance?==?null)? ????????????????//6:第二次檢查? ????????????????instance?=?new?DoubleCheckedLocking();? ????????????//7:問題的根源出在這里? ????????????}? ????????//8? ????????}? ????????//9? ????????return?instance;? ????//10? ????}? ????//11? }
如上面代碼所示,如果第一次檢查instance不為null,那么就不需要執行下面的加鎖和初始化操作。因此,可以大幅降低synchronized帶來的性能開銷。雙重檢查鎖定看起來似乎很完美,但這是一個錯誤的優化!為什么呢?在線程執行到第4行,代碼讀取到instance不為null時,instance引用的對象有可能還沒有完成初始化。在第7行創建了一個對象,這行代碼可以分解為如下的3行偽代碼
memory=allocate();? //1:分配對象的內存空間? ctorInstance(memory);? //2:初始化對象? instance=memory;? //3:設置instance指向剛分配的內存地址
上面3行代碼中的2和3之間,可能會被重排序(在一些JIT編譯器上,這種重排序是真實發生的,如果不了解重排序,后文JMM會詳細解釋)。2和3之間重排序之后的執行時序如下
memory=allocate();? //1:分配對象的內存空間? instance=memory;? //3:設置instance指向剛分配的內存地址,注意此時對象還沒有被初始化? ctorInstance(memory);? //2:初始化對象
回到示例代碼第7行,如果發生重排序,另一個并發執行的線程B就有可能在第4行判斷instance不為null。線程B接下來將訪問instance所引用的對象,但此時這個對象可能還沒有被A線程初始化。在知曉問題發生的根源之后,我們可以想出兩個辦法解決
不允許2和3重排序
允許2和3重排序,但不允許其他線程“看到”這個重排序
下面就介紹這兩個解決方案的具體實現
基于volatile的解決方案
對于前面的基于雙重檢查鎖定的方案,只需要做一點小的修改,就可以實現線程安全的延遲初始化。請看下面的示例代碼
public?class?SafeDoubleCheckedLocking?{? ????private?volatile?static?SafeDoubleCheckedLocking?instance;? ????private?SafeDoubleCheckedLocking()?{?}? ????public?static?SafeDoubleCheckedLocking?getInstance()?{? ????????if?(instance?==?null)?{? ????????????synchronized?(SafeDoubleCheckedLocking.class)?{? ????????????????if?(instance?==?null)? ????????????????????instance?=?new?SafeDoubleCheckedLocking(); ????????????????????//instance為volatile,現在沒問題了? ????????????}? ????????}? ????????return?instance;? ????}? }
當聲明對象的引用為volatile后,前面偽代碼談到的2和3之間的重排序,在多線程環境中將會被禁止。
基于類初始化的解決方案
JVM在類的初始化階段(即在Class被加載后,且被線程使用之前),會執行類的初始化。在執行類的初始化期間,JVM會去獲取多個線程對同一個類的初始化。基于這個特性,實現的示例代碼如下
public?class?InstanceFactory?{? ????private?InstanceFactory()?{?}? ????private?static?class?InstanceHolder?{? ????????public?static?InstanceFactory?instance?=?new?InstanceFactory();? ????}? ????public?static?InstanceFactory?getInstance()?{? ????????return?InstanceHolder.instance;? ????????//這里將導致InstanceHolder類被初始化? ????}? }
這個方案的本質是允許前面偽代碼談到的2和3重排序,但不允許其他線程“看到”這個重排序。在InstanceFactory示例代碼中,首次執行getInstance()方法的線程將導致InstanceHolder類被初始化。由于Java語言是多線程的,多個線程可能在同一時間嘗試去初始化同一個類或接口(比如這里多個線程可能會在同一時刻調用getInstance()方法來初始化IInstanceHolder類)。Java語言規定,對于每一個類和接口C,都有一個唯一的初始化鎖LC與之對應。從C到LC的映射,由JVM的具體實現去自由實現。JVM在類初始化期間會獲取這個初始化鎖,并且每個線程至少獲取一次鎖來確保這個類已經被初始化過了。
JMM
也許你還存在疑問,前面談的重排序是什么鬼?為什么volatile在某方面就能禁止重排序?現在引出本文的另一個話題JMM(Java Memory Model——Java內存模型)。什么是JMM呢?JMM是一個抽象概念,它并不存在。Java虛擬機規范中試圖定義一種Java內存模型(JMM)來屏蔽掉各種硬件和操作系統的內存訪問差異,以實現讓Java程序在各種平臺下都能達到一致的內存訪問效果。在此之前,主流程序語言(如C/C++等)直接使用物理硬件和操作系統的內存模型,因此,會由于不同平臺的內存模型的差異,有可能導致程序在一套平臺上并發完全正常,而在另一套平臺上并發訪問卻經常出錯,因此在某些場景就必須針對不同的平臺來編寫程序。
Java線程之間的通信由JMM來控制,JMM決定一個線程共享變量的寫入何時對另一個線程可見。JMM保證如果程序是正確同步的,那么程序的執行將具有順序一致性。從抽象的角度看,JMM定義了線程和主內存之間的抽象關系:線程之間的共享變量(實例域、靜態域和數據元素)存儲在主內存(Main Memory)中,每個線程都有一個私有的本地內存(Local Memory),本地內存中存儲了該線程以讀/寫共享變量的副本(局部變量、方法定義參數和異常處理參數是不會在線程之間共享,它們存儲在線程的本地內存中)。從物理角度上看,主內存僅僅是虛擬機內存的一部分,與物理硬件的主內存名字一樣,兩者可以互相類比;而本地內存,可與處理器高速緩存類比。Java內存模型的抽象示意圖如圖所示
這里先介紹幾個基礎概念:8種操作指令、內存屏障、順序一致性模型、as-if-serial、happens-before 、數據依賴性、 重排序。
8種操作指令
關于主內存與本地內存之間具體的交互協議,即一個變量如何從主內存拷貝到本地內存、如何從本地內存同步回主內存之類的實現細節,JMM中定義了以下8種操作來完成,虛擬機實現時必須保證下面提及的每種操作都是原子的、不可再分的(對于double和long類型的遍歷來說,load、store、read和write操作在某些平臺上允許有例外):
lock(鎖定):作用于主內存的變量,它把一個變量標識為一條線程獨立的狀態。
unlock(解鎖):作用于主內存的變量,它把一個處于鎖定狀態的變量釋放出來,釋放后的變量才可以被其他線程鎖定。
read(讀取):作用于主內存的變量,它把一個變量的值從主內存傳輸到線程的本地內存中,以便隨后的load動作使用。
load(載入):作用于本地內存的變量,它把read操作從主內存中得到變量值放入本地內存的變量副本中。
use(使用):作用于本地內存的變量,它把本地內存中一個變量的值傳遞給執行引擎,每當虛擬機遇到一個需要使用到變量的值的字節碼指令時將會執行這個操作。
assign(賦值):作用于本地內存的變量,它把一個從執行引擎接收到的值賦給本地內存的變量,每當虛擬機遇到一個給變量賦值的字節碼指令時執行這個操作。
store(存儲):作用于本地內存的變量,它把本地內存中的一個變量的值傳送到主內存中,以便隨后的write操作使用。
write(寫入):作用于主內存的變量,它把store操作從本地內存中提到的變量的值放入到主內存的變量中。
如果要把一個變量從主內存模型復制到本地內存,那就要順序的執行read和load操作,如果要把變量從本地內存同步回主內存,就要順序的執行store和write操作。注意,Java內存模型只要求上述兩個操作必須按順序執行,而沒有保證是連續執行。也就是說read與load之間、store與write之間是可插入其他指令的,如對主內存中的變量a、b進行訪問時,一種可能出現的順序是read a read b、load b、load a。
內存屏障
內存屏障是一組處理器指令(前面的8個操作指令),用于實現對內存操作的順序限制。包括LoadLoad, LoadStore, StoreLoad, StoreStore共4種內存屏障。內存屏障存在的意義是什么呢?它是在Java編譯器生成指令序列的適當位置插入內存屏障指令來禁止特定類型的處理器重排序,從而讓程序按我們預想的流程去執行,內存屏障是與相應的內存重排序相對應的。JMM把內存屏障指令分為4類
StoreLoad Barriers是一個“全能型 ”的屏障,它同時具有其他3個屏障的效果?,F在的多數處理器大多支持該屏障(其他類型的屏障不一定被所有處理器支持)。執行該屏障開銷會很昂貴,因為當前處理器通常要把寫緩沖區中的數據全部刷新到內存中。
數據依賴性
如果兩個操作訪問同一個變量,且這兩個操作中有一個為寫操作,此時這兩個操作之間就存在數據依賴性。數據依賴性分3種類型:寫后讀、寫后寫、讀后寫。這3種情況,只要重排序兩個操作的執行順序,程序的執行結果就會被改變。編譯器和處理器可能對操作進行重排序。而它們進行重排序時,會遵守數據依賴性,不會改變數據依賴關系的兩個操作的執行順序。
這里所說的數據依賴性僅針對單個處理器中執行的指令序列和單個線程中執行的操作,不同處理器之間和不同線程之間的數據依賴性不被編譯器和處理器考慮。
順序一致性內存模型
順序一致性內存模型是一個理論參考模型,在設計的時候,處理器的內存模型和編程語言的內存模型都會以順序一致性內存模型作為參照。它有兩個特性:
一個線程中的所有操作必須按照程序的順序來執行
(不管程序是否同步)所有線程都只能看到一個單一的操作執行順序。在順序一致性的內存模型中,每個操作必須原子執行并且立刻對所有線程可見。
從順序一致性模型中,我們可以知道程序所有操作完全按照程序的順序串行執行。而在JMM中,臨界區內的代碼可以重排序(但JMM不允許臨界區內的代碼“逸出”到臨界區外,那樣就破壞監視器的語義)。JMM會在退出臨界區和進入臨界區這兩個關鍵時間點做一些特別處理,使得線程在這兩個時間點具有與順序一致性模型相同的內存視圖。雖然線程A在臨界區內做了重排序,但由于監視器互斥執行的特性,這里的線程B根本無法“觀察”到線程A在臨界區內的重排序。這種重排序既提高了執行效率,又沒有改變程序的執行結果。像前面單例示例的類初始化解決方案就是采用了這個思想。
as-if-serial
as-if-serial的意思是不管怎么重排序,(單線程)程序的執行結果不能改變。編譯器、runtime和處理器都必須遵守as-if-serial語義。為了遵守as-if-serial語義,編譯器和處理器不會對存在數據依賴關系的操作做重排序。
as-if-serial語義把單線程程序保護了起來,遵守as-if-serial語義的編譯器、runtime和處理器共同為編寫單線程程序的程序員創建了一個幻覺:單線程程序是按程序的順序來執行的。as-if-serial語義使單線程程序員無需擔心重排序會干擾他們,也無需擔心內存可見性問題。
happens-before
happens-before是JMM最核心的概念。從JDK5開始,Java使用新的JSR-133內存模型,JSR-133 使用happens-before的概念闡述操作之間的內存可見性,如果一個操作執行的結果需要對另一個操作可見,那么這兩個操作之間必須存在happens-before關系。
happens-before規則如下:
程序次序法則:線程中的每個動作 A 都 happens-before 于該線程中的每一個動作 B,其中,在程序中,所有的動作 B 都出現在動作 A 之后。(注:此法則只是要求遵循 as-if-serial語義)
監視器鎖法則:對一個監視器鎖的解鎖 happens-before 于每一個后續對同一監視器鎖的加鎖。(顯式鎖的加鎖和解鎖有著與內置鎖,即監視器鎖相同的存儲語意。)
volatile變量法則:對 volatile 域的寫入操作 happens-before 于每一個后續對同一域的讀操作。(原子變量的讀寫操作有著與 volatile 變量相同的語意。)(volatile變量具有可見性和讀寫原子性。)
線程啟動法則:在一個線程里,對 Thread.start 的調用會 happens-before 于每一個啟動線程中的動作。 線程終止法則:線程中的任何動作都 happens-before 于其他線程檢測到這個線程已終結,或者從 Thread.join 方法調用中成功返回,或者 Thread.isAlive 方法返回false。
中斷法則法則:一個線程調用另一個線程的 interrupt 方法 happens-before 于被中斷線程發現中斷(通過拋出InterruptedException, 或者調用 isInterrupted 方法和 interrupted 方法)。
終結法則:一個對象的構造函數的結束 happens-before 于這個對象 finalizer 開始。
傳遞性:如果 A happens-before 于 B,且 B happens-before 于 C,則 A happens-before 于 C。
happens-before與JMM的關系如下圖所示
as-if-serial語義和happens-before本質上一樣,參考順序一致性內存模型的理論,在不改變程序執行結果的前提下,給編譯器和處理器以最大的自由度,提高并行度。
重排序
終于談到我們反復提及的重排序了,重排序是指編譯器和處理器為了優化程序性能而對指令序列進行重新排序的一種手段。重排序分3種類型。
編譯器優化的重排序。編譯器在不改變單線程程序語義(as-if-serial )的前提下,可以重新安排語句的執行順序。
指令級并行的重排序。現代處理器采用了指令級并行技術(Instruction Level Parallelism,ILP)來將多條指令重疊執行。如果不存在數據依賴性,處理器可以改變語句對機器指令的執行順序。
內存系統的重排序。由于處理器使用緩存和讀/寫緩沖區,這使得加載和存儲操作看上去可能是在亂序執行。
從Java源代碼到最終實際執行的指令序列,會分別經歷下面3種重排序
上述的1屬于編譯器重排序,2和3屬于處理器重排序。這些重排序可能會導致多線程程序出現內存可見性問題。對于編譯器,JMM的編譯器重排序規則會禁止特定類型的編譯器重排序(不是所有的編譯器重排序都要禁止)。對于處理器重排序,JMM的處理器重排序規則會要求Java編譯器在生成指令序列時,插入特定類型的內存屏障指令,通過內存屏障指令來禁止特定類型的處理器重排序。
JMM屬于語言級的內存模型,它確保在不同的編譯器和不同的處理器平臺之上,通過禁止特定類型的編譯器重排序和處理器重排序,為程序員提供一致的內存可見性保證。
從JMM設計者的角度來說,在設計JMM時,需要考慮兩個關鍵因素:
程序員對內存模型的使用。程序員希望內存模型易于理解,易于編程。程序員希望基于一個強內存模型(程序盡可能的順序執行)來編寫代碼。
編譯器和處理器對內存模型的實現。編譯器和處理器希望內存模型對它們的束縛越少越好,這樣它們就可以做盡可能多的優化(對程序重排序,做盡可能多的并發)來提高性能。編譯器和處理器希望實現一個弱內存模型。
JMM設計就需要在這兩者之間作出協調。JMM對程序采取了不同的策略:
對于會改變程序執行結果的重排序,JMM要求編譯器和處理器必須禁止這種重排序。
對于不會改變程序執行結果的重排序,JMM對編譯器和處理器不作要求(JMM允許這種重排序)。
介紹完了這幾個基本概念,我們不難推斷出JMM是圍繞著在并發過程中如何處理原子性、可見性和有序性這三個特征來建立的:
原子性:由Java內存模型來直接保證的原子性操作就是我們前面介紹的8個原子操作指令,其中lock(lock指令實際在處理器上原子操作體現對總線加鎖或對緩存加鎖)和unlock指令操作JVM并未直接開放給用戶使用,但是卻提供了更高層次的字節碼指令monitorenter和monitorexit來隱式使用這兩個操作,這兩個字節碼指令反映到Java代碼中就是同步塊——synchronize關鍵字,因此在synchronized塊之間的操作也具備原子性。除了synchronize,在Java中另一個實現原子操作的重要方式是自旋CAS,它是利用處理器提供的cmpxchg指令實現的。至于自旋CAS后面J.U.C中會詳細介紹,它和volatile是整個J.U.C底層實現的核心。
可見性:可見性是指一個線程修改了共享變量的值,其他線程能夠立即得知這個修改。而我們上文談的happens-before原則禁止某些處理器和編譯器的重排序,來保證了JMM的可見性。而體現在程序上,實現可見性的關鍵字包含了volatile、synchronize和final。
有序性:談到有序性就涉及到前面說的重排序和順序一致性內存模型。我們也都知道了as-if-serial是針對單線程程序有序的,即使存在重排序,但是最終程序結果還是不變的,而多線程程序的有序性則體現在JMM通過插入內存屏障指令,禁止了特定類型處理器的重排序。通過前面8個操作指令和happens-before原則介紹,也不難推斷出,volatile和synchronized兩個關鍵字來保證線程之間的有序性,volatile本身就包含了禁止指令重排序的語義,而synchronized則是由監視器法則獲得。
J.U.C
談完了JMM,那么Java相關類庫是如何實現的呢?這里就談談J.U.C( java.util.concurrent),先來張J.U.C的思維導圖
不難看出,J.U.C由atomic、locks、tools、collections、executor這五部分組成。它們的實現基于volatile的讀寫和CAS所具有的volatile讀和寫。AQS(AbstractQueuedSynchronizer,隊列同步器)、非阻塞數據結構和原子變量類,這些J.U.C中的基礎類都是使用了這種模式實現的,而J.U.C中的高層類又依賴于這些基礎類來實現的。從整體上看,J.U.C的實現示意圖如下
也許你對volatile和CAS的底層實現原理不是很了解,這里先這里先簡單介紹下它們的底層實現
volatile
Java語言規范第三版對volatile的定義為:Java編程語言允許線程訪問共享變量,為了確保共享變量能被準確和一致性的更新,線程應該確保通過排他鎖單獨獲得這個變量。如果一個字段被聲明為volatile,Java內存模型確保這個所有線程看到這個值的變量是一致的。而volatile是如何來保證可見性的呢?如果對聲明了volatile的變量進行寫操作,JVM就會向處理器發送一條Lock前綴的指令,將這個變量所在緩存行的數據寫回到系統內存(Lock指令會在聲言該信號期間鎖總線/緩存,這樣就獨占了系統內存)。但是,就算是寫回到內存,如果其他處理器緩存的值還是舊的,再執行計算操作就會有問題。所以,在多處理器下,為了保證各個處理器的緩存是一致的,就會實現緩存一致性協議,每個處理器通過嗅探在總線(注意處理器不直接跟系統內存交互,而是通過總線)上傳播的數據來檢查自己緩存的值是不是過期了,當處理器發現直接緩存行對應的內存地址被修改,就會將當前處理器的緩存行設置成無效狀態,當處理器對這個數據進行修改操作的時候,會重新從系統內存中把數據讀到處理器緩存里。
CAS
CAS其實應用挺廣泛的,我們常常聽到的悲觀鎖樂觀鎖的概念,樂觀鎖(無鎖)指的就是CAS。這里只是簡單說下在并發的應用,所謂的樂觀并發策略,通俗的說,就是先進性操作,如果沒有其他線程爭用共享數據,那操作就成功了,如果共享數據有爭用,產生了沖突,那就采取其他的補償措施(最常見的補償措施就是不斷重試,治到成功為止,這里其實也就是自旋CAS的概念),這種樂觀的并發策略的許多實現都不需要把線程掛起,因此這種操作也被稱為非阻塞同步。而CAS這種樂觀并發策略操作和沖突檢測這兩個步驟具備的原子性,是靠什么保證的呢?硬件,硬件保證了一個從語義上看起來需要多次操作的行為只通過一條處理器指令就能完成。
也許你會存在疑問,為什么這種無鎖的方案一般會比直接加鎖效率更高呢?這里其實涉及到線程的實現和線程的狀態轉換。實現線程主要有三種方式:使用內核線程實現、使用用戶線程實現和使用用戶線程加輕量級進程混合實現。而Java的線程實現則依賴于平臺使用的線程模型。至于狀態轉換,Java定義了6種線程狀態,在任意一個時間點,一個線程只能有且只有其中的一種狀態,這6種狀態分別是:新建、運行、無限期等待、限期等待、阻塞、結束。 Java的線程是映射到操作系統的原生線程之上的,如果要阻塞或喚醒一個線程,都需要操作系統來幫忙完成,這就需要從用戶態轉換到核心態中,因此狀態轉換需要耗費很多的處理器時間。對于簡單的同步塊(被synchronized修飾的方法),狀態轉換消耗的時間可能比用戶代碼執行的時間還要長。所以出現了這種優化方案,在操作系統阻塞線程之間引入一段自旋過程或一直自旋直到成功為止。避免頻繁的切入到核心態之中。
但是這種方案其實也并不完美,在這里就說下CAS實現原子操作的三大問題
ABA問題。因為CAS需要在操作值的時候,檢查值有沒有變化,如果沒有發生變化則更新,但是如果一個值原來是A,變成了B,又變成了A,那么使用CAS進行檢查時會發現它的值沒有變化,但是實際上發生變化了。ABA解決的思路是使用版本號。在變量前面追加上版本號,每次變量更新的時候把版本號加1。JDK的Atomic包里提供了一個類AtomicStampedReference來解決ABA問題。不過目前來說這個類比較“雞肋”,大部分情況下ABA問題不會影響程序并發的正確性,如果需要解決ABA問題,改用原來的互斥同步可能會比原子類更高效。
循環時間長開銷大。自旋CAS如果長時間不成功,會給CPU帶來非常大的執行開銷。所以說如果是長時間占用鎖執行的程序,這種方案并不適用于此。
只能保證一個共享變量的原子操作。當對一個共享變量執行操作時,我們可以使用自旋CAS來保證原子性,但是對多個共享變量的操作時,自旋CAS就無法保證操作的原子性,這個時候可以用鎖。
談完了這兩個概念,下面我們就來逐個分析這五部分的具體源碼實現
atomic
atomic包的原子操作類提供了一種簡單、性能高效、線程安全操作一個變量的方式。atomic包里一共13個類,屬于4種類型的原子更新方式,分別是原子更新基本類型、原子更新數組、原子更新引用、原子更新屬性。atomic包里的類基本使用Unsafe實現的包裝類。
下面通過一個簡單的CAS方式實現計數器(一個線程安全的計數器方法safeCount和一個非線程安全的計數器方法count)的示例來說下
public?class?CASTest?{? ????public?static?void?main(String[]?args){? ????????final?Counter?cas=new?Counter();? ????????List?ts=new?ArrayList(600);? ????????long?start=System.currentTimeMillis();? ????????for(int?j=0;j<100;j++){? ????????????Thread?t=new?Thread( ????????????????new?Runnable()?{? ????????????????????@Override?public?void?run()?{? ????????????????????????for(int?i=0;i<10000;i++){? ????????????????????????????cas.count();?cas.safeCount();? ????????????????????????}? ????????????????????}? ????????????????} ????????????);? ????????????ts.add(t);? ????????}? ????????for(Thread?t:ts){? ????????????t.start();? ????????}? ????????for(Thread?t:ts){? ????????????try?{? ????????????????t.join();? ????????????}catch?(InterruptedException?e)?{? ????????????????e.printStackTrace();? ????????????}? ????????}? ????????System.out.println(cas.i);? ????????System.out.println(cas.atomicI.get());? ????????System.out.println(System.currentTimeMillis()-start);? ????}? }? public?class?Counter?{? ????public?AtomicInteger?atomicI=new?AtomicInteger(0);? ????public?int?i=0;? ????/**?*?使用CAS實現線程安全計數器?*/? ????public?void?safeCount(){? ????????for(;;){? ????????????int?i=atomicI.get();? ????????????boolean?suc=atomicI.compareAndSet(i,++i);? ????????????if(suc){?break;?}? ????????} ????}? ????/**?*?非線程安全計數器?*/? ????public?void?count(){?i++;?}? }
safeCount()方法的代碼塊其實是getandIncrement()方法的實現,源碼for循環體第一步優先取得atomicI里存儲的數值,第二步對atomicI的當前數值進行加1操作,關鍵的第三步調用compareAndSet()方法來進行原子更新操作,該方法先檢查當前數值是否等于current,等于意味著atomicI的值沒有被其他線程修改過,則將atomicI的當前數值更新成next的值,如果不等compareAndSet()方法會返回false,程序則進入for循環重新進行compareAndSet()方法操作進行不斷嘗試直到成功為止。在這里我們跟蹤下compareAndSet()方法如下
public?final?boolean?compareAndSet(int?expect,?int?update)?{? ????return?unsafe.compareAndSwapInt(this,?valueOffset,?expect,?update);? }
從上面源碼我們發現是使用Unsafe實現的,其實atomic里的類基本都是使用Unsafe實現的。我們再回到這個本地方法調用,這個本地方法在openjdk中依次調用c++代碼為unsafe.cpp、atomic.app和atomic_windows_x86.inline.hpp。關于本地方法實現的源代碼這里就不貼出來了,其實大體上是程序會根據當前處理器的類型來決定是否為cmpxchg指令添加lock前綴。如果程序是在多處理器上運行,就為cmpxchg指令加上lock前綴(Lock Cmpxchg)。反之,如果程序是在單處理器上運行,就省略lock前綴(單處理器自身就會維護單處理器內的順序一致性,不需要lock前綴提供的內存屏障效果)。
從一個簡單的Java單例示例談談并發(下)
本文轉載自異步社區。
軟件開發 Web應用防火墻 WAF
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。