那麼當寫兩條線程Thread-A與Threab-B同時操做主存中的一個volatile變量i時,Thread-A寫了變量i,那麼:java
Thread-A發出LOCK#指令編程
Thread-B讀取變量i,那麼:緩存
由此能夠看出,volatile關鍵字的讀和普通變量的讀取相比基本沒差異,差異主要仍是在變量的寫操做上。安全
爲何static volatile int i = 0; i++;不保證線程安全?多線程
由於i++並非一個原子操做(這是由i++自己特質決定的),它包含了三步(實際上對應的機器碼步驟更多,可是這裏分解爲三步已經足夠說明問題):架構
一、獲取i
二、i自增
三、回寫i併發
A、B兩個線程同時自增i
因爲volatile可見性,所以步驟1兩條線程必定拿到的是最新的i,也就是相同的i
可是從第2步開始就有問題了,有可能出現的場景是線程A自增了i並回寫,可是線程B此時已經拿到了i,不會再去拿線程A回寫的i,所以對原值進行了一次自增並回寫
這就致使了線程非安全,也就是你說的多線程技術器結果不對jvm
若是線程A對i進行自增了之後cpu緩存不是應該通知其餘緩存,而且從新load i麼?高併發
拿的前提是讀,問題是,線程A對i進行了自增,線程B已經拿到了i並不存在須要再次讀取i的場景,固然是不會從新load i這個值的。性能
ps:也就是線程B的緩存行內容的確會失效。可是此時線程B中i的值已經運行在加法指令中,不存在須要再次從緩存行讀取i的場景。
volatile是「輕量級」synchronized,保證了共享變量的「可見性」(JMM確保全部線程看到這個變量的值是一致的),當CPU寫數據時,若是發現操做的變量是共享變量,即在其餘CPU中也存在該變量的副本,會發出信號通知其餘CPU將該變量的緩存行置爲無效狀態而且鎖住緩存行,所以當其餘CPU須要讀取這個變量時,要等鎖釋放,並發現本身緩存行是無效的,那麼它就會從內存從新讀取。
volatile是「輕量級」synchronized,保證了共享變量的「可見性」(JMM確保全部線程看到這個變量的值是一致的),使用和執行成本比synchronized低,由於它不會引發線程上下文切換和調度。
工做內存Work Memory其實就是對CPU寄存器和高速緩存的抽象,或者說每一個線程的工做內存也能夠簡單理解爲CPU寄存器和高速緩存。
volatile做用:
1.鎖總線,其它CPU對內存的讀寫請求都會被阻塞,直到鎖釋放,不過實際後來的處理器都採用鎖緩存替代鎖總線,由於鎖總線的開銷比較大,鎖總線期間其餘CPU無法訪問內存
2.lock後的寫操做會回寫已修改的數據,同時讓其它CPU相關緩存行失效,從而從新從主存中加載最新的數據
3.不是內存屏障卻能完成相似內存屏障的功能,阻止屏障兩遍的指令重排序
volatile只能保證對單次讀/寫的原子性。由於long和double兩種數據類型的操做可分爲高32位和低32位兩部分,所以普通的long或double類型讀/寫可能不是原子的。所以,鼓勵你們將共享的long和double變量設置爲volatile類型,這樣能保證任何狀況下對long和double的單次讀/寫操做都具備原子性。
隊列集合類LinkedTransferQueue,在使用volatile變量時,追加64字節的方式來優化隊列出隊和入隊的性能。
追加字節能優化性能?這種方式看起來很神奇,但若是深刻理解處理器架構就能理解其中的奧祕。讓咱們先來看看LinkedTransferQueue這個類,它使用一個內部類類型來定義隊列的頭節點(head)和尾節點(tail),而這個內部類PaddedAtomicReference相對於父類AtomicReference只作了一件事情,就是將共享變量追加到64字節。咱們能夠來計算下,一個對象的引用佔4個字節,它追加了15個變量(共佔60個字節),再加上父類的value變量,一共64個字節。
爲何追加64字節可以提升併發編程的效率呢?由於對於英特爾酷睿i七、酷睿、Atom和NetBurst,以及Core Solo和Pentium M處理器的L一、L2或L3緩存的高速緩存行是64個字節寬,不支持部分填充緩存行(處理器支持也能夠),這意味着,若是隊列的頭節點和尾節點都不足64字節的話,處理器會將它們都讀到同一個高速緩存行中,在多處理器下每一個處理器都會緩存一樣的頭、尾節點,當一個處理器試圖修改頭節點時,會將整個緩存行鎖定,那麼在緩存一致性機制的做用下,會致使其餘處理器不能訪問本身高速緩存中的尾節點,而隊列的入隊和出隊操做則須要不停修改頭節點和尾節點,所以在多處理器的狀況下將會嚴重影響到隊列的入隊和出隊效率。
Doug lea使用追加到64字節的方式來填滿高速緩衝區的緩存行,避免頭節點和尾節點加載到同一個緩存行,使頭、尾節點在修改時不會互相鎖定。
那麼是否是在使用volatile變量時都應該追加到64字節呢?不是的。在兩種場景下不該該使用這種方式。
緩存行非64字節寬的處理器。如P6系列和奔騰處理器,它們的L1和L2高速緩存行是32個字節寬。
共享變量不會被頻繁地寫。由於使用追加字節的方式須要處理器讀取更多的字節到高速緩衝區,這自己就會帶來必定的性能消耗,若是共享變量不被頻繁寫的話,鎖的概率也很是小,就不必經過追加字節的方式來避免相互鎖定。
volatile關鍵字使用的是Lock指令,volatile的做用取決於Lock指令。CAS不是保證原子的更新,而是使用死循環保證更新成功時候只有一個線程更新,不包括主工做內存的同步。 CAS配合volatile既保證了只有一個線程更新又保證了多個線程更新得到的是最新的值互不影響。
volatile的變量在進行寫操做時,會在前面加上lock質量前綴。
Lock前綴,Lock不是一種內存屏障,可是它能完成相似內存屏障的功能。Lock會對CPU總線和高速緩存加鎖,能夠理解爲CPU指令級的一種鎖。
而Lock前綴是這樣實現的
它先對總線/緩存加鎖,而後執行後面的指令,最後釋放鎖後會把高速緩存中的髒數據所有刷新回主內存。
在Lock鎖住總線的時候,其餘CPU的讀寫請求都會被阻塞,直到鎖釋放。Lock後的寫操做會讓其餘CPU相關的cache失效,從而重新從內存加載最新的數據,這個是經過緩存一致性協議作的。
lock前綴指令至關於一個內存屏障(也稱內存柵欄)(既不是Lock中使用了內存屏障,也不是內存屏障使用了Lock指令),內存屏障主要提供3個功能:
內存屏障是CPU指令。若是你的字段是volatile,Java內存模型將在寫操做後插入一個寫屏障指令,在讀操做前插入一個讀屏障指令。
下面是基於保守策略的JMM內存屏障插入策略:
在每一個volatile寫操做的前面插入一個StoreStore屏障。
在每一個volatile寫操做的後面插入一個StoreLoad屏障。
在每一個volatile讀操做的前面插入一個LoadLoad屏障。
在每一個volatile讀操做的後面插入一個LoadStore屏障。
內存屏障,又稱內存柵欄,是一組處理器指令,用於實現對內存操做的順序限制。
內存屏障能夠被分爲如下幾種類型
LoadLoad屏障:對於這樣的語句Load1; LoadLoad; Load2,在Load2及後續讀取操做要讀取的數據被訪問前,保證Load1要讀取的數據被讀取完畢。
StoreStore屏障:對於這樣的語句Store1; StoreStore; Store2,在Store2及後續寫入操做執行前,保證Store1的寫入操做對其它處理器可見。
LoadStore屏障:對於這樣的語句Load1; LoadStore; Store2,在Store2及後續寫入操做被刷出前,保證Load1要讀取的數據被讀取完畢。
StoreLoad屏障:對於這樣的語句Store1; StoreLoad; Load2,在Load2及後續全部讀取操做執行前,保證Store1的寫入對全部處理器可見。它的開銷是四種屏障中最大的。 在大多數處理器的實現中,這個屏障是個萬能屏障,兼具其它三種內存屏障的功能。
StoreLoad Barriers是一個「全能型」的屏障,它同時具備其餘3個屏障的效果。
在每一個volatile寫操做前插入StoreStore屏障(這個屏障先後的2個Store指令不能交換順序),在寫操做後插入StoreLoad屏障(這個屏障先後的2個Store Load指令不能交換順序);
在每一個volatile讀操做前插入LoadLoad屏障(這個屏障先後的2個Load指令不能交換順序),在讀操做後插入LoadStore屏障(這個屏障先後的2個Load Store指令不能交換順序);
Java經過幾種原子操做完成工做內存和主內存的交互:
lock:做用於主內存,鎖住主內存主變量。
unlock:做用於主內存,解鎖主內存主變量。
read:做用主內存,主內存傳遞到工做內存。
load:做用於工做內存,主內存傳遞來的值賦給工做內存工做變量。
use:做用工做內存,工做內存工做變量值傳給執行引擎。
assign:做用工做內存,引擎的結果值賦值給工做內存工做變量。
store:做用於工做內存的變量,工做內存工做變量傳送到主內存中。
write:做用於主內存的變量,工做內存傳來工做變量賦值給主內存主變量。‘
’
read and load 從主存複製變量到當前工做內存
use and assign 執行代碼,改變共享變量值
store and write 用工做內存數據刷新主存相關內容
其中use and assign 能夠屢次出現
可是這一些操做並非原子性,也就是在read load以後,若是主內存count變量發生修改以後,線程工做內存中的值因爲已經加載,不會產生對應的變化,因此計算出來的結果會和預期不同.