高併發編程 -- Java中CAS詳解

目錄

鎖機制存在以下問題:

volatile機制

什麼是CAS

模擬CAS算法 :

CAS的用處:

處理器是如何實現原子操作

1.處理器自動保證基本內存操作的原子性

2. 總線鎖定

3. 緩存鎖定

CAS優缺點

使用樂觀鎖還是悲觀鎖

CAS應用

concurrent包的實現


 

在JDK 5之前Java語言主要依靠synchronized關鍵字保證同步,這會導致有鎖。

一、鎖機制存在以下問題:

(1)在多線程競爭下,加鎖、釋放鎖會導致比較多的上下文切換和調度延時,引起性能問題。

(2)一個線程持有鎖會導致其它所有需要此鎖的線程掛起。

(3)如果一個優先級高的線程等待一個優先級低的線程釋放鎖會導致優先級倒置,引起性能風險。

 

二、volatile機制

volatile到底如何保證可見性和禁止指令重排序的。

“觀察加入volatile關鍵字和沒有加入volatile關鍵字時所生成的彙編代碼發現,加入volatile關鍵字時,會多出一個lock前綴指令”

0x000000011214bb49: mov    %rdi,%rax
  0x000000011214bb4c: dec    %eax
  0x000000011214bb4e: mov    %eax,0x10(%rsi)
  0x000000011214bb51: lock addl $0x0,(%rsp)     ;*putfield v1
                                                ; - com.earnfish.VolatileBarrierExample::readAndWrite@21 (line 35)

  0x000000011214bb56: imul   %edi,%ebx
  0x000000011214bb59: mov    %ebx,0x14(%rsi)
  0x000000011214bb5c: lock addl $0x0,(%rsp)     ;*putfield v2
                                                ; - com.earnfish.VolatileBarrierExample::readAndWrite@28 (line 36)

lock前綴指令實際上相當於一個內存屏障(也成內存柵欄),內存屏障會提供3個功能:

1)它確保指令重排序時不會把其後面的指令排到內存屏障之前的位置,也不會把前面的指令排到內存屏障的後面;即在執行到內存屏障這句指令時,在它前面的操作已經全部完成;

2)它會強制將對緩存的修改操作立即寫入主存;

3)如果是寫操作,它會導致其他CPU中對應的緩存行無效。

 

但是volatile不能保證原子性。因此對於同步最終還是要回到鎖機制上來。

獨佔鎖是一種悲觀鎖,synchronized就是一種獨佔鎖,會導致其它所有需要鎖的線程掛起,等待持有鎖的線程釋放鎖。而另一個更加有效的鎖就是樂觀鎖。所謂樂觀鎖就是,每次不加鎖而是假設沒有衝突而去完成某項操作,如果因爲衝突失敗就重試,直到成功爲止。樂觀鎖用到的機制就是CAS,Compare and Swap。

來看一段代碼:

代碼證明volatile不能保證原子性

package org.hcgao.common.blog.AQS;

import java.util.concurrent.CountDownLatch;

public class Test_Volatile implements Runnable {
	
	CountDownLatch cdOrder;
	
	public Test_Volatile(CountDownLatch cdOrder) {
		this.cdOrder = cdOrder;
	}
	
	private volatile int count = 0;
	
	public void add() {
		count += 1;
	}
	
	public void add2() {
		count = count + 1;
	}
	
	@Override
	public void run() {
		try {
			//System.out.println("準備:"+count);
			cdOrder.await();
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		// TODO Auto-generated method stub
		add();
		System.out.println(count);
	}
	
	public static void main(String[] args) throws InterruptedException {
		
		final CountDownLatch cdOrder = new CountDownLatch(1);
	    Test_Volatile test = new Test_Volatile(cdOrder); 
	    for (int i = 0; i < 10000; i++) {
	    Thread th1 = new Thread(test); th1.start(); }
	    Thread.sleep(5000);
	    cdOrder.countDown();
		
	}
	
}
//add()方法的執行結果
2
2
3
5
4
6
10
9
8
7
11
14
13
12
15
19
18
17
16
20
......
9993
9994
9995
9996
9997
9998
9999
10000
//add2()方法的執行結果
3
3
3
4
6
5
7
8
12
......
9729
9725
9724
9723
9719
9716
9715
9714
9704
9696
9695

volatile變量自身具有下列特性。

❑ 可見性。對一個volatile變量的讀,總是能看到(任意線程)對這個volatile變量最後的寫入。

❑ 原子性:對任意單個volatile變量的讀/寫具有原子性,但類似於volatile++這種複合操作不具有原子性。

 

什麼是CAS

CAS,compare and swap的縮寫,中文:比較並交換。

我們都知道,在java語言之前,併發就已經廣泛存在並在服務器領域得到了大量的應用。所以硬件廠商老早就在芯片中加入了大量直至併發操作的原語,從而在硬件層面提升效率。在intel的CPU中,使用cmpxchg指令。

 

在Java發展初期,java語言是不能夠利用硬件提供的這些便利來提升系統的性能的。而隨着java不斷的發展,Java本地方法(JNI)的出現,使得java程序越過JVM直接調用本地方法提供了一種便捷的方式,因而java在併發的手段上也多了起來。而在Doug Lea提供的cucurenct包中,CAS理論是它實現整個java包的基石。

 

CAS 操作包含三個操作數 —— 內存位置(V)、預期原值(A)和新值(B)。 如果內存位置的值與預期原值相匹配,那麼處理器會自動將該位置值更新爲新值 。否則,處理器不做任何操作。無論哪種情況,它都會在 CAS 指令之前返回該 位置的值。(在 CAS 的一些特殊情況下將僅返回 CAS 是否成功,而不提取當前 值。)CAS 有效地說明了“我認爲位置 V 應該包含值 A;如果包含該值,則將 B 放到這個位置;否則,不要更改該位置,只告訴我這個位置現在的值即可。”

通常將 CAS 用於同步的方式是從地址 V 讀取值 A,執行多步計算來獲得新 值 B,然後使用 CAS 將 V 的值從 A 改爲 B。如果 V 處的值尚未同時更改,則 CAS 操作成功。

類似於 CAS 的指令允許算法執行讀-修改-寫操作,而無需害怕其他線程同時 修改變量,因爲如果其他線程修改變量,那麼 CAS 會檢測它(並失敗),算法 可以對該操作重新計算。

CPU指令對CAS的支持

或許我們可能會有這樣的疑問,假設存在多個線程執行CAS操作並且CAS的步驟很多,有沒有可能在判斷V和E相同後,正要賦值時,切換了線程,更改了值。造成了數據不一致呢?答案是否定的,因爲CAS是一種系統原語,原語屬於操作系統用語範疇,是由若干條指令組成的,用於完成某個功能的一個過程,並且原語的執行必須是連續的,在執行過程中不允許被中斷,也就是說CAS是一條CPU的原子指令,不會造成所謂的數據不一致問題。

compareAndSet利用JNI來完成CPU指令的操作。   
 public final boolean compareAndSet(int expect, int update) { 
        //compareAndSwapInt就是藉助C來調用CPU底層指令實現的。  
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }



intel x86處理器的源代碼的片段:
 
// Adding a lock prefix to an instruction on MP machine
// VC++ doesn't like the lock prefix to be on a single line
// so we can't insert a label after the lock prefix.
// By emitting a lock prefix, we can define a label after it.
#define LOCK_IF_MP(mp) __asm cmp mp, 0  \
                       __asm je L0      \
                       __asm _emit 0xF0 \
                       __asm L0:
 
inline jint     Atomic::cmpxchg    (jint     exchange_value, volatile jint*     dest, jint     compare_value) {
  // alternative for InterlockedCompareExchange
  int mp = os::is_MP();
  __asm {
    mov edx, dest
    mov ecx, exchange_value
    mov eax, compare_value
    LOCK_IF_MP(mp)
    cmpxchg dword ptr [edx], ecx
  }
}

根據CPU處理器源代碼所示,程序會根據當前處理器的類型來決定是否爲cmpxchg指令添加lock前綴。如果程序是在多處理器上運行,就爲cmpxchg指令加上lock前綴(lock cmpxchg)。反之,如果程序是在單處理器上運行,就省略lock前綴(單處理器自身會維護單處理器內的順序一致性,不需要lock前綴提供的內存屏障效果)。

 

 

內存值 V 
預估值 A 
更新值 B

當且僅當,V==A 時,纔將 B 的只值賦給 A,否則,將不做任何操作。  //(CAS算法的一個特性)

模擬CAS算法 :

CAS的流程:獲取 —> 比較 —> 設置

public class TestCompareAndSwap {
    public static void main(String[] args) {
        final CompareAndSwap cas=new CompareAndSwap();

        for(int i=0;i<10;i++){
            new Thread(new Runnable(){
                @Override
                public void run() { 
                    int expectedValue=cas.get(); //每次執行前先獲取內存中的值
                    boolean isTrue=cas.compareAndSet(expectedValue, (int)(Math.random()*101));
                    System.out.println(isTrue);
                }
            }).start();
        }
    }
}

class CompareAndSwap{
    //內存值
    private int value;

    //獲取內存值
    public synchronized int get(){
        return value;
    }

    //比較
    public synchronized boolean compareAndSwap(int expectedValue,int newValue){
        int oldValue=value;//線程讀取內存值,與預估值比較
        if(oldValue==expectedValue){
            this.value=newValue;
            return true;
        }
        return false;
    }
  
  	//設置
  	public synchronized boolean compareAndSet(int expectedValue,int newValue){
    	return expectedValue == compareAndSwap(expectedValue,newValue);
  	}
}

 

CAS的用處:

原子類例如AtomicInteger裏的方法都很簡單,我們看一下getAndIncrement方法:

//該方法功能是Interger類型加1
public final int getAndIncrement() {
        //主要看這個getAndAddInt方法
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }

//var1 是this指針
//var2 是地址偏移量
//var4 是自增的數值,是自增1還是自增N
public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            //獲取內存值,這是內存值已經是舊的,假設我們稱作期望值E
            var5 = this.getIntVolatile(var1, var2);
            //compareAndSwapInt方法是重點,
            //var5是期望值,var5 + var4是要更新的值
            //這個操作就是調用CAS的JNI,每個線程將自己內存裏的內存值M
            //與var5期望值E作比較,如果相同將內存值M更新爲var5 + var4,否則做自旋操作
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }

 

處理器是如何實現原子操作

32位IA-32處理器使用基於對緩存加鎖或總線加鎖的方式來實現多處理器之間的原子操作。首先處理器會自動保證基本的內存操作的原子性。處理器保證從系統內存中讀取或寫入一個字節是原子的,意思是當一個處理器讀取一個字節時,其他處理器不能訪問這個字節的內存地址。

 

1.處理器自動保證基本內存操作的原子性

首先處理器會自動保證基本的內存操作的原子性。處理器保證從系統內存當中讀取或者寫入一個字節是原子的,意思是當一個處理器讀取一個字節時,其他處理器不能訪問這個字節的內存地址。奔騰6和最新的處理器能自動保證單處理器對同一個緩存行裏進行16/32/64位的操作是原子的,但是複雜的內存操作處理器不能自動保證其原子性,比如跨總線寬度,跨多個緩存行,跨頁表的訪問。但是處理器提供總線鎖定和緩存鎖定兩個機制來保證複雜內存操作的原子性。

 

2. 總線鎖定

這裏寫圖片描述

如果多個處理器一起對共享變量進行讀改寫操作(i++就是典型的讀改寫操作),這個讀改寫操作就不是原子的。如上圖所示,各個處理器將i的值讀入自己的處理器緩存中,各自對各自的緩存裏的i值進行操作,然後分別寫入系統內存從而導致了問題的產生。要想對共享變量的讀改寫操作也是原子性的,必須保證,CPU1讀改寫共享變量的時候,CPU2不能操作該共享變量內存地址的緩存行。

處理器的總線鎖就是這麼來保證原子性的,所謂總線鎖就是使用處理器提供的一個LOCK #信號,當一個處理器在總線上輸出此信號時,其他處理器的請求將會被阻塞住,那麼該處理器可以獨佔共享內存。

3. 緩存鎖定

在同一時刻我們只需要保證對某一個內存地址的操作是原子性就可以了,但是總線鎖把CPU和內存之間的通信鎖住了,這使得鎖定期間,其他處理器不能操作其他內存地址的數據,所以總線鎖定的開銷比較大。

頻繁使用的內存會緩存在處理器的L1,L2,L3高速緩存中,那麼原子操作就可以直接在處理器內部緩存進行,並不需要聲明總線鎖。

緩存鎖是:一個處理器的緩存寫回到內存會導致其他處理器的緩存無效,在多核處理器中,例如在Pentium和P6 family處理中,如果通過嗅探一個處理器來檢測到其他處理器打算寫內存地址,而這個地址當前處於共享狀態,那麼正在嗅探的處理器將使它的緩存行無效,在下次訪問相同內存地址時,強制執行緩存行填充。

但是有兩種情況是不能使用緩存鎖:一是不能緩存到處理器的數據以及跨多個緩存行的數據;而是有些處理器不支持緩存鎖定。

 

CAS優缺點

  • 優點

非阻塞的輕量級的樂觀鎖,通過CPU指令實現,在資源競爭不激烈的情況下性能高,相比synchronized重量鎖,synchronized會進行比較複雜的加鎖、解鎖和喚醒操作。

  • 缺點

CAS存在的問題

CAS雖然很高效的解決原子操作,但是CAS仍然存在三大問題。ABA問題,循環時間長開銷大和只能保證一個共享變量的原子操作

1.  ABA問題。因爲CAS需要在操作值的時候檢查下值有沒有發生變化,如果沒有發生變化則更新,但是如果一個值原來是A,變成了B,又變成了A,那麼使用CAS進行檢查時會發現它的值沒有發生變化,但是實際上卻變化了。ABA問題的解決思路就是使用版本號。在變量前面追加上版本號,每次變量更新的時候把版本號加一,那麼A-B-A 就會變成1A-2B-3A。

從Java1.5開始JDK的atomic包裏提供了一個類AtomicStampedReference來解決ABA問題。這個類的compareAndSet方法作用是首先檢查當前引用是否等於預期引用,並且當前標誌是否等於預期標誌,如果全部相等,則以原子方式將該引用和該標誌的值設置爲給定的更新值。

關於ABA問題參考文檔: http://blog.hesey.net/2011/09/resolve-aba-by-atomicstampedreference.html

2. 循環時間長開銷大。自旋時間過長,消耗CPU資源,如果資源競爭激烈,多線程自旋長時間消耗資源。自旋CAS如果長時間不成功,會給CPU帶來非常大的執行開銷。如果JVM能支持處理器提供的pause指令那麼效率會有一定的提升,pause指令有兩個作用,第一它可以延遲流水線執行指令(de-pipeline),使CPU不會消耗過多的執行資源,延遲的時間取決於具體實現的版本,在一些處理器上延遲時間是零。第二它可以避免在退出循環的時候因內存順序衝突(memory order violation)而引起CPU流水線被清空(CPU pipeline flush),從而提高CPU的執行效率。

3. 只能保證一個共享變量的原子操作。當對一個共享變量執行操作時,我們可以使用循環CAS的方式來保證原子操作,但是對多個共享變量操作時,循環CAS就無法保證操作的原子性,這個時候就可以用鎖,或者有一個取巧的辦法,就是把多個共享變量合併成一個共享變量來操作。比如有兩個共享變量i=2,j=a,合併一下ij=2a,然後用CAS來操作ij。從Java1.5開始JDK提供了AtomicReference類來保證引用對象之間的原子性,你可以把多個變量放在一個對象裏來進行CAS操作。

 

使用樂觀鎖還是悲觀鎖

從上面對兩種鎖的介紹,我們知道兩種鎖各有優缺點,不可認爲一種好於另一種,像樂觀鎖適用於寫比較少的情況下(多讀場景),即衝突真的很少發生的時候,這樣可以省去了鎖的開銷,加大了系統的吞吐量。但如果是多寫的情況,一般會經常發生衝突,這就會導致CAS算法會不斷的進行retry,這樣反倒是降低了性能,所以一般多寫的場景下用悲觀鎖就比較合適。

 

CAS應用

由於CAS是CPU指令,我們只能通過JNI與操作系統交互,關於CAS的方法都在sun.misc包下Unsafe的類裏,java.util.concurrent.atomic包下的原子類等通過CAS來實現原子操作。

 

concurrent包的實現

由於java的CAS同時具有 volatile 讀和volatile寫的內存語義,因此Java線程之間的通信現在有了下面四種方式:

  1. A線程寫volatile變量,隨後B線程讀這個volatile變量。
  2. A線程寫volatile變量,隨後B線程用CAS更新這個volatile變量。
  3. A線程用CAS更新一個volatile變量,隨後B線程用CAS更新這個volatile變量。
  4. A線程用CAS更新一個volatile變量,隨後B線程讀這個volatile變量。

 

對volatile寫和volatile讀的內存語義做個總結。

❑ 線程A寫一個volatile變量,實質上是線程A向接下來將要讀這個volatile變量的某個線程發出了(其對共享變量所做修改的)消息。

❑ 線程B讀一個volatile變量,實質上是線程B接收了之前某個線程發出的(在寫這個volatile變量之前對共享變量所做修改的)消息。

❑ 線程A寫一個volatile變量,隨後線程B讀這個volatile變量,這個過程實質上是線程A通過主內存向線程B發送消息。

 

Java的CAS會使用現代處理器上提供的高效機器級別原子指令,這些原子指令以原子方式對內存執行讀-改-寫操作,這是在多處理器中實現同步的關鍵(從本質上來說,能夠支持原子性讀-改-寫指令的計算機器,是順序計算圖靈機的異步等價機器,因此任何現代的多處理器都會去支持某種能對內存執行原子性讀-改-寫操作的原子指令)。同時,volatile變量的讀/寫和CAS可以實現線程之間的通信。把這些特性整合在一起,就形成了整個concurrent包得以實現的基石。如果我們仔細分析concurrent包的源代碼實現,會發現一個通用化的實現模式:

  1. 首先,聲明共享變量爲volatile;
  2. 然後,使用CAS的原子條件更新來實現線程之間的同步;
  3. 同時,配合以volatile的讀/寫和CAS所具有的volatile讀和寫的內存語義來實現線程之間的通信。

AQS,非阻塞數據結構和原子變量類(java.util.concurrent.atomic包中的類),這些concurrent包中的基礎類都是使用這種模式來實現的,而concurrent包中的高層類又是依賴於這些基礎類來實現的。從整體來看,concurrent包的實現示意圖如下:

 

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章