15.深刻分析Volatile的實現原理html
===================java
15.深刻分析Volatile的實現原理linux
在多線程併發編程中synchronized和Volatile都扮演着重要的角色,Volatile是 輕量級的synchronized ,它在多處理器開發中保證了共享變量的「可見性」。可見性的意思是當一個線程修改一個共享變量時,另一個線程能讀到這個修改的值。c++
它在某些狀況下比synchronized的開銷更小,本文將深刻分析在硬件層面上Inter處理器是如何實現Volatile的,經過深刻分析能幫助咱們正確的使用Volatile變量。算法
術語sql |
英文單詞數據庫 |
描述編程 |
共享變量數組 |
在多個線程之間可以被共享的變量被稱爲共享變量。共享變量包括全部的實例變量,靜態變量和數組元素。他們都被存放在堆內存中,Volatile只做用於共享變量。緩存 |
|
內存屏障 |
Memory Barriers |
是一組處理器指令,用於實現對內存操做的順序限制。 |
緩衝行 |
Cache line |
緩存中能夠分配的最小存儲單位。處理器填寫緩存線時會加載整個緩存線,須要使用多個主內存讀週期。 |
原子操做 |
Atomic operations |
不可中斷的一個或一系列操做。 |
緩存行填充 |
cache line fill |
當處理器識別到從內存中讀取操做數是可緩存的,處理器讀取整個緩存行到適當的緩存(L1,L2,L3的或全部) |
緩存命中 |
cache hit |
若是進行高速緩存行填充操做的內存位置仍然是下次處理器訪問的地址時,處理器從緩存中讀取操做數,而不是從內存。 |
寫命中 |
write hit |
當處理器將操做數寫回到一個內存緩存的區域時,它首先會檢查這個緩存的內存地址是否在緩存行中,若是存在一個有效的緩存行,則處理器將這個操做數寫回到緩存,而不是寫回到內存,這個操做被稱爲寫命中。 |
寫缺失 |
write misses the cache |
一個有效的緩存行被寫入到不存在的內存區域。 |
Java語言規範第三版中對volatile的定義以下: java編程語言容許線程訪問共享變量,爲了確保共享變量能被準確和一致的更新,線程應該確保經過排他鎖單獨得到這個變量。Java語言提供了volatile,在某些狀況下比鎖更加方便。若是一個字段被聲明成volatile,java線程內存模型確保全部線程看到這個變量的值是一致的。
Volatile變量修飾符若是使用 恰當 的話,它比synchronized的 使用和執行成本會更低 ,由於它不會引發線程上下文的切換和調度。
那麼Volatile是如何來保證可見性的呢?在x86處理器下經過工具獲取JIT編譯器生成的彙編指令來看看對Volatile進行寫操做CPU會作什麼事情。
Java代碼: |
instance = new Singleton();//instance是volatile變量 |
彙編代碼: |
0x01a3de1d: movb $0x0,0x1104800(%esi); 0x01a3de24: lock addl $0x0,(%esp); |
有volatile變量修飾的共享變量進行寫操做的時候會多第二行彙編代碼,經過查IA-32架構軟件開發者手冊可知,lock前綴的指令在多核處理器下會引起了兩件事情。
處理器爲了提升處理速度,不直接和內存進行通信,而是先將系統內存的數據讀到內部緩存(L1,L2或其餘)後再進行操做,但操做完以後不知道什麼時候會寫到內存,若是對聲明瞭Volatile變量進行寫操做,JVM就會向處理器發送一條Lock前綴的指令,將這個變量所在緩存行的數據寫回到系統內存。可是就算寫回到內存,若是其餘處理器緩存的值仍是舊的,再執行計算操做就會有問題,因此在多處理器下,爲了保證各個處理器的緩存是一致的,就會實現緩存一致性協議,每一個處理器經過嗅探在總線上傳播的數據來檢查本身緩存的值是否是過時了,當處理器發現本身緩存行對應的內存地址被修改,就會將當前處理器的緩存行設置成無效狀態,當處理器要對這個數據進行修改操做的時候,會強制從新從系統內存裏把數據讀處處理器緩存裏。
這兩件事情在IA-32軟件開發者架構手冊的第三冊的多處理器管理章節(第八章)中有詳細闡述。
Lock前綴指令會引發處理器緩存回寫到內存 。Lock前綴指令致使在執行指令期間,聲言處理器的 LOCK# 信號。在多處理器環境中,LOCK# 信號確保在聲言該信號期間,處理器能夠獨佔使用任何共享內存。(由於它會鎖住總線,致使其餘CPU不能訪問總線,不能訪問總線就意味着不能訪問系統內存),可是在最近的處理器裏,LOCK#信號通常不鎖總線,而是鎖緩存,畢竟鎖總線開銷比較大。在8.1.4章節有詳細說明鎖定操做對處理器緩存的影響,對於Intel486和Pentium處理器,在鎖操做時,老是在總線上聲言LOCK#信號。但在P6和最近的處理器中,若是訪問的內存區域已經緩存在處理器內部,則不會聲言LOCK#信號。相反地,它會鎖定這塊內存區域的緩存並回寫到內存,並使用緩存一致性機制來確保修改的原子性,此操做被稱爲「緩存鎖定」,緩存一致性機制會阻止同時修改被兩個以上處理器緩存的內存區域數據 。
一個處理器的緩存回寫到內存會致使其餘處理器的緩存無效 。IA-32處理器和Intel 64處理器使用MESI(修改,獨佔,共享,無效)控制協議去維護內部緩存和其餘處理器緩存的一致性。在多核處理器系統中進行操做的時候,IA-32 和Intel 64處理器能嗅探其餘處理器訪問系統內存和它們的內部緩存。它們使用嗅探技術保證它的內部緩存,系統內存和其餘處理器的緩存的數據在總線上保持一致。例如在Pentium和P6 family處理器中,若是經過嗅探一個處理器來檢測其餘處理器打算寫內存地址,而這個地址當前處理共享狀態,那麼正在嗅探的處理器將無效它的緩存行,在下次訪問相同內存地址時,強制執行緩存行填充。
著名的Java併發編程大師Doug lea在JDK7的併發包裏新增一個隊列集合類LinkedTransferQueue,他在使用Volatile變量時,用一種追加字節的方式來優化隊列出隊和入隊的性能。
追加字節能優化性能?這種方式看起來很神奇,但若是深刻理解處理器架構就能理解其中的奧祕。讓咱們先來看看LinkedTransferQueue這個類,它使用一個內部類類型來定義隊列的頭隊列(Head)和尾節點(tail),而這個內部類PaddedAtomicReference相對於父類AtomicReference只作了一件事情,就將共享變量追加到64字節。咱們能夠來計算下,一個對象的引用佔4個字節,它追加了15個變量共佔60個字節,再加上父類的Value變量,一共64個字節。
/** head of the queue */
private transient final PaddedAtomicReference < QNode > head; /** tail of the queue */ private transient final PaddedAtomicReference < QNode > tail; static final class PaddedAtomicReference < T > extends AtomicReference < T > { // enough padding for 64bytes with 4byte refs Object p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, pa, pb, pc, pd, pe; PaddedAtomicReference(T r) { super(r); } } public class AtomicReference < V > implements java.io.Serializable { private volatile V value; //省略其餘代碼 }
爲何追加64字節可以提升併發編程的效率呢 ? 由於對於英特爾酷睿i7,酷睿, Atom和NetBurst, Core Solo和Pentium M處理器的L1,L2或L3緩存的高速緩存行是64個字節寬,不支持部分填充緩存行,這意味着若是隊列的頭節點和尾節點都不足64字節的話,處理器會將它們都讀到同一個高速緩存行中,在多處理器下每一個處理器都會緩存一樣的頭尾節點,當一個處理器試圖修改頭接點時會將整個緩存行鎖定,那麼在緩存一致性機制的做用下,會致使其餘處理器不能訪問本身高速緩存中的尾節點,而隊列的入隊和出隊操做是須要不停修改頭接點和尾節點,因此在多處理器的狀況下將會嚴重影響到隊列的入隊和出隊效率。Doug lea使用追加到64字節的方式來填滿高速緩衝區的緩存行,避免頭接點和尾節點加載到同一個緩存行,使得頭尾節點在修改時不會互相鎖定。
那麼是否是在使用Volatile變量時都應該追加到64字節呢?不是的。在兩種場景下不該該使用這種方式。第一: 緩存行非64字節寬的處理器 ,如P6系列和奔騰處理器,它們的L1和L2高速緩存行是32個字節寬。第二: 共享變量不會被頻繁的寫 。由於使用追加字節的方式須要處理器讀取更多的字節到高速緩衝區,這自己就會帶來必定的性能消耗,共享變量若是不被頻繁寫的話,鎖的概率也很是小,就不必經過追加字節的方式來避免相互鎖定。
原子(atom)本意是「不能被進一步分割的最小粒子」,而原子操做(atomic operation)意爲"不可被中斷的一個或一系列操做" 。在多處理器上實現原子操做就變得有點複雜。本文讓咱們一塊兒來聊一聊在Intel處理器和Java裏是如何實現原子操做的。
術語 | 英文 | 解釋 |
---|---|---|
緩存行 | Cache line | 緩存的最小操做單位 |
比較並交換 | Compare and Swap | CAS操做須要輸入兩個數值,一箇舊值(指望操做前的值)和一個新值,在操做期間先比較下舊值有沒有發生變化,若是沒有發生變化,才交換成新值,發生了變化則不交換。 |
CPU流水線 | CPU pipeline | CPU流水線的工做方式就象工業生產上的裝配流水線,在CPU中由5~6個不一樣功能的電路單元組成一條指令處理流水線,而後將一條X86指令分紅5~6步後再由這些電路單元分別執行,這樣就能實如今一個CPU時鐘週期完成一條指令,所以提升CPU的運算速度。 |
內存順序衝突 | Memory order violation | 內存順序衝突通常是由假共享引發,假共享是指多個CPU同時修改同一個緩存行的不一樣部分而引發其中一個CPU的操做無效,當出現這個內存順序衝突時,CPU必須清空流水線。 |
32位IA-32處理器使用基於對緩存加鎖或總線加鎖的方式來實現多處理器之間的原子操做。
首先處理器會自動保證基本的內存操做的原子性。處理器保證從系統內存當中讀取或者寫入一個字節是原子的,意思是當一個處理器讀取一個字節時,其餘處理器不能訪問這個字節的內存地址。奔騰6和最新的處理器能自動保證單處理器對同一個緩存行裏進行16/32/64位的操做是原子的,可是複雜的內存操做處理器不能自動保證其原子性,好比跨總線寬度,跨多個緩存行,跨頁表的訪問。可是處理器提供總線鎖定和緩存鎖定兩個機制來保證複雜內存操做的原子性。
第一個機制是經過總線鎖保證原子性。若是多個處理器同時對共享變量進行讀改寫(i++就是經典的讀改寫操做)操做,那麼共享變量就會被多個處理器同時進行操做,這樣讀改寫操做就不是原子的,操做完以後共享變量的值會和指望的不一致,舉個例子:若是i=1,咱們進行兩次i++操做,咱們指望的結果是3,可是有可能結果是2。以下圖
(例1)
緣由是有可能多個處理器同時從各自的緩存中讀取變量i,分別進行加一操做,而後分別寫入系統內存當中。那麼想要保證讀改寫共享變量的操做是原子的,就必須保證CPU1讀改寫共享變量的時候,CPU2不能操做緩存了該共享變量內存地址的緩存。
處理器使用總線鎖就是來解決這個問題的。所謂總線鎖就是使用處理器提供的一個LOCK#信號,當一個處理器在總線上輸出此信號時,其餘處理器的請求將被阻塞住,那麼該處理器能夠獨佔使用共享內存。
第二個機制是經過緩存鎖定保證原子性。在同一時刻咱們只需保證對某個內存地址的操做是原子性便可,但總線鎖定把CPU和內存之間通訊鎖住了,這使得鎖按期間,其餘處理器不能操做其餘內存地址的數據,因此總線鎖定的開銷比較大,最近的處理器在某些場合下使用緩存鎖定代替總線鎖定來進行優化。
頻繁使用的內存會緩存在處理器的L1,L2和L3高速緩存裏,那麼原子操做就能夠直接在處理器內部緩存中進行,並不須要聲明總線鎖,在奔騰6和最近的處理器中可使用「緩存鎖定」的方式來實現複雜的原子性。所謂「緩存鎖定」就是若是緩存在處理器緩存行中內存區域在LOCK操做期間被鎖定,當它執行鎖操做回寫內存時,處理器不在總線上聲言LOCK#信號,而是修改內部的內存地址,並容許它的緩存一致性機制來保證操做的原子性,由於緩存一致性機制會阻止同時修改被兩個以上處理器緩存的內存區域數據,當其餘處理器回寫已被鎖定的緩存行的數據時會起緩存行無效,在例1中,當CPU1修改緩存行中的i時使用緩存鎖定,那麼CPU2就不能同時緩存了i的緩存行。
可是有兩種狀況下處理器不會使用緩存鎖定。第一種狀況是:當操做的數據不能被緩存在處理器內部,或操做的數據跨多個緩存行(cache line),則處理器會調用總線鎖定。第二種狀況是:有些處理器不支持緩存鎖定。對於Inter486和奔騰處理器,就算鎖定的內存區域在處理器的緩存行中也會調用總線鎖定。
以上兩個機制咱們能夠經過Inter處理器提供了不少LOCK前綴的指令來實現。好比位測試和修改指令BTS,BTR,BTC,交換指令XADD,CMPXCHG和其餘一些操做數和邏輯指令,好比ADD(加),OR(或)等,被這些指令操做的內存區域就會加鎖,致使其餘處理器不能同時訪問它。
在java中能夠經過鎖和循環CAS的方式來實現原子操做。
JVM中的CAS操做正是利用了上一節中提到的處理器提供的CMPXCHG指令實現的。自旋CAS實現的基本思路就是循環進行CAS操做直到成功爲止,如下代碼實現了一個基於CAS線程安全的計數器方法safeCount和一個非線程安全的計數器count。
public class Counter { private AtomicInteger atomicI = new AtomicInteger(0); private int i = 0; public static void main(String[] args) { final Counter cas = new Counter(); List<Thread> ts = new ArrayList<Thread>(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); } /** * 使用CAS實現線程安全計數器 */ private void safeCount() { for (;;) { int i = atomicI.get(); boolean suc = atomicI.compareAndSet(i, ++i); if (suc) { break; } } } /** * 非線程安全計數器 */ private void count() { i++; } }
在java併發包中有一些併發框架也使用了自旋CAS的方式來實現原子操做,好比LinkedTransferQueue類的Xfer方法。CAS雖然很高效的解決原子操做,可是CAS仍然存在三大問題。ABA問題,循環時間長開銷大和只能保證一個共享變量的原子操做。
public boolean compareAndSet (V expectedReference,//預期引用 V newReference,//更新後的引用 int expectedStamp, //預期標誌 int newStamp) //更新後的標誌
循環時間長開銷大。自旋CAS若是長時間不成功,會給CPU帶來很是大的執行開銷。若是JVM能支持處理器提供的pause指令那麼效率會有必定的提高,pause指令有兩個做用,第一它能夠延遲流水線執行指令(de-pipeline),使CPU不會消耗過多的執行資源,延遲的時間取決於具體實現的版本,在一些處理器上延遲時間是零。第二它能夠避免在退出循環的時候因內存順序衝突(memory order violation)而引發CPU流水線被清空(CPU pipeline flush),從而提升CPU的執行效率。
只能保證一個共享變量的原子操做。當對一個共享變量執行操做時,咱們可使用循環CAS的方式來保證原子操做,可是對多個共享變量操做時,循環CAS就沒法保證操做的原子性,這個時候就能夠用鎖,或者有一個取巧的辦法,就是把多個共享變量合併成一個共享變量來操做。好比有兩個共享變量i=2,j=a,合併一下ij=2a,而後用CAS來操做ij。從Java1.5開始JDK提供了AtomicReference類來保證引用對象之間的原子性,你能夠把多個變量放在一個對象裏來進行CAS操做。
鎖機制保證了只有得到鎖的線程可以操做鎖定的內存區域。JVM內部實現了不少種鎖機制,有偏向鎖,輕量級鎖和互斥鎖,有意思的是除了偏向鎖,JVM實現鎖的方式都用到的循環CAS,當一個線程想進入同步塊的時候使用循環CAS的方式來獲取鎖,當它退出同步塊的時候使用循環CAS釋放鎖。詳細說明能夠參見文章Java SE1.6中的Synchronized。
方騰飛 ,花名清英,淘寶資深開發工程師,關注併發編程,目前在廣告技術部從事無線廣告聯盟的開發和設計工做。我的博客: http://ifeve.com 微博: http://weibo.com/kirals 歡迎經過個人微博進行技術交流。
http://www.infoq.com/cn/articles/atomic-operation
所謂原子操做,就是該操做毫不會在執行完畢前被任何其餘任務或事件打斷,也就說,它的最小的執行單位,不可能有比它更小的執行單位,所以這裏的原子實際是使用了物理學裏的物質微粒的概念。
原子操做須要硬件的支持,所以是架構相關的,其API和原子類型的定義都定義在內核源碼樹的include/asm/atomic.h文件中,它們都使用匯編語言實現,由於C語言並不能實現這樣的操做。
原子操做主要用於實現資源計數,不少引用計數(refcnt)就是經過原子操做實現的。原子類型定義以下:
typedef struct { volatile int counter; } atomic_t; |
volatile修飾字段告訴gcc不要對該類型的數據作優化處理,對它的訪問都是對內存的訪問,而不是對寄存器的訪問。
原子操做API包括:
atomic_read(atomic_t * v); |
該函數對原子類型的變量進行原子讀操做,它返回原子類型的變量v的值。
atomic_set(atomic_t * v, int i); |
該函數設置原子類型的變量v的值爲i。
void atomic_add(int i, atomic_t *v); |
該函數給原子類型的變量v增長值i。
atomic_sub(int i, atomic_t *v); |
該函數從原子類型的變量v中減去i。
int atomic_sub_and_test(int i, atomic_t *v); |
該函數從原子類型的變量v中減去i,並判斷結果是否爲0,若是爲0,返回真,不然返回假。
void atomic_inc(atomic_t *v); |
該函數對原子類型變量v原子地增長1。
void atomic_dec(atomic_t *v); |
該函數對原子類型的變量v原子地減1。
int atomic_dec_and_test(atomic_t *v); |
該函數對原子類型的變量v原子地減1,並判斷結果是否爲0,若是爲0,返回真,不然返回假。
int atomic_inc_and_test(atomic_t *v); |
該函數對原子類型的變量v原子地增長1,並判斷結果是否爲0,若是爲0,返回真,不然返回假。
int atomic_add_negative(int i, atomic_t *v); |
該函數對原子類型的變量v原子地增長I,並判斷結果是否爲負數,若是是,返回真,不然返回假。
int atomic_add_return(int i, atomic_t *v); |
該函數對原子類型的變量v原子地增長i,而且返回指向v的指針。
int atomic_sub_return(int i, atomic_t *v); |
該函數從原子類型的變量v中減去i,而且返回指向v的指針。
int atomic_inc_return(atomic_t * v); |
該函數對原子類型的變量v原子地增長1而且返回指向v的指針。
int atomic_dec_return(atomic_t * v); |
該函數對原子類型的變量v原子地減1而且返回指向v的指針。
原子操做一般用於實現資源的引用計數,在TCP/IP協議棧的IP碎片處理中,就使用了引用計數,碎片隊列結構struct ipq描述了一個IP碎片,字段refcnt就是引用計數器,它的類型爲atomic_t,當建立IP碎片時(在函數ip_frag_create中),使用atomic_set函數把它設置爲1,當引用該IP碎片時,就使用函數atomic_inc把引用計數加1。
當不須要引用該IP碎片時,就使用函數ipq_put來釋放該IP碎片,ipq_put使用函數atomic_dec_and_test把引用計數減1並判斷引用計數是否爲0,若是是就釋放IP碎片。函數ipq_kill把IP碎片從ipq隊列中刪除,並把該刪除的IP碎片的引用計數減1(經過使用函數atomic_dec實現)。
原子操做僅執行一次,在執行過程當中不會中斷也不會休眠;是最小的執行單元;鑑於原子操做這些特性,能夠利用它來解決競態問題。
日後其餘同步機制都是在原子操做的基礎上進行擴展的。
原子操做有整型原子操做、64位原子操做以及位原子操做。
1 整型原子操做
(Atomic Integer Operations)
要使用原子操做,須要定義一個原子變量,而後使用內核提供的接口對其進行原子操做。
整型原子變量結構以下
typedef struct
{
能夠看出整型原子變量實質上是一個32位整型變量。
整型原子變量操做接口,其實現方式與具體的架構有關。
#include <asm/atomic.h>
2 64位原子操做
(64-Bit Atomic Operations)
64位原子變量結構
typedef struct
{
64位原子變量操做接口與整型變量操做接口相似,只要將整型變量接口名稱的"atomic"改爲"atomic64"便可。
3 位原子操做
(Atomic Bitwise Operations)
位原子操做接口
#include < asm / bitops . h
>
14.java多線程編程底層原理剖析以及volatile原理
今天總結一下java多線程機制,以及volatile
首先,爲何須要多線程?
主要是由於計算機的運算能力遠遠大於I/O,通訊傳輸,還有數據庫訪問等操做。因此緩存出現了,從而提升了訪問速度。可是因爲會有多個緩存,以及數據讀寫問題,頗有可能會讀到髒數據,其實這也就是緩存的一致性。
另外爲了提升效率,處理器會對程序進行亂序執行優化,而對於虛擬機來講,就是指令重排序。意思就是說代碼順序與實際執行順序無關,實際執行順序是虛擬機根據先後依賴關係,結合運算器來決定的,可是結果是同樣的。
走入正題,先介紹一下java內存模型,內存模型主要用來屏蔽硬件與內存訪問的差別。
對於每個線程會有工做內存,多個線程共享一個主內存,例如對象實例就在主內存會多個線程共享,而引用這個對象的變量實際在每一個線程的工做內存,工做內存擁有主內存實例的副本拷貝,經過它來對實例進行,讀取與賦值都在工做內存,而且線程之間沒法讀取對方的變量,都是經過主內存作一個過渡做用。(這裏工做內存與主內存跟堆內存與棧內存不是一個概念,這是爲了好理解)
接下來工做內存與主內存怎麼進行交互?虛擬機定義了8種原子操做,包括lock(鎖定主內存的變量,使其被某一線程獨佔),unlock(同理),read(把一個主內存的變量傳遞到工做內存中,以便load),load(將從主內存傳遞的值傳遞到工做內存的變量副本中),store(將工做內存中變量副本傳遞到主內存中去,以便write),write(將工做內存傳遞過來的值賦到主內存中變量),use(將工做內存的值傳遞給執行引擎),assign(將執行引擎的值傳遞到工做內存),這8中操做能夠用來肯定你的訪問是否安全。
下面介紹一下volatile,常常被問到的一個關鍵字,他的做用主要有兩個,咱們一一說明:1 保證變量在各個線程的可見性,意思就是說這個變量的值一修改,其餘線程能夠當即得知。而一個普通變量須要先寫回主內存,而後其餘線程去讀取這個值。2:禁止指令重排序優化。然而它並不能保證原子性,以及運算的線程安全,下面代碼來解釋一下第一個特性。
咱們但願結果會是10000,然而並非,緣由就是a++這一條指令並非原子操做,volatile的確保證從主內存得到的數據是最正確的,可是當你運算的時候,其餘線程頗有可能會把一個值穿進去,致使值會變小。
那麼什麼狀況下用volatile呢?必定要明白,它的開銷必定會小與同步塊。下面就是使用的狀況,不符合這兩條就要用同步塊了。
1:運算結果並不依賴與當前值。
2:變量不須要與其餘變量參與不變約束。
一樣用代碼解釋一下。
我的理解就是a的值不依賴與如今在主內存a的實際值,無論a是幾,都變成1,而其餘線程也會當即受到通知,由於也沒有運算,也會直接變爲1.
接下來說一下指令重排序優化的東東,其實指令重排序對於單線程來講有利無害,反正最後的結果是同樣的,並且還提升了效率,可是對於多線程,可能會出現一些問題,而volatile修飾的變量,會在操做的時候,設置一個屏障,後面的操做,確定不會比這個提早。不然後面的操做先執行,從而提早影響其餘的線程。
好的,下面介紹幾個概念1:原子性 就像前面說的那8張操做就是,粒度小到多線程也不可能拆開它,而用synchronized,內部的東西其實就是一個組裝的「大原子」,可是記住volatile是不能夠的 2 可見性 意思就是線程修改了值以後會當即同步到主內存,而且獲取值會從主內存直接獲取,而非緩存,volatile和synchronized均可以保證 3有序性 意思是保證線程內部執行順序,volatile能夠保證禁止指令重排序,而synchronized,直接就鎖上了,因此它能解決幾乎全部同步問題,形成了濫用。
線程是cpu調度的基本單位,粒度比進程小,Thread的類不少方法是native,可能會爲了效率,然而同時可能會平臺相關,注意線程的優先級不太靠譜,覺得可能與平臺線程的優先級不同,形成衝突。再次補充一個線程狀態模型(本文章主要介紹java多線程模型,以及volatile,線程基礎再也不贅述)
阻塞狀態與掛起狀態的區別在於阻塞在等待一個排它鎖,而掛起是等待時間到,或者是喚醒。
更多細節請查看個人線程基礎的這篇博客,多謝你們支持
本博客知識來源於深刻理解java虛擬機,值得一看,強力 推薦,特別底層!!!
Java編程語言容許線程訪問共享變量,爲了確保共享變量能被準確和一致地更新,線程應該經過排它鎖單獨獲取這個變量
Java語言提供了Violatile來確保多處理開發中,共享變量的「可見性」,即當另一個線程修改一個共享變量時,另一個線程能讀到這個修改的值。它是輕量級的synchronized,不會引發線程上下文的切換和調度,執行開銷更小。
使用Violatile修飾的變量在彙編階段,會多出一條lock前綴指令,它在多核處理器下回引起兩件事情:
一般處理器和內存之間都有幾級緩存來提升處理速度,處理器先將內存中的數據讀取到內部緩存後再進行操做,可是對於緩存寫會內存的時機則沒法得知,所以在一個處理器裏修改的變量值,不必定能及時寫會緩存,這種變量修改對其餘處理器變得「不可見」了。可是,使用Volatile修飾的變量,在寫操做的時候,會強制將這個變量所在緩存行的數據寫回到內存中,但即便寫回到內存,其餘處理器也有可能使用內部的緩存數據,從而致使變量不一致,因此,在多處理器下,爲了保證各個處理器的緩存是一致的,就會實現緩存一致性協議,每一個處理器經過嗅探在總線上傳播的數據來檢查本身緩存的值是否是過時,若是過時,就會將該緩存行設置成無效狀態,下次要使用就會從新從內存中讀取。
在某些狀況下,經過將共享變量追加到64字節能夠優化其使用性能。
在JDK 7 的併發包裏,有一個隊列集合類LinkedTransferQueue,它在使用volatile變量時,用一種追加字節的方式來優化隊列出隊和入隊的性能。隊裏定義了兩個共享結點,頭結點和尾結點,都由使用了volatile的內部類定義,經過將兩個共享結點的字節數增長到64字節來優化效率,具體分析以下:
部分CPU的L一、L2或L3緩存的高速緩存行64字節寬,不支持部分填充緩存行
這意味着,若是隊列的頭結點和尾結點都不足64字節,處理器會將他們讀到同一個高速緩存行,在多處理器下每一個處理器都會緩存一樣的頭尾結點,當一個處理器試圖修改頭結點時,會將整個緩存行鎖定,那麼在緩存一致性的機制下,其餘處理器不能訪問本身高速緩存中的尾節點,而頭尾結點在隊列中都是會頻繁訪問的,所以會影響使用性能。而經過填充字節使頭尾結點加載到不一樣的緩存行,避免頭尾結點在修改時相互鎖定。
可是在如下兩種場景,不該該使用這種優化方式:
volatile是一種「輕量級的鎖」,它能保證鎖的可見性,但不能保證鎖的原子性。
以下面的例子
public class Test { public volatile int inc = 0; public void increase() { inc++; } public static void main(String[] args) { final Test test = new Test(); for(int i=0;i<10;i++){ new Thread(){ public void run() { for(int j=0;j<1000;j++) test.increase(); }; }.start(); } while(Thread.activeCount()>1) //保證前面的線程都執行完 Thread.yield(); System.out.println(test.inc); } }
上面程序輸出的結果是多少?不少人可能都覺得是10000,以爲對變量inc進行自增操做,因爲volatile保證了可見性,那麼在每一個線程中對inc自增完以後,在其餘線程中都能看到修改後的值啊,因此有10個線程分別進行了1000次操做,那麼最終inc的值應該是1000*10=10000。這裏面就有一個誤區了,volatile關鍵字能保證可見性沒有錯,可是上面的程序錯在沒能保證原子性。可見性只能保證每次讀取的是最新的值,可是volatile沒辦法保證對變量的操做的原子性。
因爲自增操做是不具有原子性的,它包括讀取變量的原始值、進行加1操做、寫入工做內存。那麼就是說自增操做的三個子操做可能會分割開執行,就有可能致使下面這種狀況出現:
假如某個時刻變量inc的值爲10,線程1對變量進行自增操做,線程1先讀取了變量inc的原始值,而後線程1被阻塞了;而後線程2對變量進行自增操做,線程2也去讀取變量inc的原始值,因爲線程1只是對變量inc進行讀取操做,而沒有對變量進行修改操做,因此不會致使線程2的工做內存中緩存變量inc的緩存行無效,因此線程2會直接去主存讀取inc的值,發現inc的值時10,而後進行加1操做,並把11寫入工做內存,最後寫入主存。
而後線程1接着進行加1操做,因爲已經讀取了inc的值,注意此時在線程1的工做內存中inc的值仍然爲10,因此線程1對inc進行加1操做後inc的值爲11,而後將11寫入工做內存,最後寫入主存。那麼兩個線程分別進行了一次自增操做後,inc只增長了1。
解釋到這裏,可能有朋友會有疑問,不對啊,前面不是保證一個變量在修改volatile變量時,會讓緩存行無效嗎?而後其餘線程去讀就會讀到新的值,可是要注意,線程1對變量進行讀取操做以後,被阻塞了的話,並無對inc值進行修改。而後雖然volatile能保證線程2對變量inc的值讀取是從內存中讀取的,可是線程1沒有進行修改,因此線程2根本就不會看到修改的值。
根源就在這裏,自增操做不是原子性操做,並且volatile也沒法保證對變量的任何操做都是原子性的。所以在使用Violatile修飾變量時,必定要保證對該變量的寫操做是原子性的,例如程序中的狀態變量,對該變量的修改不依賴於其當前值。
12.Java多線程-java.util.concurrent.atomic包原理解讀
三、實現的核心源碼:
/**
* Atomically increments by one the current value.
*
* @return the updated value
*/
public final int incrementAndGet() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return next;
}
}
public final boolean compareAndSet(int expect, int update) {
//使用unsafe的native方法,實現高效的硬件級別CAS
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
他比直接使用傳統的java鎖機制(阻塞的)有什麼好處?
最大的好處就是能夠避免多線程的優先級倒置和死鎖狀況的發生,固然高併發下的性能提高也是很重要的
四、CAS線程安全
說了半天,咱們要回歸到最原始的問題了:這樣怎麼實現線程安全呢?請你們本身先考慮一下這個問題,其實咱們在語言層面是沒有作任何同步的操做的,
你們也能夠看到源碼沒有任何鎖加在上面,可它爲何是線程安全的呢?這就是Atomic包下這些類的奧祕:語言層面不作處理,咱們將其交給硬件—CPU和內存,
利用CPU的多處理能力,實現硬件層面的阻塞,再加上volatile變量的特性便可實現基於原子操做的線程安全。因此說,CAS並非無阻塞,
只是阻塞並不是在語言、線程方面,而是在硬件層面,因此無疑這樣的操做會更快更高效!
5總結
雖然基於CAS的線程安全機制很好很高效,但要說的是,並不是全部線程安全均可以用這樣的方法來實現,這隻適合一些粒度比較小,
如計數器這樣的需求用起來纔有效,不然也不會有鎖的存在了
java編程語言容許線程訪問共享變量,爲了確保共享變量可以被準確和一致的更新,線程應該經過排他鎖得到這個變量。java提供了volatile,在某些狀況下比鎖更加方便。若是一個字段被聲明成volatile,java線程內存模型確保全部線程看到的這個變量的值是一致的。
使用命令得到彙編代碼
java -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly
Java HotSpot(TM) 64-Bit Server VM warning: PrintAssembly is enabled; turning on DebugNonSafepoints to gain additional output Loaded disassembler from /Library/Java/JavaVirtualMachines/jdk1.8.0_101.jdk/Contents/Home/jre/lib/hsdis-amd64.dylib Decoding compiled method 0x0000000110da4b50: Code: [Disassembling for mach='i386:x86-64'] [Entry Point] [Constants] # {method} {0x000000010f163000} 'hashCode' '()I' in 'java/lang/String' # [sp+0x40] (sp of caller) 0x0000000110da4cc0: mov 0x8(%rsi),%r10d 0x0000000110da4cc4: shl $0x3,%r10 0x0000000110da4cc8: cmp %rax,%r10 0x0000000110da4ccb: jne 0x0000000110ceae20 ; {runtime_call} 0x0000000110da4cd1: data32 data32 nopw 0x0(%rax,%rax,1) 0x0000000110da4cdc: data32 data32 xchg %ax,%ax [Verified Entry Point] 0x0000000110da4ce0: mov %eax,-0x14000(%rsp) 0x0000000110da4ce7: push %rbp 0x0000000110da4ce8: sub $0x30,%rsp ......
mac系統下使用此命令的前提是下載hsdis-amd64.dylib,並將其放入到jdk的jre下的lib目錄下
經過利用工具得到class文件的彙編代碼,會發現,標有volatile的變量在進行寫操做時,會在前面加上lock質量前綴。
而lock指令前綴會作以下兩件事
將當前處理器緩存行的數據寫回到內存。lock指令前綴在執行指令的期間,會產生一個lock信號,lock信號會保證在該信號期間會獨佔任何共享內存。lock信號通常不鎖總線,而是鎖緩存。由於鎖總線的開銷會很大。
將緩存行的數據寫回到內存的操做會使得其餘CPU緩存了該地址的數據無效。