jmm內存可見性與CAS

前言:在慕課網上學習劍指Java面試-Offer直通車時所做的筆記,供本人複習之用.

目錄

第一章 Java內存模型

第二章 JMM中的主內存和工作內存 

2.1 主內存與工作內存介紹

2.2 JMM與JVM的區別

2.3 可見性問題

2.4 指令重排序需要滿足的條件

2.5 happens-before原則

2.5.1 例1

2.5.2 volatile 

2.5.3 例2

2.5.4 例3

2.5.5 例4

第三章 CAS(Compare and Swap)

3.1 AtomicInteger


 

第一章 Java內存模型

 由於JVM運行程序的實體是線程,而每個線程創建時JVM都會爲其創建一個工作內存,有些地方成爲棧空間,用於存儲線程私有的數據,而java內存模型中規定,所有變量都存儲在主內存中,主內存是共享內存區域,所有線程都可以訪問,線程對變量的操作如讀取,賦值等必須在工作內存中進行,首先將變量從主內存拷貝到自己的工作內存中,然後對變量操作,操作後再將變量寫回主內存.不能直接操作主內存中的變量,工作內存中存儲着主內存中變量的副本拷貝,工作內存是每個變量的私有區域,因此不同線程間無法訪問對方的工作內存,線程間的通信(傳值)必須通過主內存來完成.

第二章 JMM中的主內存和工作內存 

2.1 主內存與工作內存介紹

JMM中的主內存: 

JMM中的工作內存:

注意第一條,即使兩個線程執行的是同一個方法,它們也會在自身的工作內存中創建當前線程的工作變量.

2.2 JMM與JVM的區別

JMM與JVM內存區域劃分是不同的概念層次.

JMM描述的是一組規則,通過這組規則,控制程序中各個變量在共享數據區域和私有數據區域的訪問方式,jmm是圍繞原子性,有序性,可見性展開的.

相似點:存在共享區域和私有區域.

JMM中主內存是共享數據區域,應該包括堆和方法區,而工作內存數據線程私有數據區域某個程度上講,應該包括程序計數器,虛擬機棧,以及本地方法棧.

對於一個實例中成員方法而言,如果方法中包含的本地變量是基本數據類型的(那8種),這些本地變量將直接存儲在工作內存的棧幀結構中.

倘若本地變量是引用類型的,該變量的引用會存儲在工作內存的棧幀中,而對象實例則存儲在主內存.即我們前面說的共享區域堆當中.

對於b實例對象的成員變量,static變量,類信息均會被存儲在主內存的堆區.

主內存的實例對象可以被多線程共享,倘若兩個線程調用了同一個對象的同一個方法,兩個線程會將要操作的數據拷貝一份到自己的工作內存中,對數據操作完成後刷新到主內存中.

2.3 可見性問題

把數據從內存加載到緩存寄存器,運算結束,寫回主內存.

但是當線程共享變量的時候,情況就變得非常複雜了,如果處理器對某個變量進行了修改,可能只是體現在該內核的緩存裏,而運行在其它內核上的線程可能加載的是舊狀態,這很可能導致一致性的問題,從理論上來說,多線程共享引入了複雜的數據依賴性問題,不管處理器,編譯器怎麼做重排序都必須尊重數據依賴型的要求,否則就打破了數據的正確性.這就是jmm所要解決的問題.

2.4 指令重排序需要滿足的條件

在執行程序的時候,爲了提高性能,處理器和編譯器常常會對指令進行重排序,需要滿足以下條件:

以上兩點可以歸結爲一點:

jmm內部的實現通常是依賴於所謂的內存屏障,通過禁止某些重排序的方式提供內存可見性保證,也就是實現了各種happens-before的規則,更多的複雜度在於,需要儘量確保各種編譯器,各種體系結構的處理器,能夠提供一致的行爲.

在jmm中,如果一個操作執行的結果需要對另一個操作可見,那麼這兩個操作必須滿足happens-before的關係.

2.5 happens-before原則

happens-before原則非常重要,它是判斷數據是否存在競爭,線程是否安全的主要依據,依靠此原則我們變能解決在併發環境下兩個操作存在衝突的問題.

我們分析以下代碼,假設線程A happens-before線程B,即線程A先於線程B發生,可以確定線程B操作後j是1是確定的,如果它們不存在happens-before原則,那麼j=1就不一定能夠成立.

happens-before的八大原則:

2.5.1 例1

我們約定線程A執行write操作,線程B執行read操作,且線程A優先於線程B去執行,那麼線程B獲得的結果是什麼呢?

可以看到5,6,7,8這四個規則是可以被忽略的,因爲與這段代碼毫無關係.

兩個線程,規則1不適用,沒有鎖,規則2不適用,規則3肯定也不適用,沒使用volatile,規則4也不適合.

所以無法通過happens-before原則推導出A happens-before B,不知道B什麼時候執行.

所以這段代碼不是線程安全的.

我們只需滿足2,3規則中的一個即可保證線程安全,即加同步鎖或者volatile.

2.5.2 volatile 

volatile:JVM提供的輕量級同步機制

volatile的作用:

volatile的可見性:我們必須意識到volatile修飾的變量總是對所有線程立即可見的,對volatile的所有寫操作總是能立即反映到其它線程中,但是對於volatile運算操作在多線程環境中並不保證安全性.

volatile變量爲何立即可見?

volatile如何禁止重排優化?

首先要了解內存屏障:

 volatile正是通過內存屏障實現其在內存中的語義即可見性和禁止重排優化.

volatile和synchronized的區別:

2.5.3 例2

value變量的任何改變都會反映到線程中,但是若有多條線程同時訪問increase方法,就會出現線程安全問題,畢竟value++操作並不具備原子性.

value++操作是先讀取值,然後再寫回一個新值,相當於原來的值加1分兩步來完成.如果第二個線程在第一個線程讀取舊值寫回新值之間,讀取value的值,那麼第二個線程就會與第一個線程一起看到同一個值.並執行相同的加1操作,引發了線程安全問題.所以對於increase必須用synchronized修飾,以便保證線程安全,需要補充並且注意的是,synchronized關鍵字解決的是執行控制的問題,它會阻止其它線程獲取當前對象的監控鎖,這就使被synchronized保護的代碼塊無法被其它線程訪問,也就無法併發執行.

修改線程安全:

synchronized會創建一個內存屏障指令,其保證了所有CPU結果都會直接刷到主存中,從而保證了操作的內存可見性.也保證了順序執行.

2.5.4 例3

由於對boolen的修改屬於原子操作,因此可以使volatile修飾該變量,使其修改對其它線程立即可見,從而達到線程安全的目的.

2.5.5 例4

面試時經常要寫的所謂實現線程安全的單例寫法,通過引入synchronized代碼塊試圖解決多線程請求單例時重複創建單例的隱患.下面的代碼在多線程環境下依然會有隱患.

原因:

new singleton()創建時會有三步

並可以有如下的重排序優化.這樣就可能導致getInstance返回null,一個線程走到了第二步,memory還是空,另一個線程判斷instance不是null,直接返回memory,造成錯誤.

解決方法如下,使用volatile使instance禁止指令進行重排序即可,即2,和3不能顛倒過來.

 

第三章 CAS(Compare and Swap)

像synchronized這種獨佔鎖屬於悲觀鎖,悲觀鎖始終假定,因此會屏蔽一切可能違反數據完整性的操作,除此之外,還有樂觀鎖,它假定不會發生併發衝突,因此只在提交操作時檢查是否違反數據完整性,如果提交失敗則會進行重試,而樂觀鎖最常見的就是CAS.

CAS是一種高效實現線程安全性的方法.

CAS思想:

包含三個操作數-內存位置(V),預期原值(A)和新值(B)

將內存位置的值與預期原值進行比較,如果相匹配則處理器會自動將內存位置的值更新爲新值,否則處理器不做任何操作,這裏內存位置的值V即主內存的值.

舉個例子,當一個線程需要修改共享變量的值,完成這個操作先取出共享變量的值賦給A,基於A的基礎進行計算得到新值B,執行完畢需要更新共享變量的值的時候,我們調用CAS方法去更新共享變量的值.

看一下之前的例子:

查看其字節碼:

可以看到value++被拆分成了如下的指令,首先需要getfield拿到原始的value,也就是從我們的主內存中將value加載進當前線程的工作內存中,執行iadd進行+1的操作,之後再執行putfield把累加後的值寫回我們的主內存當中.

通過volatile修飾的變量,可以保證線程之間的可見性,同時也不允許JVM對它們進行重排序.但是並不能保證這三個指令的原子執行,在多線程併發下,無法做到線程安全.

該如何解決呢?

在add前加入synchronized操作即可解決.

但是能否儘量提升性能呢?

3.1 AtomicInteger

可以使用AtomicInteger來滿足需求,其位於concurrent.atomic包中,

從AtomicInteger的內部屬性可以看出,它依賴於unsafe提供的一些底層能力,進行底層操作,以volatile的value字段記錄數值以保證可見性.

其中的getAndIncrement方法可以解決上面value++的不安全性.此方法會利用value字段的地址偏移直接完成操作.

點進getAndIncrement,因爲需要返回數值多以需要添加失敗重試的邏輯.

 而向返回布爾類型的,因爲其返回值表現得就是成功與否,所以不需要進行重試.

Unsafe裏的這些方法,如compareAndSetInt 則實現了CAS.

 

CAS多數情況下對開發者來說是透明的,我們更多的是使用併發包間接享受到lock-free機制在擴展性上的好處.

CAS缺點:

 

 

 

 

 

 

 

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章