14.深入分析Volatile的實現原理
13.java多線程編程底層原理剖析以及volatile原理
12.Java中Volatile底層原理與應用
11.volatile底層實現原理
===================
14.深入分析Volatile的實現原理
引言
在多線程並發編程中synchronized和Volatile都扮演着重要的角色,Volatile是 輕量級的synchronized ,它在多處理器開發中保證了共享變量的“可見性”。
可見性的意思是當一個線程修改一個共享變量時,另外一個線程能讀到這個修改的值。
它在某些情況下比synchronized的開銷更小,本文將深入分析在硬件層面上Inter處理器是如何實現Volatile的,通過深入分析能幫助我們正確的使用Volatile變量。
- public class VolatileTest extends Thread{
- public static volatile int a=0;
- public void run() {
- a++;
- }
- public static void main(String[] args) {
- VolatileTest array[]=new VolatileTest[10000];
- for (int i = 0; i < array.length; i++) {
- array[i]=new VolatileTest();
- array[i].start();
- }
- System.out.println(VolatileTest.a);
- }
- }
術語定義
術語 |
英文單詞 |
描述 |
共享變量 |
在多個線程之間能夠被共享的變量被稱為共享變量。共享變量包括所有的實例變量,靜態變量和數組元素。他們都被存放在堆內存中,Volatile只作用於共享變量。 |
|
內存屏障 |
Memory Barriers |
是一組處理器指令,用於實現對內存操作的順序限制。 |
緩沖行 |
Cache line |
緩存中可以分配的最小存儲單位。處理器填寫緩存線時會加載整個緩存線,需要使用多個主內存讀周期。 |
原子操作 |
Atomic operations |
不可中斷的一個或一系列操作。 |
緩存行填充 |
cache line fill |
當處理器識別到從內存中讀取操作數是可緩存的,處理器讀取整個緩存行到適當的緩存(L1,L2,L3的或所有) |
緩存命中 |
cache hit |
如果進行高速緩存行填充操作的內存位置仍然是下次處理器訪問的地址時,處理器從緩存中讀取操作數,而不是從內存。 |
寫命中 |
write hit |
當處理器將操作數寫回到一個內存緩存的區域時,它首先會檢查這個緩存的內存地址是否在緩存行中,如果存在一個有效的緩存行,則處理器將這個操作數寫回到緩存,而不是寫回到內存,這個操作被稱為寫命中。 |
寫缺失 |
write misses the cache |
一個有效的緩存行被寫入到不存在的內存區域。 |
Volatile的官方定義
Java語言規范第三版中對volatile的定義如下: java編程語言允許線程訪問共享變量,為了確保共享變量能被准確和一致的更新,線程應該確保通過排他鎖單獨獲得這個變量。Java語言提供了volatile,在某些情況下比鎖更加方便。如果一個字段被聲明成volatile,java線程內存模型確保所有線程看到這個變量的值是一致的。
為什么要使用Volatile
Volatile變量修飾符如果使用 恰當 的話,它比synchronized的 使用和執行成本會更低 ,因為它不會引起線程上下文的切換和調度。
Volatile的實現原理
那么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前綴的指令在多核處理器下會引發了兩件事情。
- 將當前處理器緩存行的數據會寫回到系統內存。
- 這個寫回內存的操作會引起在其他CPU里緩存了該內存地址的數據無效。
處理器為了提高處理速度,不直接和內存進行通訊,而是先將系統內存的數據讀到內部緩存(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處理器中,如果通過嗅探一個處理器來檢測其他處理器打算寫內存地址,而這個地址當前處理共享狀態,那么正在嗅探的處理器將無效它的緩存行,在下次訪問相同內存地址時,強制執行緩存行填充。
Volatile的使用優化
著名的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個字節寬。第二: 共享變量不會被頻繁的寫 。因為使用追加字節的方式需要處理器讀取更多的字節到高速緩沖區,這本身就會帶來一定的性能消耗,共享變量如果不被頻繁寫的話,鎖的幾率也非常小,就沒必要通過追加字節的方式來避免相互鎖定。
原子操作的實現原理
1. 引言
原子(atom)本意是“不能被進一步分割的最小粒子”,而原子操作(atomic operation)意為"不可被中斷的一個或一系列操作" 。在多處理器上實現原子操作就變得有點復雜。本文讓我們一起來聊一聊在Intel處理器和Java里是如何實現原子操作的。
2. 術語定義
術語 | 英文 | 解釋 |
---|---|---|
緩存行 | Cache line | 緩存的最小操作單位 |
比較並交換 | Compare and Swap | CAS操作需要輸入兩個數值,一個舊值(期望操作前的值)和一個新值,在操作期間先比較下舊值有沒有發生變化,如果沒有發生變化,才交換成新值,發生了變化則不交換。 |
CPU流水線 | CPU pipeline | CPU流水線的工作方式就象工業生產上的裝配流水線,在CPU中由5~6個不同功能的電路單元組成一條指令處理流水線,然后將一條X86指令分成5~6步后再由這些電路單元分別執行,這樣就能實現在一個CPU時鍾周期完成一條指令,因此提高CPU的運算速度。 |
內存順序沖突 | Memory order violation | 內存順序沖突一般是由假共享引起,假共享是指多個CPU同時修改同一個緩存行的不同部分而引起其中一個CPU的操作無效,當出現這個內存順序沖突時,CPU必須清空流水線。 |
3. 處理器如何實現原子操作
32位IA-32處理器使用基於對緩存加鎖或總線加鎖的方式來實現多處理器之間的原子操作。
3.1 處理器自動保證基本內存操作的原子性
首先處理器會自動保證基本的內存操作的原子性。處理器保證從系統內存當中讀取或者寫入一個字節是原子的,意思是當一個處理器讀取一個字節時,其他處理器不能訪問這個字節的內存地址。奔騰6和最新的處理器能自動保證單處理器對同一個緩存行里進行16/32/64位的操作是原子的,但是復雜的內存操作處理器不能自動保證其原子性,比如跨總線寬度,跨多個緩存行,跨頁表的訪問。但是處理器提供總線鎖定和緩存鎖定兩個機制來保證復雜內存操作的原子性。
3.2 使用總線鎖保證原子性
第一個機制是通過總線鎖保證原子性。如果多個處理器同時對共享變量進行讀改寫(i++就是經典的讀改寫操作)操作,那么共享變量就會被多個處理器同時進行操作,這樣讀改寫操作就不是原子的,操作完之后共享變量的值會和期望的不一致,舉個例子:如果i=1,我們進行兩次i++操作,我們期望的結果是3,但是有可能結果是2。如下圖
(例1)
原因是有可能多個處理器同時從各自的緩存中讀取變量i,分別進行加一操作,然后分別寫入系統內存當中。那么想要保證讀改寫共享變量的操作是原子的,就必須保證CPU1讀改寫共享變量的時候,CPU2不能操作緩存了該共享變量內存地址的緩存。
處理器使用總線鎖就是來解決這個問題的。所謂總線鎖就是使用處理器提供的一個LOCK#信號,當一個處理器在總線上輸出此信號時,其他處理器的請求將被阻塞住,那么該處理器可以獨占使用共享內存。
3.3 使用緩存鎖保證原子性
第二個機制是通過緩存鎖定保證原子性。在同一時刻我們只需保證對某個內存地址的操作是原子性即可,但總線鎖定把CPU和內存之間通信鎖住了,這使得鎖定期間,其他處理器不能操作其他內存地址的數據,所以總線鎖定的開銷比較大,最近的處理器在某些場合下使用緩存鎖定代替總線鎖定來進行優化。
頻繁使用的內存會緩存在處理器的L1,L2和L3高速緩存里,那么原子操作就可以直接在處理器內部緩存中進行,並不需要聲明總線鎖,在奔騰6和最近的處理器中可以使用“緩存鎖定”的方式來實現復雜的原子性。所謂“緩存鎖定”就是如果緩存在處理器緩存行中內存區域在LOCK操作期間被鎖定,當它執行鎖操作回寫內存時,處理器不在總線上聲言LOCK#信號,而是修改內部的內存地址,並允許它的緩存一致性機制來保證操作的原子性,因為緩存一致性機制會阻止同時修改被兩個以上處理器緩存的內存區域數據,當其他處理器回寫已被鎖定的緩存行的數據時會起緩存行無效,在例1中,當CPU1修改緩存行中的i時使用緩存鎖定,那么CPU2就不能同時緩存了i的緩存行。
但是有兩種情況下處理器不會使用緩存鎖定。第一種情況是:當操作的數據不能被緩存在處理器內部,或操作的數據跨多個緩存行(cache line),則處理器會調用總線鎖定。第二種情況是:有些處理器不支持緩存鎖定。對於Inter486和奔騰處理器,就算鎖定的內存區域在處理器的緩存行中也會調用總線鎖定。
以上兩個機制我們可以通過Inter處理器提供了很多LOCK前綴的指令來實現。比如位測試和修改指令BTS,BTR,BTC,交換指令XADD,CMPXCHG和其他一些操作數和邏輯指令,比如ADD(加),OR(或)等,被這些指令操作的內存區域就會加鎖,導致其他處理器不能同時訪問它。
4. JAVA如何實現原子操作
在java中可以通過鎖和循環CAS的方式來實現原子操作。
4.1 使用循環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問題,循環時間長開銷大和只能保證一個共享變量的原子操作。
- ABA問題。因為CAS需要在操作值的時候檢查下值有沒有發生變化,如果沒有發生變化則更新,但是如果一個值原來是A,變成了B,又變成了A,那么使用CAS進行檢查時會發現它的值沒有發生變化,但是實際上卻變化了。ABA問題的解決思路就是使用版本號。在變量前面追加上版本號,每次變量更新的時候把版本號加一,那么A-B-A 就會變成1A-2B-3A。
從Java1.5開始JDK的atomic包里提供了一個類AtomicStampedReference來解決ABA問題。這個類的compareAndSet方法作用是首先檢查當前引用是否等於預期引用,並且當前標志是否等於預期標志,如果全部相等,則以原子方式將該引用和該標志的值設置為給定的更新值。
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操作。
4.2 使用鎖機制實現原子操作
鎖機制保證了只有獲得鎖的線程能夠操作鎖定的內存區域。JVM內部實現了很多種鎖機制,有偏向鎖,輕量級鎖和互斥鎖,有意思的是除了偏向鎖,JVM實現鎖的方式都用到的循環CAS,當一個線程想進入同步塊的時候使用循環CAS的方式來獲取鎖,當它退出同步塊的時候使用循環CAS釋放鎖。詳細說明可以參見文章Java SE1.6中的Synchronized。
5. 參考資料
作者介紹
方騰飛 ,花名清英,淘寶資深開發工程師,關注並發編程,目前在廣告技術部從事無線廣告聯盟的開發和設計工作。個人博客: http://ifeve.com 微博: http://weibo.com/kirals 歡迎通過我的微博進行技術交流。
http://www.infoq.com/cn/articles/atomic-operation
Linux 原子操作
所謂原子操作,就是該操作絕不會在執行完畢前被任何其他任務或事件打斷,也就說,它的最小的執行單位,不可能有比它更小的執行單位,因此這里的原子實際是使用了物理學里的物質微粒的概念。
原子操作需要硬件的支持,因此是架構相關的,其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)
要使用原子操作,需要定義一個原子變量,然后使用內核提供的接口對其進行原子操作。
整型原子變量結構如下
- #include <linux/type.h>
-
typedef struct
{
- int counter ;
- } atomic_t ;
可以看出整型原子變量實質上是一個32位整型變量。
整型原子變量操作接口,其實現方式與具體的架構有關。
-
#include <asm/atomic.h>
- ATOMIC_INIT ( int i ) // 定義原子變量時,將其值賦為i
- int atomic_read ( atomic_t * v ) // 讀v的值
- void atomic_set ( atomic_t * v , int i ) // 設置v的值為i
- void atomic_add ( int i , atomic * v ) // v的值增加i
- void atomic_sub ( int i , atomic * v ) // v的值減少i
- void atomic_inc ( atomic * v ) // v的值加1
- void atomic_dec ( atomic * v ) // v的值減1
- int atomic_sub_and_test ( int i , atomic_t * v ) // v的值減少i , 且結果為0時返回true
- int atomic_add_negative ( int i , atomic_t * v ) // v的值增加i , 且結果為負數時返回true
- int atomic_add_return ( int i , atomic_t * v ) // v的值增加i , 且返回結果
- int atomic_sub_return ( int i , atomic_t * v ) // v的值減少i , 且返回結果
- int atomic_inc_return ( atomic_t * v ) // v的值加1,且返回結果
- int atomic_dec_return ( atomic_t * v ) // v的值減1,且返回結果
- int atomic_dec_and_test ( atomic_t * v ) // v的值減1,且結果為0時返回true
- int atomic_inc_and_test ( atomic_t * v ) // v的值加1,且結果為0時返回true
2 64位原子操作
(64-Bit Atomic Operations)
64位原子變量結構
-
typedef struct
{
- u64 __aligned ( 8 ) counter ;
- } atomic64_t ;
64位原子變量操作接口與整型變量操作接口類似,只要將整型變量接口名稱的"atomic"改成"atomic64"即可。
3 位原子操作
(Atomic Bitwise Operations)
位原子操作接口
-
#include < asm / bitops . h
>
- void set_bit ( int nr , void * addr ) / / 將addr第nr位置1
- void clear_bit ( int nr , void * addr ) / / 將addr第nr位置0
- void change_bit ( int nr , void * addr ) / / 將addr第nr位值取反
- int test_and_set_bit ( int nr , void * addr ) / / 將addr第nr位置1,並將該位之前值返回
- int test_and_clear_bit ( int nr , void * addr ) / / 將addr第nr位置0,並將該位之前值返回
- int test_and_change_bit ( int nr , void * addr ) / / 將addr第nr位取反,並將該位之前值返回
- int test_bit ( int nr , void * addr ) / / 將addr第nr位的值返回
13.java多線程編程底層原理剖析以及volatile原理
今天總結一下java多線程機制,以及volatile
首先,為什么需要多線程?
主要是因為計算機的運算能力遠遠大於I/O,通信傳輸,還有數據庫訪問等操作。所以緩存出現了,從而提高了訪問速度。但是由於會有多個緩存,以及數據讀寫問題,很有可能會讀到臟數據,其實這也就是緩存的一致性。
另外為了提高效率,處理器會對程序進行亂序執行優化,而對於虛擬機來說,就是指令重排序。意思就是說代碼順序與實際執行順序無關,實際執行順序是虛擬機根據前后依賴關系,結合運算器來決定的,但是結果是一樣的。
走入正題,先介紹一下java內存模型,內存模型主要用來屏蔽硬件與內存訪問的差異。
對於每一個線程會有工作內存,多個線程共享一個主內存,例如對象實例就在主內存會多個線程共享,而引用這個對象的變量實際在每個線程的工作內存,工作內存擁有主內存實例的副本拷貝,通過它來對實例進行,讀取與賦值都在工作內存,並且線程之間無法讀取對方的變量,都是通過主內存做一個過渡作用。(這里工作內存與主內存跟堆內存與棧內存不是一個概念,這是為了好理解)
接下來工作內存與主內存怎么進行交互?虛擬機定義了8種原子操作,包括lock(鎖定主內存的變量,使其被某一線程獨占),unlock(同理),read(把一個主內存的變量傳遞到工作內存中,以便load),load(將從主內存傳遞的值傳遞到工作內存的變量副本中),store(將工作內存中變量副本傳遞到主內存中去,以便write),write(將工作內存傳遞過來的值賦到主內存中變量),use(將工作內存的值傳遞給執行引擎),assign(將執行引擎的值傳遞到工作內存),這8中操作可以用來確定你的訪問是否安全。
下面介紹一下volatile,經常被問到的一個關鍵字,他的作用主要有兩個,我們一一說明:1 保證變量在各個線程的可見性,意思就是說這個變量的值一修改,其他線程可以立即得知。而一個普通變量需要先寫回主內存,然后其他線程去讀取這個值。2:禁止指令重排序優化。然而它並不能保證原子性,以及運算的線程安全,下面代碼來解釋一下第一個特性。
- public class VolatileTest extends Thread{
- public static volatile int a=0;
- public void run() {
- a++;
- }
- public static void main(String[] args) {
- VolatileTest array[]=new VolatileTest[10000];
- for (int i = 0; i < array.length; i++) {
- array[i]=new VolatileTest();
- array[i].start();
- }
- System.out.println(VolatileTest.a);
- }
- }
我們希望結果會是10000,然而並不是,原因就是a++這一條指令並不是原子操作,volatile的確保證從主內存獲得的數據是最正確的,但是當你運算的時候,其他線程很有可能會把一個值穿進去,導致值會變小。
那么什么情況下用volatile呢?一定要明白,它的開銷一定會小與同步塊。下面就是使用的情況,不符合這兩條就要用同步塊了。
1:運算結果並不依賴與當前值。
2:變量不需要與其他變量參與不變約束。
同樣用代碼解釋一下。
- public class VolatileTest extends Thread{
- public static volatile int a=0;
- public void run() {
- a=2;
- }
- public static void main(String[] args) {
- // TODO Auto-generated method stub
- VolatileTest array[]=new VolatileTest[100];
- for (int i = 0; i < array.length; i++) {
- array[i]=new VolatileTest();
- array[i].start();
- System.out.print(array[i].a+" ");
- }
- }}
個人理解就是a的值不依賴與現在在主內存a的實際值,不管a是幾,都變成1,而其他線程也會立即受到通知,因為也沒有運算,也會直接變為1.
接下來講一下指令重排序優化的東東,其實指令重排序對於單線程來說有利無害,反正最后的結果是一樣的,而且還提高了效率,但是對於多線程,可能會出現一些問題,而volatile修飾的變量,會在操作的時候,設置一個屏障,后面的操作,肯定不會比這個提前。否則后面的操作先執行,從而提前影響其他的線程。
好的,下面介紹幾個概念1:原子性 就像前面說的那8張操作就是,粒度小到多線程也不可能拆開它,而用synchronized,內部的東西其實就是一個組裝的“大原子”,但是記住volatile是不可以的 2 可見性 意思就是線程修改了值之后會立即同步到主內存,並且獲取值會從主內存直接獲取,而非緩存,volatile和synchronized都可以保證 3有序性 意思是保證線程內部執行順序,volatile可以保證禁止指令重排序,而synchronized,直接就鎖上了,所以它能解決幾乎所有同步問題,造成了濫用。
線程是cpu調度的基本單位,粒度比進程小,Thread的類很多方法是native,可能會為了效率,然而同時可能會平台相關,注意線程的優先級不太靠譜,以為可能與平台線程的優先級不一樣,造成沖突。再次補充一個線程狀態模型(本文章主要介紹java多線程模型,以及volatile,線程基礎不再贅述)
阻塞狀態與掛起狀態的區別在於阻塞在等待一個排它鎖,而掛起是等待時間到,或者是喚醒。
更多細節請查看我的線程基礎的這篇博客,多謝大家支持
本博客知識來源於深入理解java虛擬機,值得一看,強力 推薦,特別底層!!!
--------
12.Java中Volatile底層原理與應用
Volatile定義與原理
Java編程語言允許線程訪問共享變量,為了確保共享變量能被准確和一致地更新,線程應該通過排它鎖單獨獲取這個變量
Java語言提供了Violatile來確保多處理開發中,共享變量的“可見性”,即當另外一個線程修改一個共享變量時,另外一個線程能讀到這個修改的值。它是輕量級的synchronized,不會引起線程上下文的切換和調度,執行開銷更小。
使用Violatile修飾的變量在匯編階段,會多出一條lock前綴指令,它在多核處理器下回引發兩件事情:
- 將當前處理器緩存行的數據寫回到系統內存
- 這個寫回內存的操作會使在其他CPU里緩存了該內存地址的數據無效。
通常處理器和內存之間都有幾級緩存來提高處理速度,處理器先將內存中的數據讀取到內部緩存后再進行操作,但是對於緩存寫會內存的時機則無法得知,因此在一個處理器里修改的變量值,不一定能及時寫會緩存,這種變量修改對其他處理器變得“不可見”了。但是,使用Volatile修飾的變量,在寫操作的時候,會強制將這個變量所在緩存行的數據寫回到內存中,但即使寫回到內存,其他處理器也有可能使用內部的緩存數據,從而導致變量不一致,所以,在多處理器下,為了保證各個處理器的緩存是一致的,就會實現緩存一致性協議,每個處理器通過嗅探在總線上傳播的數據來檢查自己緩存的值是不是過期,如果過期,就會將該緩存行設置成無效狀態,下次要使用就會重新從內存中讀取。
追加字節優化Volatile性能
在某些情況下,通過將共享變量追加到64字節可以優化其使用性能。
在JDK 7 的並發包里,有一個隊列集合類LinkedTransferQueue,它在使用volatile變量時,用一種追加字節的方式來優化隊列出隊和入隊的性能。隊里定義了兩個共享結點,頭結點和尾結點,都由使用了volatile的內部類定義,通過將兩個共享結點的字節數增加到64字節來優化效率,具體分析如下:
部分CPU的L1、L2或L3緩存的高速緩存行64字節寬,不支持部分填充緩存行
這意味着,如果隊列的頭結點和尾結點都不足64字節,處理器會將他們讀到同一個高速緩存行,在多處理器下每個處理器都會緩存同樣的頭尾結點,當一個處理器試圖修改頭結點時,會將整個緩存行鎖定,那么在緩存一致性的機制下,其他處理器不能訪問自己高速緩存中的尾節點,而頭尾結點在隊列中都是會頻繁訪問的,因此會影響使用性能。而通過填充字節使頭尾結點加載到不同的緩存行,避免頭尾結點在修改時相互鎖定。
但是在以下兩種場景,不應該使用這種優化方式:
- 緩存行非64字節寬的處理器(自行調整補充字節長度,原理一樣)
- 共享變量不會被頻繁的寫。追加字節會導致CPU讀取性能下降,如果共享變量寫的頻率很低,那么被鎖的幾率也很小,就沒必要避免相互鎖定了
Volatile無法保證原子性
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修飾變量時,一定要保證對該變量的寫操作是原子性的,例如程序中的狀態變量,對該變量的修改不依賴於其當前值。
1.1. volatile底層實現原理
定義
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緩存了該地址的數據無效。