Java 8新特性探究(十)StampedLock將是解決同步問題的新寵

Java8就像一個寶藏,一個小的API改進,也足與寫一篇文章,好比同步,一直是多線程併發編程的一個老話題,相信沒有人喜歡同步的代碼,這會下降應用的吞吐量等性能指標,最壞的時候會掛起死機,可是即便這樣你也沒得選擇,由於要保證信息的正確性。因此本文決定將從synchronized、Lock到Java8新增的StampedLock進行對比分析,相信StampedLock不會讓你們失望。java

synchronized

在java5以前,實現同步主要是使用synchronized。它是Java語言的關鍵字,當它用來修飾一個方法或者一個代碼塊的時候,可以保證在同一時刻最多隻有一個線程執行該段代碼。編程

有四種不一樣的同步塊:多線程

  1. 實例方法併發

  2. 靜態方法工具

  3. 實例方法中的同步塊性能

  4. 靜態方法中的同步塊優化

你們對此應該不陌生,因此很少講了,如下是代碼示例
this

synchronized(this)
// do operation
}

小結:在多線程併發編程中Synchronized一直是元老級角色,不少人都會稱呼它爲重量級鎖,可是隨着Java SE1.6對Synchronized進行了各類優化以後,性能上也有所提高。spa

Lock

它是Java 5在java.util.concurrent.locks新增的一個API。線程

Lock是一個接口,核心方法是lock(),unlock(),tryLock(),實現類有ReentrantLock, ReentrantReadWriteLock.ReadLock, ReentrantReadWriteLock.WriteLock;

ReentrantReadWriteLock, ReentrantLock 和synchronized鎖都有相同的內存語義。

與synchronized不一樣的是,Lock徹底用Java寫成,在java這個層面是無關JVM實現的。Lock提供更靈活的鎖機制,不少synchronized 沒有提供的許多特性,好比鎖投票,定時鎖等候和中斷鎖等候,但由於lock是經過代碼實現的,要保證鎖定必定會被釋放,就必須將unLock()放到finally{}中

下面是Lock的一個代碼示例

rwlock.writeLock().lock();
try {
// do operation
} finally {
rwlock.writeLock().unlock();
}

小結:比synchronized更靈活、更具可伸縮性的鎖定機制,但無論怎麼說仍是synchronized代碼要更容易書寫些

StampedLock

它是java8在java.util.concurrent.locks新增的一個API。

ReentrantReadWriteLock 在沒有任何讀寫鎖時,才能夠取得寫入鎖,這可用於實現了悲觀讀取(Pessimistic Reading),即若是執行中進行讀取時,常常可能有另外一執行要寫入的需求,爲了保持同步,ReentrantReadWriteLock 的讀取鎖定就可派上用場。

然而,若是讀取執行狀況不少,寫入不多的狀況下,使用 ReentrantReadWriteLock 可能會使寫入線程遭遇飢餓(Starvation)問題,也就是寫入線程吃吃沒法競爭到鎖定而一直處於等待狀態。

StampedLock控制鎖有三種模式(寫,讀,樂觀讀),一個StampedLock狀態是由版本和模式兩個部分組成,鎖獲取方法返回一個數字做爲票據stamp,它用相應的鎖狀態表示並控制訪問,數字0表示沒有寫鎖被受權訪問。在讀鎖上分爲悲觀鎖和樂觀鎖。

所謂的樂觀讀模式,也就是若讀的操做不少,寫的操做不多的狀況下,你能夠樂觀地認爲,寫入與讀取同時發生概率不多,所以不悲觀地使用徹底的讀取鎖定,程序能夠查看讀取資料以後,是否遭到寫入執行的變動,再採起後續的措施(從新讀取變動信息,或者拋出異常) ,這一個小小改進,可大幅度提升程序的吞吐量!!

下面是java doc提供的StampedLock一個例子

class Point {
   private double x, y;
   private final StampedLock sl = new StampedLock();
   void move(double deltaX, double deltaY) { // an exclusively locked method
     long stamp = sl.writeLock();
     try {
       x += deltaX;
       y += deltaY;
     } finally {
       sl.unlockWrite(stamp);
     }
   }
  //下面看看樂觀讀鎖案例
   double distanceFromOrigin() { // A read-only method
     long stamp = sl.tryOptimisticRead(); //得到一個樂觀讀鎖
     double currentX = x, currentY = y; //將兩個字段讀入本地局部變量
     if (!sl.validate(stamp)) { //檢查發出樂觀讀鎖後同時是否有其餘寫鎖發生?
        stamp = sl.readLock(); //若是沒有,咱們再次得到一個讀悲觀鎖
        try {
          currentX = x; // 將兩個字段讀入本地局部變量
          currentY = y; // 將兩個字段讀入本地局部變量
        } finally {
           sl.unlockRead(stamp);
        }
     }
     return Math.sqrt(currentX * currentX + currentY * currentY);
   }
//下面是悲觀讀鎖案例
   void moveIfAtOrigin(double newX, double newY) { // upgrade
     // Could instead start with optimistic, not read mode
     long stamp = sl.readLock();
     try {
       while (x == 0.0 && y == 0.0) { //循環,檢查當前狀態是否符合
         long ws = sl.tryConvertToWriteLock(stamp); //將讀鎖轉爲寫鎖
         if (ws != 0L) { //這是確認轉爲寫鎖是否成功
           stamp = ws; //若是成功 替換票據
           x = newX; //進行狀態改變
           y = newY; //進行狀態改變
           break;
         }
         else { //若是不能成功轉換爲寫鎖
           sl.unlockRead(stamp); //咱們顯式釋放讀鎖
           stamp = sl.writeLock(); //顯式直接進行寫鎖 而後再經過循環再試
         }
       }
     } finally {
       sl.unlock(stamp); //釋放讀鎖或寫鎖
     }
   }
 }

小結:

StampedLock要比ReentrantReadWriteLock更加廉價,也就是消耗比較小。

StampedLock與ReadWriteLock性能對比

下圖是和ReadWritLock相比,在一個線程狀況下,是讀速度其4倍左右,寫是1倍。

下圖是六個線程狀況下,讀性能是其幾十倍,寫性能也是近10倍左右:

下圖是吞吐量提升:

總結

一、synchronized是在JVM層面上實現的,不但能夠經過一些監控工具監控synchronized的鎖定,並且在代碼執行時出現異常,JVM會自動釋放鎖定;

二、ReentrantLock、ReentrantReadWriteLock,、StampedLock都是對象層面的鎖定,要保證鎖定必定會被釋放,就必須將unLock()放到finally{}中;

三、StampedLock 對吞吐量有巨大的改進,特別是在讀線程愈來愈多的場景下;

四、StampedLock有一個複雜的API,對於加鎖操做,很容易誤用其餘方法;

五、當只有少許競爭者的時候,synchronized是一個很好的通用的鎖實現;

六、當線程增加可以預估,ReentrantLock是一個很好的通用的鎖實現;

StampedLock 能夠說是Lock的一個很好的補充,吞吐量以及性能上的提高足以打動不少人了,但並非說要替代以前Lock的東西,畢竟他仍是有些應用場景的,起碼API比StampedLock容易入手,下篇博文爭取更新快一點,可能會是Nashorn的內容,這裏容許我先賣個關子。。。

相關文章
相關標籤/搜索