1. 程式人生 > 實用技巧 >【併發程式設計】4.JUC中常用的鎖

【併發程式設計】4.JUC中常用的鎖

JUC及java.util.concurrent的簡稱,在這個包中增加了在併發程式設計中很常用的工具類,用於定義類似於執行緒的自定義子系統,包括執行緒池,非同步 IO 和輕量級任務框架,還提供了設計用於多執行緒上下文中。通過她們能夠很好地幫助我們在開發中提高一些程式的效能。

1.Lock與Condition

  • condition
    Lock與condition是Java中管程模型除了synchronized的另一套實現,不同是的支援多條件佇列。Lock&Condition 實現的管程裡只能使用前面的 await()、signal()、signalAll()。
    synchronized的管程模型

    Lock&Condition的管程模型

Condition的使用 需要獲取到鎖

public class BlockedQueue<T>{
    final Lock lock =
            new ReentrantLock();
    // 條件變數:佇列不滿
    final Condition notFull =
            lock.newCondition();
    // 條件變數:佇列不空
    final Condition notEmpty =
            lock.newCondition();
    // 入隊
    void enq(T x) {
        lock.lock();
        try {
            while (佇列已滿){
                // 等待佇列不滿
                notFull.await();
            }
            // 省略入隊操作...
            // 入隊後, 通知可出隊
            notEmpty.signal();
        }finally {
            lock.unlock();
        }
    }
    // 出隊
    void deq(){
        lock.lock();
        try {
            while (佇列已空){
                // 等待佇列不空
                notEmpty.await();
            }
            // 省略出隊操作...
            // 出隊後,通知可入隊
            notFull.signal();
        }finally {
            lock.unlock();
        }
    }
}

上述程式碼中就是有兩個條件變數,對應兩個條件佇列,分別進行入隊和出隊。 佇列已滿通知等待出隊操作的執行緒,佇列已空則通知等待入隊操作的執行緒。
如果是使用synchronized關鍵字實現的話呼叫notify/notifyAll方法應該是通知唯一的等待佇列裡的所有執行緒,然後判斷是執行入隊或者是出隊。

  • ReentrantLock
    上述程式碼中使用的鎖就是 ReentrantLock 即為可重入鎖,就是再已經獲取鎖的情況下可以再次獲取到同一把鎖。
class X {
    private final Lock rtl = new ReentrantLock();
    int value;
    public int get() {
        // 獲取鎖
        rtl.lock(); 
        try {
            return value;
        } finally {
            // 保證鎖能釋放
            rtl.unlock();
        }
    }
    public void addOne() {
        // 獲取鎖
        rtl.lock();
        try {
            value = 1 + get(); //get方法再次獲取鎖
        } finally {
            // 保證鎖能釋放
            rtl.unlock();
        }
    }
}
  • ReadWriteLock
    讀寫鎖,適用於讀多寫少的場景,例如快取
    ReadWriteLock 是介面
    ReentrantReadWriteLock 是實現類
  1. 允許多個執行緒同時讀共享變數; (優於互斥鎖的關鍵) 如果有執行緒正在讀,寫執行緒需要等待讀執行緒釋放鎖後才能獲取寫鎖,即讀的過程中不允許寫,這是一種悲觀的讀鎖。
  2. 只允許一個執行緒寫共享變數;
  3. 如果一個寫執行緒正在執行寫操作,此時禁止讀執行緒讀共享變數 (區別於對讀操作不加鎖)
/**
* className: Cache
* create by: zhujun
* description: 使用讀寫鎖實現的快取工具類
* 讀寫鎖 使用於讀多寫少的併發場景
* create time: 2019/7/23 11:21
*/
public class Cache<K,V> {
    //hashmap 儲存資料
    final HashMap<K,V> hashMap = new HashMap<>();
    final ReentrantReadWriteLock reentrantReadWriteLock  = new ReentrantReadWriteLock();
    final Lock readLock = reentrantReadWriteLock.readLock();
    final Lock writeLock = reentrantReadWriteLock.writeLock();

    /**
     * 存入資料
     * @param key
     * @param value
     */
    void set(K key,V value){
        writeLock.lock();
        try{
            hashMap.put(key,value);
        }finally {
            writeLock.unlock();
        }
    }

    /**
     * 讀取資料
     * @param key
     * @return
     */
    V get(K key){
      //讀鎖
      readLock.lock();
      try{
          return hashMap.get(key);
      }finally {
          readLock.unlock();
      }
    }
}

注意點:1.寫鎖支援條件變數,讀鎖不支援條件變數
2.支援鎖的降級,不支援鎖的升級
鎖的升級

        read.lock();
        try {
            v = m.get(key);//驗證值是否存在         
            //獲取寫鎖從資料庫中更新快取                       //鎖的升級
        } finally{
            read.unlock();
        }

鎖的降級

      writeLock.lock();
      try{
              ....
          readLock.lock();//釋放寫鎖之前 釋放讀鎖
      }finally{
          writeLock.unlock();
          readLock.lock()
      }
  • StampedLock
    Java 1.8提供,效能優於ReadWriteLock 支援三種鎖模式:寫鎖,樂觀讀鎖,悲觀讀鎖。
    因為 ReadWriteLock 是悲觀讀鎖,讀取的時候不允許寫入,StampedLock為了提高效能提供了樂觀讀鎖,讀的過程中大概率不會有寫入。

首先我們通過tryOptimisticRead()獲取一個樂觀讀鎖,並返回版本號。
接著進行讀取,讀取完成後,我們通過validate()去驗證版本號,如果在讀取過程中沒有寫入,版本號不變,驗證成功,我們就可以放心地繼續後續操作。
如果在讀取過程中有寫入,版本號會發生變化,驗證將失敗。在失敗的時候,我們再通過獲取悲觀讀鎖再次讀取。

/**
* className: Point
* create by: zhujun
* description:StampLock 的樂觀讀與悲觀讀鎖
* create time: 2019/7/30 17:01
*/
public class Point {
    private int x;
    private int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    final StampedLock s1 = new StampedLock();


    //計算帶到原點的舉例
    double getDistance() throws InterruptedException {
       long stamp = s1.tryOptimisticRead();//樂觀讀
        System.out.println("樂觀讀stamp:"+stamp);
        int curX = x;
        int cutY = y;
        System.out.println("讀取成功:"+x+","+y);
        Thread.sleep(1000);//睡眠 方便測試時進行寫操作
       if(!s1.validate(stamp)){//通過驗證stamp
            //期間有寫操作 升級為悲觀讀鎖 等待寫操作完成
           stamp =  s1.readLock();
            try{
                System.out.println("存在寫操作,重新讀取x,y座標");
                System.out.println("此時stamp:"+stamp);
                curX = x;
                cutY = y;
            }finally {
                s1.unlockRead(stamp);
            }
       }
       return  Math.sqrt(curX*curX+cutY*cutY);
    }



    void reLocation(int x,int y){
        //寫操作 寫鎖
        long stamp = s1.writeLock();
        System.out.println("寫鎖stamp:"+stamp);
        try{
            this.x = x;
            this.y = y;
            System.out.println("重定位成功:"+x+","+y);
        }finally {
            s1.unlockWrite(stamp);
        }
    }
}
public class PointTest {
    public static void main(String[] args) throws InterruptedException {
        Point p = new Point(1,2);
        //執行緒1 計算距離
        Thread th1 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    double distance = p.getInstance();
                    System.out.println("距離原點的舉例:"+distance);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        //執行緒2 重寫座標
        Thread th2 = new Thread(new Runnable() {
            @Override
            public void run() {
                p.reLocation(3,4);
            }
        });
        th1.start();
        th2.start();
    }
}

注意:1.StampedLock 不支援可重入
2.如果需要支援中斷功能,一定使用可中斷的悲觀讀鎖 readLockInterruptibly()和寫鎖 writeLockInterruptibly()。