天天看點

并發程式設計系列(六)—深入了解CAS和Unsafe類

并發程式設計系列(六)—深入了解CAS和Unsafe類

前言

大家好,牧碼心今天給大家推薦一篇并發程式設計系列(六)—深入了解CAS和Unsafe類的文章,希望對你有所幫助。内容如下:

  • CAS概要
  • CAS核心思想
  • CAS缺點
  • CAS應用
  • unsafe類

CAS概要

在前面系列文章中分析過通過synchronized關鍵字可以保證多線程安全的通路共享資源,其原理是目前線程持有對象的内置鎖,其他線程沒有擷取到該鎖是無法通路,需要排隊阻塞等待鎖的釋放。本篇我們将分析通過無鎖化機制如何來保證線程安全,分析前我們先來看兩個概念:

  • 兩種鎖機制
    • 悲觀鎖:所謂悲觀鎖就是假定會發生并發安全問題(每次取資料時都認為其他線程會修改),會進行加鎖來屏蔽其他線程對資料進行修改。如使用synchronized和ReentrantLock等
    • 樂觀鎖:所謂樂觀鎖就是假定操作不會發生并發安全問題((不會有其他線程對資料進行修改)),是以不會加鎖,但是在更新資料時會判斷其他線程是否有沒有對資料進行過修改。如使用版本号機制或CAS(compare and swap)算法。
  • 無鎖化

    無鎖是一種樂觀政策,總是假設對共享資源的通路沒有沖突,線程可以不停執行,無需加鎖,無需等待,一旦發現沖突,無鎖政策則采用一種稱為CAS的技術來保證線程執行的安全性,這項CAS技術就是無鎖政策實作的關鍵。

CAS 核心思想

  • 什麼是CAS

    CAS全稱 Compare And Swap(比較與交換),是一種無鎖算法。在不使用鎖(沒有線程被阻塞)的情況下實作多線程之間的變量同步。java.util.concurrent包中的原子類就是通過CAS來實作了樂觀鎖。

  • 算法思想

    CAS采用樂觀鎖的機制,是一種無鎖化技術。其執行函數為:CAS(V,E,N),函數包含3個操作數:

    • V:表示需要更新的值;
    • E:表示期望值;
    • N:表示拟寫入的新值;

      函數思想是如果V相等于E,則将V的值寫入N,若V不能于E,說明有其他線程做了更新,則目前線程什麼也不做,但可以選擇重新讀取該變量再嘗試再次修改該變量,也可以放棄操作。其原理圖如下:

      并發程式設計系列(六)—深入了解CAS和Unsafe類
      說明:由于CAS采用樂觀鎖機制,它總認為自己可以成功完成操作,當多個線程同時使用CAS操作一個變量時,隻有一個會勝出,并成功更新,其餘均會失敗,但失敗的線程并不會被挂起,僅是被告知失敗,并且允許再次嘗試,當然也允許失敗的線程放棄操作,基于這樣的原理,CAS操作即使沒有鎖,同樣知道其他線程對共享資源操作影響,并執行相應的處理措施。同時也不會出現死鎖的情況。
  • CPU對CAS的支援

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

CAS 缺點

CAS雖然能高效解決并發中的原子操作問題,但是CAS也存在幾個問題:ABA問題,循環時間長開銷大和隻能保證一個共享變量的原子操作。

  • ABA問題

    因為CAS需要在操作值的時候檢查下值有沒有發生變化,如果沒有發生變化則更新,但是如果一個值原來是A,變成了B,又變成了A,那麼使用CAS進行檢查時會發現它的值沒有發生變化,但是實際上卻變化了。

    解決方案: CAS類似于樂觀鎖,即每次去拿資料的時候都認為别人不會修改,是以不會上鎖,但是在更新的時候會判斷一下在此期間别人有沒有去更新這個資料。是以解決方案也可以跟樂觀鎖一樣:使用版本号機制,如手動增加版本号字段,在變量前面追加上版本号,每次變量更新的時候把版本号加一,那麼A-B-A 就會變成1A-2B-3A。

    JDK1.5開始Atomic包裡提供了一個類AtomicStampedReference來解決ABA問題。這個類的compareAndSet方法的作用是首先檢查目前引用是否等于預期引用,并且檢查目前的标志是否等于預期标志,如果全部相等,則以原子方式将該應用和該标志的值設定為給定的更新值。

  • 循環時間長開銷大

    自旋CAS預測未來會擷取到鎖而不斷循環等待。在經過若幹次循環後,如果得到鎖,就順利進入臨界區。如果還不能獲得鎖,那就會将線程在作業系統層面挂起,這種方式确實也是可以提升效率的。但問題是當線程越來越多競争很激烈時,占用CPU的時間變長會導緻性能急劇下降。

    解決方案: 設定自旋鎖的循環次數來破壞掉死循環,當超過一定時間或者一定次數時退出循環。

  • 隻能保證一個共享變量的原子操作

    當對一個共享變量執行操作時,我們可以使用循環CAS的方式來保證原子操作,但是對多個共享變量操作時,循環CAS就無法保證操作的原子性。

    解決方案: 可以用鎖,或者有一個取巧的辦法,就是把多個共享變量合并成一個共享變量來操作。比如有兩個共享變量i=a,j=b,合并一下ij=ab,然後用CAS來操作ij。從JDK1.5開始提供了AtomicReference類來保證引用對象之間的原子性,你可以把多個變量放在一個對象裡來進行CAS操作。

CAS的應用

CAS廣泛應用在JDK的并發包下的原子架構(java.util.concurrent.atomic)中,如AtomicXXX等類中都引用了CAS操作,而在java.util.concurrent中其它的大多數類在實作時都直接或間接的使用了這些原子變量類。下面看下AtomicInteger在使用CAS的地方:

// AtomicInteger 自增方法
public final int incrementAndGet() {
  return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
// Unsafe.class
public final int getAndAddInt(Object var1, long var2, int var4) {
  int var5;
  do {
      var5 = this.getIntVolatile(var1, var2);
  } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
  return var5;
}
           

我們檢視AtomicInteger的自增函數incrementAndGet()的源碼時,發現自增函數底層調用的是unsafe.getAndAddInt(),而getAndAddInt()循環擷取給定對象o中的偏移量處的值v,然後判斷記憶體值是否等于v。如果相等則将記憶體值設定為 v + delta,否則傳回false,繼續循環進行重試,直到設定成功才能退出循環,并且将舊值傳回。整個“比較+更新”操作封裝在compareAndSwapInt()中,在JNI裡是借助于一個CPU指令完成的,屬于原子操作,可以保證多個線程都能夠看到同一個變量的修改值。

Unsafe類

上面我們分析了CAS的核心思想,缺點和應用場景,其中有些地方也會涉及到Unsafe類,那什麼是Unsafe類,怎麼擷取Unsafe類以及有哪些功能呢?

  • 什麼是Unsafe類

    Unsafe是位于sun.misc包下的一個類,主要提供一些用于執行低級别、不安全操作的方法,如直接通路系統記憶體資源、自主管理記憶體資源等,這些方法在提升Java運作效率、增強Java語言底層資源操作能力方面起到了很大的作用。但由于Unsafe類使Java語言擁有了類似C語言指針一樣操作記憶體空間的能力,這無疑也增加了程式發生相關指針問題的風險。在程式中過度、不正确使用Unsafe類會使得程式出錯的機率變大,使得Java這種安全的語言變得不再“安全”,是以對Unsafe的使用一定要慎用。

  • Unsafe類實作

    Unsafe類為一單例實作,提供靜态方法getUnsafe擷取Unsafe執行個體,當且僅當調用getUnsafe方法的類為引導類加載器所加載時才合法,否則抛出SecurityException異常,如下所示:

public class Unsafe {
	// 單例對象
	private static final Unsafe theUnsafe;
	private Unsafe() {}
	@CallerSensitive
	public static Unsafe getUnsafe() {
		Class var0 = Reflection.getCallerClass();
		// 僅在引導類加載器`BootstrapClassLoader`加載時才合法
		if(!VM.isSystemDomainLoader(var0.getClassLoader())) {
		throw new SecurityException("Unsafe");
	} else {
		return theUnsafe;
		}
	}
}
           
  • Unsafe功能

    Unsafe提供的API的功能可分為記憶體操作、CAS、Class相關、對象操作、線程排程、系統資訊擷取、記憶體屏障、數組操作等幾類,如圖所示:

    并發程式設計系列(六)—深入了解CAS和Unsafe類
    下面将從記憶體操作,CAS操作來介紹,其他方面可自行查閱相關資料。
  • 記憶體操作

    記憶體操作主要包含堆外記憶體的配置設定、拷貝、釋放、給定位址值操作等方法。引用源碼:

//配置設定記憶體, 相當于C++的malloc函數
public native long allocateMemory(long bytes);
//擴充記憶體
public native long reallocateMemory(long address, long bytes);
//釋放記憶體
public native void freeMemory(long address);
//在給定的記憶體塊中設定值
public native void setMemory(Object o, long offset, long bytes,byte value);
//記憶體拷貝
public native void copyMemory(Object srcBase, long srcOffset,Object destBase, long destOffset, long bytes);
//擷取給定位址值,忽略修飾限定符的通路限制。與此類似操作還有: getInt,getDouble,getLong,getChar等
public native Object getObject(Object o, long offset);
//為給定位址設定值,忽略修飾限定符的通路限制,與此類似操作還有:putInt,putDouble,putLong,putChar等
public native void putObject(Object o, long offset, Object x);
           

說明:我們在Java中建立的對象都處于堆内記憶體(heap)中,堆内記憶體是由JVM所管控的Java程序記憶體,并且它們遵循JVM的記憶體管理機制,JVM會采用垃圾回收機制統一管理堆記憶體。與之相對的是堆外記憶體,存在于JVM管控之外的記憶體區域,Java中對堆外記憶體的操作,依賴于Unsafe提供的操作堆外記憶體的native方法。使用堆外記憶體的原因:

  1. 對垃圾回收停頓的改善。由于堆外記憶體是直接受作業系統管理而不是JVM,是以當我們使用堆外記憶體時,即可保持較小的堆内記憶體規模。進而在GC時減少回收停頓對于應用的影響。
  2. 提升程式I/O操作的性能。通常在I/O通信過程中,會存在堆内記憶體到堆外記憶體的資料拷貝操作,對于需要頻繁進行記憶體間資料拷貝且生命周期較短的暫存資料,都建議存儲到堆外記憶體。

    典型應用:比如DirectByteBuffer是Java用于實作堆外記憶體的一個重要類,通常用在通信過程中做緩沖池,如在Netty、MINA等NIO架構中應用廣泛。DirectByteBuffer對于堆外記憶體的建立、使用、銷毀等邏輯均由Unsafe提供的堆外記憶體API來實作。

  • 線程排程

    線程排程包括線程挂起、恢複、鎖機制等方法。引用源碼:

//取消阻塞線程
public native void unpark(Object thread);
//阻塞線程
public native void park(boolean isAbsolute, long time);
//獲得對象鎖(可重入鎖)
@Deprecated
public native void monitorEnter(Object o);
//釋放對象鎖
@Deprecated
public native void monitorExit(Object o);
//嘗試擷取對象鎖
@Deprecated
public native boolean tryMonitorEnter(Object o)
           

說明:方法park、unpark即可實作線程的挂起與恢複,将一個線程進行挂起是通過park方法實作的,調用park方法後,線程将一直阻塞直到逾時或者中斷等條件出現;unpark可以終止一個挂起的線程,使其恢複正常。

典型應用:Java鎖和同步器架構的核心類AbstractQueuedSynchronizer,就是通過調用LockSupport.park()和LockSupport.unpark()實作線程的阻塞和喚醒的,而LockSupport的park、unpark方法實際是調用Unsafe的park、unpark方式來實作

繼續閱讀