前言java
Java代碼在編譯後會變成Java字節碼,字節碼被類加載器加載到JVM裏,JVM執行字節編程
碼,最終須要轉化爲彙編指令在CPU上執行,Java中所使用的併發機制依賴於JVM的實現和緩存
CPU的指令。多線程
2.1 volatile的應用併發
在多線程併發編程中synchronized和volatile都扮演着重要的角色,volatile是輕量級的編程語言
synchronized,它在多處理器開發中保證了共享變量的「可見性」。可見性的意思是當一個線程工具
修改一個共享變量時,另一個線程能讀到這個修改的值。若是volatile變量修飾符使用恰當線程
的話,它比synchronized的使用和執行成本更低,由於它不會引發線程上下文的切換和調度。3d
1.volatile的定義與實現原理code
Java語言規範第3版中對volatile的定義以下:Java編程語言容許線程訪問共享變量,爲了
確保共享變量能被準確和一致地更新,線程應該確保經過排他鎖單獨得到這個變量。Java語言
提供了volatile,在某些狀況下比鎖要更加方便。若是一個字段被聲明成volatile,Java線程內存
模型確保全部線程看到這個變量的值是一致的。
volatile是如何來保證可見性的呢?讓咱們在X86處理器下經過工具獲取JIT編譯器生成的
彙編指令來查看對volatile進行寫操做時,CPU會作什麼事情。
Java代碼以下。
instance = new Singleton(); // instance是volatile變量
轉變成彙編代碼,以下。
0x01a3de1d: movb $0×0,0×1104800(%esi);0x01a3de24: lock addl $0×0,(%esp);
有volatile變量修飾的共享變量進行寫操做的時候會多出第二行彙編代碼,經過查IA-32架
構軟件開發者手冊可知,Lock前綴的指令在多核處理器下會引起了兩件事情。
1)將當前處理器緩存行的數據寫回到系統內存。
2)這個寫回內存的操做會使在其餘CPU裏緩存了該內存地址的數據無效。
爲了提升處理速度,處理器不直接和內存進行通訊,而是先將系統內存的數據讀到內部
緩存(L1,L2或其餘)後再進行操做,但操做完不知道什麼時候會寫到內存。若是對聲明瞭volatile的
變量進行寫操做,JVM就會向處理器發送一條Lock前綴的指令,將這個變量所在緩存行的數據
寫回到系統內存。可是,就算寫回到內存,若是其餘處理器緩存的值仍是舊的,再執行計算操
做就會有問題。因此,在多處理器下,爲了保證各個處理器的緩存是一致的,就會實現緩存一
致性協議,每一個處理器經過嗅探在總線上傳播的數據來檢查本身緩存的值是否是過時了,當
處理器發現本身緩存行對應的內存地址被修改,就會將當前處理器的緩存行設置成無效狀
態,當處理器對這個數據進行修改操做的時候,會從新從系統內存中把數據讀處處理器緩存
裏。
下面來具體講解volatile的兩條實現原則。
1)Lock前綴指令會引發處理器緩存回寫到內存。Lock前綴指令致使在執行指令期間,聲
言處理器的LOCK#信號。在多處理器環境中,LOCK#信號確保在聲言該信號期間,處理器能夠
獨佔任何共享內存(由於它會鎖住總線,致使其餘CPU不能訪問總線,不能訪問總線就意味着不能訪問系統內
存)。可是,在最近的處理器裏,LOCK#信號通常不鎖總線,而是鎖緩存,畢
竟鎖總線開銷的比較大。在8.1.4節有詳細說明鎖定操做對處理器緩存的影響,對於Intel486和
Pentium處理器,在鎖操做時,老是在總線上聲言LOCK#信號。但在P6和目前的處理器中,若是
訪問的內存區域已經緩存在處理器內部,則不會聲言LOCK#信號。相反,它會鎖定這塊內存區
域的緩存並回寫到內存,並使用緩存一致性機制來確保修改的原子性,此操做被稱爲「緩存鎖
定」,緩存一致性機制會阻止同時修改由兩個以上處理器緩存的內存區域數據(說的明白點就
是緩存一致性機制保證一個緩存在鎖定時,其餘有相同緩存的處理器是不能修改的)。
2)一個處理器的緩存回寫到內存會致使其餘處理器的緩存無效。IA-32處理器和Intel 64處
理器使用MESI(修改、獨佔、共享、無效)控制協議去維護內部緩存和其餘處理器緩存的一致
性。在多核處理器系統中進行操做的時候,IA-32和Intel 64處理器能嗅探其餘處理器訪問系統
內存和它們的內部緩存。處理器使用嗅探技術保證它的內部緩存、系統內存和其餘處理器的
緩存的數據在總線上保持一致。例如,在Pentium和P6 family處理器中,若是經過嗅探一個處理
器來檢測其餘處理器打算寫內存地址,而這個地址當前處於共享狀態,那麼正在嗅探的處理
器將使它的緩存行無效,在下次訪問相同內存地址時,強制執行緩存行填充。