ReentrantLock 中的 4 個坑!

JDK 1.5 以前 synchronized 的性能是比較低的,但在 JDK 1.5 中,官方推出一個重量級功能 Lock,一舉改變了 Java 中鎖的格局。JDK 1.5 以前當咱們談到鎖時,只能使用內置鎖 synchronized,但現在咱們鎖的實現又多了一種顯式鎖 Lock。java

前面的文章咱們已經介紹了 synchronized,詳見如下列表:
《synchronized 加鎖 this 和 class 的區別!》
《synchronized 優化手段之鎖膨脹機制!》
《synchronized 中的 4 個優化,你知道幾個?》segmentfault

因此本文我們重點來看 Lock。安全

Lock 簡介

Lock 是一個頂級接口,它的全部方法以下圖所示:
image.png
它的子類列表以下:
image.png
咱們一般會使用 ReentrantLock 來定義其實例,它們之間的關聯以下圖所示:
image.png併發

PS:Sync 是同步鎖的意思,FairSync 是公平鎖,NonfairSync 是非公平鎖。

ReentrantLock 使用

學習任何一項技能都是先從使用開始的,因此咱們也不例外,我們先來看下 ReentrantLock 的基礎使用:ide

public class LockExample {
    // 建立鎖對象
    private final ReentrantLock lock = new ReentrantLock();
    public void method() {
        // 加鎖操做
        lock.lock();
        try {
            // 業務代碼......
        } finally {
            // 釋放鎖
            lock.unlock();
        }
    }
}

ReentrantLock 在建立以後,有兩個關鍵性的操做:post

  • 加鎖操做:lock()
  • 釋放鎖操做:unlock()性能

    ReentrantLock 中的坑

    1.ReentrantLock 默認爲非公平鎖

    不少人會認爲(尤爲是新手朋友),ReentrantLock 默認的實現是公平鎖,其實並不是如此,ReentrantLock 默認狀況下爲非公平鎖(這主要是出於性能方面的考慮),好比下面這段代碼:學習

    import java.util.concurrent.locks.ReentrantLock;
    
    public class LockExample {
      // 建立鎖對象
      private static final ReentrantLock lock = new ReentrantLock();
    
      public static void main(String[] args) {
          // 定義線程任務
          Runnable runnable = new Runnable() {
              @Override
              public void run() {
                  // 加鎖
                  lock.lock();
                  try {
                      // 打印執行線程的名字
                      System.out.println("線程:" + Thread.currentThread().getName());
                  } finally {
                      // 釋放鎖
                      lock.unlock();
                  }
              }
          };
          // 建立多個線程
          for (int i = 0; i < 10; i++) {
              new Thread(runnable).start();
          }
      }
    }

    以上程序的執行結果以下:
    image.png
    從上述執行的結果能夠看出,ReentrantLock 默認狀況下爲非公平鎖。由於線程的名稱是根據建立的前後順序遞增的,因此若是是公平鎖,那麼線程的執行應該是有序遞增的,但從上述的結果能夠看出,線程的執行和打印是無序的,這說明 ReentrantLock 默認狀況下爲非公平鎖。優化

想要將 ReentrantLock 設置爲公平鎖也很簡單,只須要在建立 ReentrantLock 時,設置一個 true 的構造參數就能夠了,以下代碼所示:this

import java.util.concurrent.locks.ReentrantLock;

public class LockExample {
    // 建立鎖對象(公平鎖)
    private static final ReentrantLock lock = new ReentrantLock(true);

    public static void main(String[] args) {
        // 定義線程任務
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                // 加鎖
                lock.lock();
                try {
                    // 打印執行線程的名字
                    System.out.println("線程:" + Thread.currentThread().getName());
                } finally {
                    // 釋放鎖
                    lock.unlock();
                }
            }
        };
        // 建立多個線程
        for (int i = 0; i < 10; i++) {
            new Thread(runnable).start();
        }
    }
}

以上程序的執行結果以下:
image.png
從上述結果能夠看出,當咱們顯式的給 ReentrantLock 設置了 true 的構造參數以後,ReentrantLock 就變成了公平鎖,線程獲取鎖的順序也變成有序的了。

其實從 ReentrantLock 的源碼咱們也能夠看出它到底是公平鎖仍是非公平鎖,ReentrantLock 部分源碼實現以下:

public ReentrantLock() {
     sync = new NonfairSync();
 }
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

從上述源碼中能夠看出,默認狀況下 ReentrantLock 會建立一個非公平鎖,若是在建立時顯式的設置構造參數的值爲 true 時,它就會建立一個公平鎖。

2.在 finally 中釋放鎖

使用 ReentrantLock 時必定要記得釋放鎖,不然就會致使該鎖一直被佔用,其餘使用該鎖的線程則會永久的等待下去,因此咱們在使用 ReentrantLock 時,必定要在 finally 中釋放鎖,這樣就能夠保證鎖必定會被釋放。

反例

import java.util.concurrent.locks.ReentrantLock;

public class LockExample {
    // 建立鎖對象
    private static final ReentrantLock lock = new ReentrantLock();
    public static void main(String[] args) {
        // 加鎖操做
        lock.lock();
        System.out.println("Hello,ReentrantLock.");
        // 此處會報異常,致使鎖不能正常釋放
        int number = 1 / 0;
        // 釋放鎖
        lock.unlock();
        System.out.println("鎖釋放成功!");
    }
}

以上程序的執行結果以下:
image.png
從上述結果能夠看出,當出現異常時鎖未被正常釋放,這樣就會致使其餘使用該鎖的線程永久的處於等待狀態。

正例

import java.util.concurrent.locks.ReentrantLock;

public class LockExample {
    // 建立鎖對象
    private static final ReentrantLock lock = new ReentrantLock();
    public static void main(String[] args) {
        // 加鎖操做
        lock.lock();
        try {
            System.out.println("Hello,ReentrantLock.");
            // 此處會報異常
            int number = 1 / 0;
        } finally {
            // 釋放鎖
            lock.unlock();
            System.out.println("鎖釋放成功!");
        }
    }
}

以上程序的執行結果以下:
image.png
從上述結果能夠看出,雖然方法中出現了異常狀況,但並不影響 ReentrantLock 鎖的釋放操做,這樣其餘使用此鎖的線程就能夠正常獲取並運行了。

3.鎖不能被釋放屢次

lock 操做的次數和 unlock 操做的次數必須一一對應,且不能出現一個鎖被釋放屢次的狀況,由於這樣就會致使程序報錯。

反例

一次 lock 對應了兩次 unlock 操做,致使程序報錯並終止執行,示例代碼以下:

import java.util.concurrent.locks.ReentrantLock;

public class LockExample {
    // 建立鎖對象
    private static final ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
        // 加鎖操做
        lock.lock();
        
        // 第一次釋放鎖
        try {
            System.out.println("執行業務 1~");
            // 業務代碼 1......
        } finally {
            // 釋放鎖
            lock.unlock();
            System.out.println("鎖釋鎖");
        }

        // 第二次釋放鎖
        try {
            System.out.println("執行業務 2~");
            // 業務代碼 2......
        } finally {
            // 釋放鎖
            lock.unlock();
            System.out.println("鎖釋鎖");
        }
        // 最後的打印操做
        System.out.println("程序執行完成.");
    }
}

以上程序的執行結果以下:
image.png
從上述結果能夠看出,執行第 2 個 unlock 時,程序報錯並終止執行了,致使異常以後的代碼都未正常執行。

4.lock 不要放在 try 代碼內

在使用 ReentrantLock 時,須要注意不要將加鎖操做放在 try 代碼中,這樣會致使未加鎖成功就執行了釋放鎖的操做,從而致使程序執行異常。

反例

import java.util.concurrent.locks.ReentrantLock;

public class LockExample {
    // 建立鎖對象
    private static final ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
        try {
            // 此處異常
            int num = 1 / 0;
            // 加鎖操做
            lock.lock();
        } finally {
            // 釋放鎖
            lock.unlock();
            System.out.println("鎖釋鎖");
        }
        System.out.println("程序執行完成.");
    }
}

以上程序的執行結果以下:
image.png
從上述結果能夠看出,若是將加鎖操做放在 try 代碼中,可能會致使兩個問題:

  1. 未加鎖成功就執行了釋放鎖的操做,從而致使了新的異常;
  2. 釋放鎖的異常會覆蓋程序原有的異常,從而增長了排查問題的難度。

總結

本文介紹了 Java 中的顯式鎖 Lock 及其子類 ReentrantLock 的使用和注意事項,Lock 在 Java 中佔據了鎖的半壁江山,但在使用時卻要注意 4 個問題:

  1. 默認狀況下 ReentrantLock 爲非公平鎖而非公平鎖;
  2. 加鎖次數和釋放鎖次數必定要保持一致,不然會致使線程阻塞或程序異常;
  3. 加鎖操做必定要放在 try 代碼以前,這樣能夠避免未加鎖成功又釋放鎖的異常;
  4. 釋放鎖必定要放在 finally 中,不然會致使線程阻塞。

本系列推薦文章

  1. 線程的 4 種建立方法和使用詳解!
  2. Java中用戶線程和守護線程區別這麼大?
  3. 深刻理解線程池 ThreadPool
  4. 線程池的7種建立方式,強烈推薦你用它...
  5. 池化技術到達有多牛?看了線程和線程池的對比嚇我一跳!
  6. 併發中的線程同步與鎖
  7. synchronized 加鎖 this 和 class 的區別!
  8. volatile 和 synchronized 的區別
  9. 輕量級鎖必定比重量級鎖快嗎?
  10. 這樣終止線程,居然會致使服務宕機?
  11. SimpleDateFormat線程不安全的5種解決方案!
  12. ThreadLocal很差用?那是你沒用對!
  13. ThreadLocal內存溢出代碼演示和緣由分析!
  14. Semaphore自白:限流器用我就對了!
  15. CountDownLatch:別浪,等人齊再團!
  16. CyclicBarrier:人齊了,司機就能夠發車了!
  17. synchronized 優化手段之鎖膨脹機制
  18. synchronized 中的 4 個優化,你知道幾個?
關注公號「Java中文社羣」查看更多有意思、漲知識的 Java 併發文章。
相關文章
相關標籤/搜索