Java核心技術-鎖

2020-10-12 12:00:35

日常說到高並行往往針對共用資源進行讀寫操作很容易得到錯誤的結果,這個時候就需要應用到各種個樣的鎖,本文通過4種鎖進行分享:

  • synchronized
  • ReentrantLock(可重入鎖)
  • ReentrantReadWriteLock(讀寫鎖)
  • StampedLock(戳鎖)

Part-1:synchronized

  • 同步程式碼塊
public void testSynchronizedCode() {
    synchronized (lockObject) {
        System.out.println("同步程式碼塊");
    }
}

執行:javap -verbose testSynchronized.class

public void testSynchronizedCode();
descriptor: ()V
flags: ACC_PUBLIC
Code:
  stack=2, locals=3, args_size=1
     0: aload_0
     1: getfield      #3                  // Field lockObject:Ljava/lang/Object;
     4: dup
     5: astore_1
     6: monitorenter
     7: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
    10: ldc           #9                  // String 同步程式碼塊
    12: invokevirtual #8                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
    15: aload_1
    16: monitorexit
    17: goto          25
    20: astore_2
    21: aload_1
    22: monitorexit
    23: aload_2
    24: athrow
    25: return
  Exception table:
     from    to  target type
         7    17    20   any
        20    23    20   any

進入程式碼塊之前先通過monitorenter獲取monitor鎖,如果程式碼塊執行過程中沒有異常通過monitorexit釋放monitor鎖,
異常則通過monitorexit鎖釋放monitor鎖

  • 同步方法塊
public synchronized void testSynchronizedMethod() {
    System.out.println("同步方法");
}

執行:javap -verbose testSynchronized.class

public synchronized void testSynchronizedMethod();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
  stack=2, locals=1, args_size=1
     0: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
     3: ldc           #7                  // String 同步方法
     5: invokevirtual #8                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
     8: return
  LineNumberTable:
    line 18: 0
    line 19: 8
  LocalVariableTable:
    Start  Length  Slot  Name   Signature
        0       9     0  this   Llsf/study/testSynchronized;

Part-2:ReentrantLock可重入鎖

// 引數fair 代表是否公平鎖,預設為false
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

//重入鎖配合Condition進行使用實現等待功能
private final ReentrantLock reentrantLock = new ReentrantLock(true);
private final Condition addCondition = reentrantLock.newCondition();
private final Condition subCondition = reentrantLock.newCondition();
private int count = 0;

public void add() {
    reentrantLock.lock();
    try {
        while (count >= 10) {
            System.out.println("加法-等待區");
            addCondition.await();
        }
        count++;
        System.out.println("加---結果:" + count);
        subCondition.signal();
    } catch (Exception ex) {
        ex.printStackTrace();
    } finally {
        reentrantLock.unlock();
    }
}

public void sub() {
    reentrantLock.lock();
    try {
        while (count <= 0) {
            System.out.println("減法-等待區");
            subCondition.await();
        }
        count--;
        System.out.println("減---結果:" + count);
        addCondition.signal();
    } catch (Exception ex) {
        ex.printStackTrace();
    } finally {
        reentrantLock.unlock();
    }
}

Part-3:ReentrantReadWriteLock可重入讀寫鎖

  • 鎖升級:ReentrantReadWriteLock中不支援
ReentrantReadWriteLock reentrantReadWriteLock=new ReentrantReadWriteLock();
reentrantReadWriteLock.readLock().lock();
System.out.println("讀鎖獲取完畢");
reentrantReadWriteLock.writeLock().lock();
System.out.println("寫鎖獲取完畢");
執行結果:
讀鎖獲取完畢
  • 鎖降級:釋放鎖的順序可以調整
ReentrantReadWriteLock reentrantReadWriteLock=new ReentrantReadWriteLock();
reentrantReadWriteLock.writeLock().lock();
System.out.println("寫鎖獲取完畢");
reentrantReadWriteLock.readLock().lock();
System.out.println("讀鎖獲取完畢");
reentrantReadWriteLock.writeLock().unlock();
System.out.println("寫鎖釋放完畢");
reentrantReadWriteLock.readLock().unlock();
System.out.println("讀鎖釋放完畢");
reentrantReadWriteLock.writeLock().lock();
System.out.println("寫鎖獲取完畢");

執行結果:
寫鎖獲取完畢
讀鎖獲取完畢
寫鎖釋放完畢
讀鎖釋放完畢
寫鎖獲取完畢

總結如下:讀讀共用、寫讀互斥,讀寫互斥、寫寫互斥

Part-4:StampedLock戳鎖

StampedLock和ReadWriteLock相比,改進讀的過程中也允許獲取寫鎖後寫入!我們讀的資料就可能不一致,
需要額外程式碼判斷讀的過程中是否有寫入,這種讀鎖是一種樂觀鎖。
通過StampedLocked類提供樣例程式碼進行分析

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);
        }
    }
}

ReentrantReadWriteLock其他執行緒嘗試獲取寫鎖的時候,會被阻塞,
StampedLock在樂觀獲取鎖後,其他執行緒嘗試獲取寫鎖,也不會被阻塞,這其實是對讀鎖的優化,
在獲取樂觀讀鎖後,還需要對結果進行校驗。

Part-5:總結

  • Synchronized:在日常使用種無需關注鎖的釋放,並且是原生內容後期優化空間很大,一般開發種也最常用
  • ReentrantLock:鎖的細粒度和靈活度,可以指定鎖分配策略(公平/非公平)
  • ReentrantReadWriteLock:在讀多寫少的場景比較合適
  • StampedLock:Jdk1.8版本新增功能,效能優於ReentrantReadWriteLock

參考資料

  • https://blog.takipi.com/java-8-stampedlocks-vs-readwritelocks-and-synchronized/
  • https://www.liaoxuefeng.com/wiki/1252599548343744/1309138673991714
  • JDK原始碼