併發程式設計之 Java 三把鎖

莫那·魯道發表於2019-02-27

前言

今天我們繼續學習併發。在之前我們學習了 JMM 的知識,知道了在併發程式設計中,為了保證執行緒的安全性,需要保證執行緒的原子性,可見性,有序性。其中,synchronized 高頻出現,因為他既保證了原子性,也保證了可見性和有序性。為什麼,因為 synchronized 是鎖。通過鎖,可以讓原本並行的任務變成序列。然而如你所見,這也導致了嚴重的效能受損。因此,不到萬不得已,不要使用鎖,特別是吞吐量要求特別高的 WEB 伺服器。如果鎖住,效能將呈幾何級下降。

但我們仍然需要鎖,在某些操作共享變數的時刻,仍然需要鎖來保證資料的準確性。而Java 世界有 3 把鎖,今天我們主要說說這 3 把鎖的用法。

  1. synchronized 關鍵字
  2. ReentrantLock 重入鎖
  3. ReadWriteLock 讀寫鎖

1. synchronized 關鍵字

synchronized 可以說是我們學習併發的時候第一個學習的關鍵字,該關鍵字粗魯有效,通常是初級程式設計師最愛使用的,也因此會經常導致一些效能損失和死鎖問題。

下面是 synchronized 的 3 個用法:

  void resource1() {
    synchronized ("resource1") {
      System.out.println("作用在同步塊中");
    }
  }

  synchronized void resource3() {
    System.out.println("作用在例項方法上");
  }

  static synchronized void resource2() {
      System.out.println("作用在靜態方法上");
  }
複製程式碼

整理以下這個關鍵字的用法:

  1. 指定加鎖物件(程式碼塊):對給定物件加鎖,進入同步程式碼前要獲得給定物件的鎖。
  2. 直接作用於例項方法:相當於對當前例項加鎖,進入同步程式碼前要獲得當前例項的鎖。
  3. 直接作用於靜態方法:相當於對當前類加鎖,進入同步程式碼塊前要獲得當前類的鎖。

synchronized 在發生異常的時候會釋放鎖,這點需要注意一下。

synchronized 修飾的程式碼在生產位元組碼的時候會有 monitorenter 和 monitorexit 指令,而這兩個指令在底層呼叫了虛擬機器8大指令中其中兩個指令—–lock 和 unlock。

synchronized 雖然萬能,但是還是有很多侷限性,比如使用它經常會發生死鎖,且無法處理,所以 Java 在 1.5版本的時候,加入了另一個鎖 Lock 介面。我們看看該介面下的有什麼。

2. ReentrantLock 重入鎖

JDK 在 1.5 版本新增了java.util.concurrent 包,有併發大師 Doug Lea 編寫,其中程式碼鬼斧神工。值得我們好好學習,包括今天說的 Lock。

Lock 介面


/**
 * @since 1.5
 * @author Doug Lea
 */
public interface Lock {

    void lock();

    void lockInterruptibly() throws InterruptedException;

    boolean tryLock();

    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

    void unlock();

    Condition newCondition();

複製程式碼

void lock(); 獲得鎖

void lockInterruptibly() ;

boolean tryLock(); 嘗試獲取鎖,如果獲取不到,立刻返回false。

boolean tryLock(long time, TimeUnit unit) 在

void unlock(); 在給定的時間裡等待鎖,超過時間則自動放棄

Condition newCondition(); 獲取一個重入鎖的好搭檔,搭配重入鎖使用

上面說了Lock的機構抽象方法,那麼 Lock 的實現是什麼呢?標準實現了 ReentrantLock, ReadWriteLock。也就是我們今天講的重入鎖和讀寫鎖。我們先講重入鎖。

先來一個簡單的例子:

package cn.think.in.java.lock;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class LockText implements Runnable {

  /**
   * Re - entrant - Lock
   * 重入鎖,表示在單個執行緒內,這個鎖可以反覆進入,也就是說,一個執行緒可以連續兩次獲得同一把鎖。
   * 如果你不允許重入,將導致死鎖。注意,lock 和 unlock 次數一定要相同,如果不同,就會導致死鎖和監視器異常。
   *
   * synchronized 只有2種情況:1繼續執行,2保持等待。
   */
  static Lock lock = new ReentrantLock();
  static int i;

  public static void main(String[] args) throws InterruptedException {
    LockText lockText = new LockText();
    Thread t1 = new Thread(lockText);
    Thread t2 = new Thread(lockText);
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.println(i);
  }

  @Override
  public void run() {
    for (int j = 0; j < 1000000; j++) {
      lock.lock();
      try {
        i++;
      } finally {
        // 因為lock 如果發生了異常,是不會釋放鎖的,所以必須在 finally 塊中釋放鎖
        // synchronized 發生異常會主動釋放鎖
        lock.unlock();
      }
    }
  }
}


複製程式碼

在上面的程式碼中,我們使用了try 塊中保護了臨界資源 i 的操作。可以看到, 重入鎖不管是開啟鎖還是釋放鎖都是顯示的,其中需要注意的一點是,重入鎖執行時如果發生了異常,不會像 synchronized 釋放鎖,因此需要在 finally 中釋放鎖。否則將產生死鎖。

什麼是重入鎖?鎖就是鎖唄,為什麼叫重入鎖?之所以這麼叫,那是因為這種鎖是可以反覆進入的(一個執行緒),大家看看下面的程式碼:

lock.lock();
lock.lock();
tyr{
  i++;
} finally{
  lock.unlock();
  lock.unlock();
}

複製程式碼

在這種情況下,一個執行緒連續兩次獲得兩把鎖,這是允許的。如果不允許這麼操作,那麼同一個執行緒咋i第二次獲得鎖是,將會和自己產生死鎖。當然,需要注意的是,如果你多次獲得了鎖,那麼也要相同的釋放多次,如果釋放鎖的次數多了,就會得到一個 IllegalMonitorStateException 異常,反之,如果釋放鎖的次數少了,那麼相當於這個執行緒還沒有釋放鎖,其他執行緒也就無法進入臨界區。

重入鎖能夠實現 synchronized 的所有功能,而且功能更為強大,我們看看有哪些功能。

中斷響應

對於 synchronized 來說,如果一個執行緒在等待鎖,那麼結果只有2種,要麼他獲得這把鎖繼續執行,要麼他就保持等待。沒有第三種可能,那如果我有一個需求:需要執行緒在等待的時候中斷執行緒,synchronizded 是做不到的。而重入鎖可以做到,就是 lockInterruptibly 方法,該方法可以獲取鎖,並且在獲取鎖的過程種支援執行緒中斷,也就是說,如果呼叫了執行緒中斷方法,那麼就會丟擲異常。相對於 lock 方法,是不是更為強大?還是寫個例子吧:

package cn.think.in.java.lock;

import java.util.concurrent.locks.ReentrantLock;

/**
 * ReentrantLock(重入鎖)
 *
 * Condition(條件)
 *
 * ReadWriteLock(讀寫鎖)
 */
public class IntLock implements Runnable {

  /**
   * 預設是不公平的鎖,設定為 true 為公平鎖
   *
   * 公平:在多個執行緒的爭用下,這些鎖傾向於將訪問權授予等待時間最長的執行緒;
   * 使用公平鎖的程式在許多執行緒訪問時表現為很低的總體吞吐量(即速度很慢,常常極其慢)
   * 還要注意的是,未定時的 tryLock 方法並沒有使用公平設定
   *
   * 不公平:此鎖將無法保證任何特定訪問順序
   *
   * 拾遺:1 該類的序列化與內建鎖的行為方式相同:一個反序列化的鎖處於解除鎖定狀態,不管它被序列化時的狀態是怎樣的。
   *      2.此鎖最多支援同一個執行緒發起的 2147483648 個遞迴鎖。試圖超過此限制會導致由鎖方法丟擲的 Error。
   */
  static ReentrantLock lock1 = new ReentrantLock(true);
  static ReentrantLock lock2 = new ReentrantLock();
  int lock;

  /**
   * 控制加鎖順序,方便製造死鎖
   * @param lock
   */
  public IntLock(int lock) {
    this.lock = lock;
  }

  /**
   * lockInterruptibly 方法: 獲得鎖,但優先響應中斷
   * tryLock 嘗試獲得鎖,不等待
   * tryLock(long time , TimeUnit unit) 嘗試獲得鎖,等待給定的時間
   */
  @Override
  public void run() {
    try {
      if (lock == 1) {
        // 如果當前執行緒未被中斷,則獲取鎖。
        lock1.lockInterruptibly();// 即在等待鎖的過程中,可以響應中斷。
        try {
          Thread.sleep(500);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
        // 試圖獲取 lock 2 的鎖
        lock2.lockInterruptibly();
      } else {

        lock2.lockInterruptibly();
        try {
          Thread.sleep(500);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
        // 該執行緒在企圖獲取 lock1 的時候,會死鎖,但被呼叫了 thread.interrupt 方法,導致中斷。中斷會放棄鎖。
        lock1.lockInterruptibly();
      }

    } catch (InterruptedException e) {
      e.printStackTrace();
    } finally {
      if (lock1.isHeldByCurrentThread()) {
        lock1.unlock();
      }

      // 查詢當前執行緒是否保持此鎖。
      if (lock2.isHeldByCurrentThread()) {
        lock2.unlock();
      }

      System.out.println(Thread.currentThread().getId() + ": 執行緒退出");
    }
  }


  public static void main(String[] args) throws InterruptedException {

    /**
     * 這部分程式碼主要是針對 lockInterruptibly 方法,該方法線上程發生死鎖的時候可以中斷執行緒。讓執行緒放棄鎖。
     * 而 synchronized 是沒有這個功能的, 他要麼獲得鎖繼續執行,要麼繼續等待鎖。
     */

    IntLock r1 = new IntLock(1);
    IntLock r2 = new IntLock(2);
    Thread t1 = new Thread(r1);
    Thread t2 = new Thread(r2);
    t1.start();
    t2.start();
    Thread.sleep(1000);
    // 中斷其中一個執行緒(只有執行緒在等待鎖的過程中才有效)
    // 如果執行緒已經拿到了鎖,中斷是不起任何作用的。
    // 注意:這點 synchronized 是不能實現此功能的,synchronized 在等待過程中無法中斷
    t2.interrupt();
    // t2 執行緒中斷,丟擲異常,並放開鎖。沒有完成任務
    // t1 順利完成任務。
  }
}

複製程式碼

在上面的程式碼種,我們分別啟動兩個執行緒,製造了一個死鎖,如果是 synchronized 是無法解除這個死鎖的,這個時候重入鎖的威力就出來了,我們呼叫執行緒的 interrupt 方法,中斷執行緒,我們說,這個方法線上程 sleep,join ,wait 的時候,都會導致異常,這裡也一羊,由於我們使用的 lock 的 lockInterruptibly 方法,該方法就像我們剛說的那樣,在等待鎖的時候,如果執行緒被中斷了,就會出現異常,同時呼叫了 finally 種的 unlock 方法,注意,我們在 finally 中用 isHeldByCurrentThread 判斷當前執行緒是否持有此鎖,這是一種預防措施,放置執行緒沒有持有此鎖,導致出現 monitorState 異常。

鎖申請

除了等待通知之外,避免死鎖還有另一種方法,就是超時等待,如果超過這個時間,執行緒就放棄獲取這把鎖,這點 ,synchronized 也是不支援的。那麼,如何使用呢?

package cn.think.in.java.lock;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

public class TimeLock implements Runnable {

  static ReentrantLock lock = new ReentrantLock(false);

  @Override
  public void run() {
    try {
      // 最多等待5秒,超過5秒返回false,若獲得鎖,則返回true
      if (lock.tryLock(5, TimeUnit.SECONDS)) {
        // 鎖住 6 秒,讓下一個執行緒無法獲取鎖
        System.out.println("鎖住 6 秒,讓下一個執行緒無法獲取鎖");
        Thread.sleep(6000);
      } else {
        System.out.println("get lock failed");
      }
    } catch (InterruptedException e) {
      e.printStackTrace();
    } finally {
      if (lock.isHeldByCurrentThread()) {
        lock.unlock();
      }
    }
  }

  public static void main(String[] args) {
    TimeLock tl = new TimeLock();
    Thread t1 = new Thread(tl);
    Thread t2 = new Thread(tl);

    t1.start();
    t2.start();


  }
}

複製程式碼

上面的程式碼中,我們設定鎖的等待時間是5秒,但是在同步塊中,我們設定了6秒暫停,鎖外面的執行緒等待了5面發現還是不能獲取鎖,就會放棄。走 else 邏輯,結束執行,注意,這裡,我們在 finally 塊中依然做了判斷,如果不做判斷,就會出現 IllegalMonitorStateException 異常。

當然了,tryLock 方法也可以不帶時間引數,如果獲取不到鎖,立刻返回false,否則返回 true。該方法也是應對死鎖的一個好辦法。我們還是寫個例子:

package cn.think.in.java.lock;

import java.util.concurrent.locks.ReentrantLock;

public class TryLock implements Runnable {

  static ReentrantLock lock1 = new ReentrantLock();
  static ReentrantLock lock2 = new ReentrantLock();
  int lock;

  public TryLock(int lock) {
    this.lock = lock;
  }

  @Override
  public void run() {
    // 執行緒1
    if (lock == 1) {
      while (true) {
        // 獲取1的鎖
        if (lock1.tryLock()) {
          try {
            // 嘗試獲取2的鎖
            if (lock2.tryLock()) {
              try {
                System.out.println(Thread.currentThread().getId() + " : My Job done");
                return;
              } finally {
                lock2.unlock();
              }
            }
          } finally {
            lock1.unlock();
          }
        }
      }
    } else {
      // 執行緒2
      while (true) {
        // 獲取2的鎖
        if (lock2.tryLock()) {
          try {
            // 嘗試獲取1的鎖
            if (lock1.tryLock()) {
              try {
                System.out.println(Thread.currentThread().getId() + ": My Job done");
                return;
              } finally {
                lock1.unlock();
              }
            }
          } finally {
            lock2.unlock();
          }
        }
      }
    }
  }

  /**
   * 這段程式碼如果使用 synchronized 肯定會引起死鎖,但是由於使用 tryLock,他會不斷的嘗試, 當第一次失敗了,他會放棄,然後執行完畢,並釋放外層的鎖,這個時候就是
   * 另一個執行緒搶鎖的好時機。
   * @param args
   */
  public static void main(String[] args) {
    TryLock r1 = new TryLock(1);
    TryLock r2 = new TryLock(2);
    Thread t1 = new Thread(r1);
    Thread t2 = new Thread(r2);
    t1.start();
    t2.start();
  }
}

複製程式碼

這段程式碼如果使用 synchronized 肯定會引起死鎖,但是由於使用 tryLock,他會不斷的嘗試, 當第一次失敗了,他會放棄,然後執行完畢,並釋放外層的鎖,這個時候就是另一個執行緒搶鎖的好時機。

公平鎖和非公平鎖

大多數情況下,為了效率,鎖都是不公平的。系統在選擇鎖的時候都是隨機的,不會按照某種順序,比如時間順序,公平鎖的一大特點:他不會產生飢餓現象。只要你排隊 ,最終還是可以得到資源的。如果我們使用 synchronized ,得到的鎖就是不公平的。因此,這也是重入鎖比 synchronized 強大的一個優勢。我們同樣寫個例子:

package cn.think.in.java.lock;

import java.util.concurrent.locks.ReentrantLock;

public class FairLock implements Runnable {

  // 公平鎖和非公平鎖的結果完全不同
  /*
  * 10 獲得鎖
    10 獲得鎖
    10 獲得鎖
    10 獲得鎖
    10 獲得鎖
    10 獲得鎖
    10 獲得鎖
    10 獲得鎖
    10 獲得鎖
    10 獲得鎖
    9 獲得鎖
    9 獲得鎖
    9 獲得鎖
    9 獲得鎖
    9 獲得鎖
    9 獲得鎖
    9 獲得鎖
    9 獲得鎖
    9 獲得鎖
    9 獲得鎖
    ======================下面是公平鎖,上面是非公平鎖
    10 獲得鎖
    9 獲得鎖
    10 獲得鎖
    9 獲得鎖
    10 獲得鎖
    9 獲得鎖
    10 獲得鎖
    9 獲得鎖
    10 獲得鎖
    9 獲得鎖
    10 獲得鎖
    9 獲得鎖
    10 獲得鎖
    9 獲得鎖
    10 獲得鎖
    9 獲得鎖
    10 獲得鎖
    9 獲得鎖
    10 獲得鎖
    9 獲得鎖
    10 獲得
  *
  * */
  static ReentrantLock unFairLock = new ReentrantLock(false);
  static ReentrantLock fairLock = new ReentrantLock(true);

  @Override
  public void run() {
    while (true) {
      try {
        fairLock.lock();
        System.out.println(Thread.currentThread().getId() + " 獲得鎖");
      } finally {
        fairLock.unlock();
      }
    }
  }

  /**
   * 預設是不公平的鎖,設定為 true 為公平鎖
   *
   * 公平:在多個執行緒的爭用下,這些鎖傾向於將訪問權授予等待時間最長的執行緒;
   * 使用公平鎖的程式在許多執行緒訪問時表現為很低的總體吞吐量(即速度很慢,常常極其慢)
   * 還要注意的是,未定時的 tryLock 方法並沒有使用公平設定
   *
   * 不公平:此鎖將無法保證任何特定訪問順序,但是效率很高
   *
   */
  public static void main(String[] args) {
    FairLock fairLock = new FairLock();
    Thread t1 = new Thread(fairLock, "cxs - t1");
    Thread t2 = new Thread(fairLock, "cxs - t2");
    t1.start();
    t2.start();
  }
}


複製程式碼

重入鎖的建構函式有一個 boolean 引數,ture 表示公平,false 表示不公平,預設是不公平的,公平鎖會降低效能。程式碼中由執行結果,可以看到,公平鎖的列印順序是完全交替執行,而不公平鎖的順序完全是隨機的。注意:如果沒有特殊需求,請不要使用公平鎖,會大大降低吞吐量。

到這裡,我們總結一下重入鎖相比 synchronized 有哪些優勢:

  1. 可以線上程等待鎖的時候中斷執行緒,synchronized 是做不到的。
  2. 可以嘗試獲取鎖,如果獲取不到就放棄,或者設定一定的時間,這也是 synchroized 做不到的。
  3. 可以設定公平鎖,synchronized 預設是非公平鎖,無法實現公平鎖。

當然,大家會說, synchronized 可以通過 Object 的 wait 方法和 notify 方法實現執行緒之間的通訊,重入鎖可以做到嗎?樓主告訴大家,當然可以了! JDK 中的阻塞佇列就是用重入鎖加 他的搭檔 condition 實現的。

重入鎖的好搭檔—–Condition

還記的剛開始說 Lock 介面有一個newCondition 方法嗎,該方法就是獲取 Condition 的。該 Condition 繫結了該鎖。Condition 有哪些方法呢?我們看看:

public interface Condition {

    void await() throws InterruptedException;

    boolean await(long time, TimeUnit unit) throws InterruptedException;

    long awaitNanos(long nanosTimeout) throws InterruptedException;

    boolean await(long time, TimeUnit unit) throws InterruptedException;

    void awaitUninterruptibly();

    boolean awaitUntil(Date deadline) throws InterruptedException;

    void signal();

    void signalAll();
}

複製程式碼

看著是不是特別屬性,Condition 為了不和 Object 類的 wait 方法衝突,使用 await 方法,而 signal 方法對應的就是 notify 方法。signalAll 方法對應的就是 notifyAll 方法。其中還有一些時間限制的 await 方法,和 Object 的 wait 方法的作用相同。注意,其中有一個 awaitUninterruptibly 方法,該方法從名字可以看出,並不會響應執行緒的中斷,而 Object 的 wait 方法是會響應的。而 awaitUntil 方法就是等待到一個給定的絕對時間。除非呼叫了 signal 或者中斷了。如何使用呢?來一段程式碼吧:

package cn.think.in.java.lock.condition;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 重入鎖的好搭檔
 *
 * await 使當前執行緒等待,同時釋放當前鎖,當其他執行緒中使用 signal 或者 signalAll 方法時,執行緒會重新獲得鎖並繼續執行。
 *       或者當執行緒被中斷時,也能跳出等待,這和 Object.wait 方法很相似。
 * awaitUninterruptibly() 方法與 await 方法基本相同,但是它並不會在等待過程中響應中斷。
 * singal() 該方法用於喚醒一個在等待中的執行緒,相對的 singalAll 方法會喚醒所有在等待的執行緒,這和 Object.notify 方法很類似。
 */
public class ConditionTest implements Runnable {

  static Lock lock = new ReentrantLock();

  static Condition condition = lock.newCondition();


  @Override
  public void run() {
    try {
      lock.lock();
      // 該執行緒會釋放 lock 的鎖,也就是說,一個執行緒想呼叫 condition 的方法,必須先獲取 lock 的鎖。
      // 否則就會像 object 的 wait 方法一樣,監視器異常
      condition.await();
      System.out.println("Thread is going on");

    } catch (InterruptedException e) {
      e.printStackTrace();
    } finally {
      lock.unlock();
    }
  }

  public static void main(String[] args) throws InterruptedException {
    ConditionTest t = new ConditionTest();
    Thread t1 = new Thread(t);
    t1.start();
    Thread.sleep(1000);
    // 通知 t1 繼續執行
    // main 執行緒必須獲取 lock 的鎖,才能呼叫 condition 的方法。否則就是監視器異常,這點和 object 的 wait 方法是一樣的。
    lock.lock(); // IllegalMonitorStateException
    // 從 condition 的等待佇列中,喚醒一個執行緒。
    condition.signal();
    lock.unlock();
  }
}

複製程式碼

可以說,condition 的使用方式和 Object 類的 wait 方法的使用方式很相似,無論在哪一個執行緒中呼叫 await 或者 signal 方法,都必須獲取對應的鎖,否則會出現 IllegalMonitorStateException 異常。

到這裡,我們可以說, Condition 的實現比 Object 的 wait 和 notify 還是強一點,其中就包括了等待到指定的絕對時間,並且還有一個不受執行緒中斷影響的 awaitUninterruptibly 方法。因此,我們說,只要允許,請使用重入鎖,儘量不要使用無腦的 synchronized 。雖然在 JDK 1.6 後, synchronized 被優化了,但仍然建議使用 重入鎖。

3. ReadWriteLock 讀寫鎖

偉大的 Doug Lea 不僅僅創造了 重入鎖,還創造了 讀寫鎖。什麼是讀寫鎖呢?我們知道,執行緒不安全的原因來自於多執行緒對資料的修改,如果你不修改資料,根本不需要鎖。我們完全可以將讀寫分離,提高效能,在讀的時候不使用鎖,在寫的時候才加入鎖。這就是 ReadWriteLock 的設計原理。

那麼,如何使用呢?

package cn.think.in.java.lock;

import java.util.Random;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReadWriteLockDemo {

  static Lock lock = new ReentrantLock();
  static ReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();

  static Lock readLock = reentrantReadWriteLock.readLock();
  static Lock writeLock = reentrantReadWriteLock.writeLock();

  int value;

  public Object handleRead(Lock lock) throws InterruptedException {
    try {
      lock.lock();
      // 模擬讀操作,讀操作的耗時越多,讀寫鎖的優勢就越明顯
      Thread.sleep(1000);
      return value;
    } finally {
      lock.unlock();
    }
  }

  public void handleWrite(Lock lock, int index) throws InterruptedException {
    try {
      lock.lock();
      Thread.sleep(1000); // 模擬寫操作
      value = index;

    } finally {
      lock.unlock();
    }
  }

  public static void main(String[] args) {
    final ReadWriteLockDemo demo = new ReadWriteLockDemo();
    Runnable readRunnable = new Runnable() {
      @Override
      public void run() {
        try {
          demo.handleRead(readLock);
//          demo.handleRead(lock);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      }
    };

    Runnable writeRunnable = new Runnable() {
      @Override
      public void run() {
        try {
          demo.handleWrite(writeLock, new Random().nextInt());
//          demo.handleWrite(lock, new Random().nextInt());
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      }
    };

    /**
     * 使用讀寫鎖,這段程式只需要2秒左右
     * 使用普通的鎖,這段程式需要20秒左右。
     */

    for (int i = 0; i < 18; i++) {
      new Thread(readRunnable).start();
    }

    for (int i = 18; i < 20; i++) {
      new Thread(writeRunnable).start();
    }


  }

}

複製程式碼

使用 ReentrantReadWriteLock 的 readLock()方法可以返回讀鎖,writeLock 可以返回寫鎖,我們使用普通的的重入鎖和讀寫鎖進行測試,怎麼測試呢?

兩個迴圈:一個迴圈開啟18個執行緒去讀資料,一個迴圈開啟兩個執行緒去寫。如果使用普通的重入鎖,將耗時20秒,因為普通的重入鎖在讀的時候依然是序列的。而如果使用讀寫鎖,只需要2秒,也就是寫的時候是序列的。讀的時候是並行的,極大的提高了效能。

注意:只要涉及到寫都是序列的。比如讀寫操作,寫寫操作,都是序列的,只有讀讀操作是並行的。

讀寫鎖 ReadWriteLock 介面只有 2個方法:

Lock readLock(); 返回一個讀鎖
Lock writeLock(); 返回一個寫鎖

他的標準實現類是 ReentrantReadWriteLock 類,該類和普通重入鎖一樣,也能實現公平鎖,中斷響應,鎖申請等特性。因為他們返回的讀鎖或者寫鎖都實現了 Lock 介面。

總結

到這裡,我們已經將 Java 世界的三把鎖的使用弄清楚了,從分析的過程中我們知道了,JDK 1.5 的重入鎖完全可以代替關鍵字 synchronized ,能實現很多 synchronized 沒有的功能。比如中斷響應,鎖申請,公平鎖等,而重入鎖的搭檔 Condition 也比 Object 的wait 和notify 強大,比如有設定絕對時間的等待,還有忽略執行緒中斷的 await 方法,這些都是 synchronized 無法實現的。還有優化讀效能的 讀寫鎖,在讀的時候完全是並行的,在某些場景下,比如讀很多,寫很少,效能將是幾何級別的提升。

所以,以後,能不用 synchronzed 就不要用,用的不好就會導致死鎖。

今天的Java 三把鎖就介紹到這裡。

good luck !!!!

相關文章