1. 程式人生 > >並發編程之 Java 三把鎖

並發編程之 Java 三把鎖

設計原理 由於 hand timeunit 訪問 什麽是 inter 提高 指令

前言

今天我們繼續學習並發。在之前我們學習了 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 就不要用,用的不好就會導致死鎖。

本站文轉載/出處:http://thinkinjava.cn/article/36

並發編程之 Java 三把鎖