簡單說說可見性和volatile

如下由寫在書上的筆記整理出來的,前一篇文章就再也不更新了(懶)java

以可見性的討論開始

可見性和硬件的關聯

計算機爲了高速訪問資源,對內存進行了必定的緩存,但緩存不必定能在各線程(處理器)之間相互通訊,所以在多線程上須要額外注意硬件帶來的可見性問題(可能會讀到髒數據),注意這裏只討論共享變量下的狀況緩存

可能致使的問題

處理器不直接與主內存執行讀寫操做(慢),而是經過寄存器/寫緩衝器/高速緩存/無效化隊列等部件執行,解決一個問題的同時會產生更多的問題,所以多線程下會致使如下問題安全

1.不可訪問:線程所共享的變量分配到寄存器中數據結構

2.不可同步:縣城所共享變量只更新到寫緩衝器中,未到達高速緩衝多線程

3.可同步,但需經過緩存一致性協議:總算寫入到高速緩存中,但其餘處理器把該更新通知的內容存入無效化隊列(x86並無該部件)併發

緩存一致性協議

先說療效:對於某個處理器,經過該協議,該協議可讀取其餘處理器的高速緩存工具

咱們稱一個處理器從自身緩存之外的其它存儲部件讀取的數據並更新到該處理器的高速緩存成爲緩存同步性能

所以緩存同步能使一個線程(處理器)讀取到其它線程(處理器)的共享變量,也就保障了可見性測試

緩存一致性協議就是一種緩存同步的方法優化

緩存一致性協議作到的事情:

1.沖刷緩存:處理器的更新最終寫入到(該處理器)的高速緩存或主內存

2.刷新緩存:處理器讀取時必須從其餘高速緩存/主內存對應的變量進行緩存同步

更爲具體的內部實現Chapter.11有提到

Java上的體現

volatile的做用之一即是寫進行沖刷緩存,讀進行沖刷緩存,以達到保證線程可見性的目的

(另外的做用是提示JIT不要亂優化,這是有序性上的問題,通常指令重排序由JIT引發)

輪到volatile

可見性的程度

前面提到volatile是調用緩存一致性在Java中的體現,保障了可見性,但可見到什麼程度?咱們須要注意這個問題

給出答案:咱們能保障的可見性僅是讀取到共享變量的相對新值,而並不是最新值

相對新值:線程更新值後,其餘線程能讀取到更新後的值

最新值:讀取變量的線程在讀取時其餘線程沒法對該值更新

舉例

我對書上P53的例子作出必定修改來講明上面的問題

假設a爲volatile int型共享變量,初值爲0,先開啓兩個線程

處理器0 處理器1
時刻0 null(a此時爲0) null(a此時爲0)
時刻1 a=1 無關操做
時刻2 無關操做 b=a+1

從時刻2來看,處理器1能看到此時a必然爲1,由於時刻1以後其餘處理器均能看到更新後的值,所以b=2

處理器0 處理器1
時刻0 null(a此時爲0) null(a此時爲0)
時刻1 a=1 無關操做
時刻2 a=2 b=a+1

但若是是時刻2中處理器1在讀取a的同時,處理器0也在更新,那麼此時a便沒法由可見性確認是1仍是2,所以b沒法肯定

題外話:Java還規定了子線程對建立前的父線程更新的可見性,所以時刻1的讀操做前不管是否有volatile均可得知a=0

題外話2:若是a僅爲int型,咱們只能確保其中的原子性,在表1的時刻2的處理器1看來a多是0或者1

題外話3:若是a爲long型,咱們甚至沒法確保原子性,在大數值時可能會產生一個不存在的數(區分高低32位)

解決重排序

volatile解決重排序除了軟件頂層提醒JIT的優化之外,還會對讀寫操做設置不一樣的內存屏障禁止存儲子系統重排序,有待更新

volatile的性能

從性能層面來講,volatile暴打內部鎖是沒問題的,緣由以下

1.沒有上下文切換的開銷

2.沒有鎖的申請

但和普通變量相比,它依然有所不足:

1.讀寫會沖刷/刷新緩存

2.變量確定不會暫存於寄存器,最多也就在L1

所以對於極爲頻繁的讀操做,仍是要打折扣的

(至於量化測試,待我學有所成再說吧)

做用總結

1.可見性,我已經(盡我所能)說明了

2.有序性,因爲Java中其它單獨控制有序性的工具沒有別的(final我會後續補充)

3.極爲有限的原子性,volatile在規定上保證longdouble的讀寫原子性,以及任意操做只與自身相關的原子性

volatile的讀和寫

讀:做爲讀的使用,咱們是能夠放心的,由於它註定只涉及自身相關,前面提到了,原子性也是能夠保證的

寫:寫僅當不涉及共享變量時才確保原子性。具體以volatile a爲例:

1.首先多個線程寫入不共享也會保證原子性,好比a=3,由於最後一步必成功(必保證單一寫的原子性,而3可認爲是immutable)

2.即便只涉及自身的運算也不必定線程安全,由於a自身即是共享變量,volatile並不保證賦值(涉及到讀共享變量和寫共享變量)必定具備原子性,好比a++即是線程不安全的

針對寫的不足,能夠採用以下方法

1.部分加鎖,可利用對讀直接返回,對寫加鎖進行處理,好比讀寫鎖的實現、單例模式的實現

2.CAS解決a++問題,而這即是Atomic類的實現思路

volatile的使用場合

1.做爲某個通知變量,只讀並輸出

2.部分代替鎖,對於建立新的對象,如volatile Map map = new HashMap(),該操做會分爲3個步驟,分配HashMap.class所需的空間、初始化引用對象、將對象引用寫入變量map。注意到前面兩個步驟依然是隻涉及局部變量,而最後的寫操做也必然保證原子性,所以該賦值是原子性的。假若設計一個類封裝許多變量,讀是併發的,但寫仿照該方式來賦值,那也無需任何加鎖

3.做爲鎖的部分優化,好比前面提到的讀寫鎖,對讀併發,對寫獨佔,雖然不足是有的,但仍是比鎖厲害

final的多線程用法

在語義上,volatilefinal是不可共存的,所以final在設計上也須要線程安全的某種保障,使人驚異的是它具備有序性卻沒有可見性和原子性,這種設計和安全發佈有必定關聯

安全發佈

以前曾經遇到private的逸出操做,深感本身菜到不行,由於這是常見的getter暴露對象的作法,但本篇重點不在這裏,相關的內容放到之後總結

這裏要提的是初始化安全問題,new的操做涉及三個步驟,1分配空間2初始化3引用寫入,但重排序會致使2和3的步驟不一致,可能發佈後對象內某個變量依然沒有初始化完畢(或者不可見)

final就保證了引用寫入前變量必然初始化完畢,這裏須要注意的是,final不保證可見性但必須保證有序性,我的認爲緣由以下:

1.對於單一線程來講,有序性是無需討論的,而多線程意義上,判斷一個對象是否初始化想必是使用obj == null來判斷,即便它不可見,但也不影響當其它線程可見時該final對象必然初始化完畢這個結論,更況且final的意義就在於發佈,所以是否可見已經無所謂了

2.若是不保證有序性,其它線程無從判斷是否徹底初始化完畢,好比上面的例子,雖然只是部分初始化,但它確實不是null,線程安全沒法保證

3.那能不能只保證可見但不保證有序,我認爲不能,起碼Java沒這操做

題外話,static能保證讀線程必然讀到初始值,也算是一種有限的可見性,該初始化可能致使上下文切換

CAS

CAS可認爲是硬件鎖,能實現C和S的原子性(由硬件大哥保證),通常搭配volatile使用

我的感悟是其特色是不阻塞,一般用於單一可變變量的判斷,好比如何在多線程下僅多開啓一個線程?你能夠用AtomicBooleantruefalse的CAS來輕鬆實現(原子性保證只有一個線程成功,其他線程均告失敗但毫不阻塞),注意這個操做僅靠bool是沒法完成的,由於原子(判)+原子(寫)≠原子

喜聞樂見的ABA可用相似時間戳的方法解決(其中ABA在數據結構上引起的問題挺有意思的,隨便搜搜就有),MySQL也有相似操做(樂觀鎖),不寫了bye

相關文章
相關標籤/搜索