并發(fā)編程之 Java 三把鎖

三把鎖

前言

今天我們繼續(xù)學(xué)習(xí)并發(fā)姚垃。在之前我們學(xué)習(xí)了 JMM 的知識念链,知道了在并發(fā)編程中,為了保證線程的安全性,需要保證線程的原子性掂墓,可見性谦纱,有序性。其中君编,synchronized 高頻出現(xiàn)跨嘉,因?yàn)樗缺WC了原子性,也保證了可見性和有序性吃嘿。為什么祠乃,因?yàn)?synchronized 是鎖。通過鎖兑燥,可以讓原本并行的任務(wù)變成串行亮瓷。然而如你所見,這也導(dǎo)致了嚴(yán)重的性能受損降瞳。因此嘱支,不到萬不得已,不要使用鎖挣饥,特別是吞吐量要求特別高的 WEB 服務(wù)器除师。如果鎖住,性能將呈幾何級下降亮靴。

但我們?nèi)匀恍枰i馍盟,在某些操作共享變量的時(shí)刻于置,仍然需要鎖來保證數(shù)據(jù)的準(zhǔn)確性茧吊。而Java 世界有 3 把鎖,今天我們主要說說這 3 把鎖的用法八毯。

  1. synchronized 關(guān)鍵字
  2. ReentrantLock 重入鎖
  3. ReadWriteLock 讀寫鎖

1. synchronized 關(guān)鍵字

synchronized 可以說是我們學(xué)習(xí)并發(fā)的時(shí)候第一個(gè)學(xué)習(xí)的關(guān)鍵字搓侄,該關(guān)鍵字粗魯有效,通常是初級程序員最愛使用的话速,也因此會經(jīng)常導(dǎo)致一些性能損失和死鎖問題讶踪。

下面是 synchronized 的 3 個(gè)用法:

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

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

  static synchronized void resource2() {
      System.out.println("作用在靜態(tài)方法上");
  }

整理以下這個(gè)關(guān)鍵字的用法:

  1. 指定加鎖對象(代碼塊):對給定對象加鎖,進(jìn)入同步代碼前要獲得給定對象的鎖泊交。
  2. 直接作用于實(shí)例方法:相當(dāng)于對當(dāng)前實(shí)例加鎖乳讥,進(jìn)入同步代碼前要獲得當(dāng)前實(shí)例的鎖。
  3. 直接作用于靜態(tài)方法:相當(dāng)于對當(dāng)前類加鎖廓俭,進(jìn)入同步代碼塊前要獲得當(dāng)前類的鎖云石。

synchronized 在發(fā)生異常的時(shí)候會釋放鎖,這點(diǎn)需要注意一下研乒。

synchronized 修飾的代碼在生產(chǎn)字節(jié)碼的時(shí)候會有 monitorenter 和 monitorexit 指令汹忠,而這兩個(gè)指令在底層調(diào)用了虛擬機(jī)8大指令中其中兩個(gè)指令-----lock 和 unlock。

synchronized 雖然萬能,但是還是有很多局限性宽菜,比如使用它經(jīng)常會發(fā)生死鎖谣膳,且無法處理,所以 Java 在 1.5版本的時(shí)候铅乡,加入了另一個(gè)鎖 Lock 接口继谚。我們看看該接口下的有什么。

2. ReentrantLock 重入鎖

JDK 在 1.5 版本新增了java.util.concurrent 包阵幸,有并發(fā)大師 Doug Lea 編寫犬庇,其中代碼鬼斧神工。值得我們好好學(xué)習(xí)侨嘀,包括今天說的 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(); 在給定的時(shí)間里等待鎖,超過時(shí)間則自動放棄

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

上面說了Lock的機(jī)構(gòu)抽象方法纽帖,那么 Lock 的實(shí)現(xiàn)是什么呢?標(biāo)準(zhǔn)實(shí)現(xiàn)了 ReentrantLock举反, ReadWriteLock懊直。也就是我們今天講的重入鎖和讀寫鎖。我們先講重入鎖火鼻。

先來一個(gè)簡單的例子:

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
   * 重入鎖室囊,表示在單個(gè)線程內(nèi),這個(gè)鎖可以反復(fù)進(jìn)入魁索,也就是說融撞,一個(gè)線程可以連續(xù)兩次獲得同一把鎖。
   * 如果你不允許重入粗蔚,將導(dǎo)致死鎖尝偎。注意,lock 和 unlock 次數(shù)一定要相同鹏控,如果不同致扯,就會導(dǎo)致死鎖和監(jiān)視器異常。
   *
   * synchronized 只有2種情況:1繼續(xù)執(zhí)行当辐,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 {
        // 因?yàn)閘ock 如果發(fā)生了異常,是不會釋放鎖的瀑构,所以必須在 finally 塊中釋放鎖
        // synchronized 發(fā)生異常會主動釋放鎖
        lock.unlock();
      }
    }
  }
}


在上面的代碼中裆针,我們使用了try 塊中保護(hù)了臨界資源 i 的操作刨摩。可以看到世吨, 重入鎖不管是開啟鎖還是釋放鎖都是顯示的澡刹,其中需要注意的一點(diǎn)是,重入鎖運(yùn)行時(shí)如果發(fā)生了異常耘婚,不會像 synchronized 釋放鎖罢浇,因此需要在 finally 中釋放鎖。否則將產(chǎn)生死鎖沐祷。

什么是重入鎖嚷闭?鎖就是鎖唄,為什么叫重入鎖赖临?之所以這么叫胞锰,那是因?yàn)檫@種鎖是可以反復(fù)進(jìn)入的(一個(gè)線程),大家看看下面的代碼:

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

在這種情況下兢榨,一個(gè)線程連續(xù)兩次獲得兩把鎖嗅榕,這是允許的。如果不允許這么操作吵聪,那么同一個(gè)線程咋i第二次獲得鎖是凌那,將會和自己產(chǎn)生死鎖。當(dāng)然吟逝,需要注意的是帽蝶,如果你多次獲得了鎖,那么也要相同的釋放多次块攒,如果釋放鎖的次數(shù)多了励稳,就會得到一個(gè) IllegalMonitorStateException 異常,反之局蚀,如果釋放鎖的次數(shù)少了麦锯,那么相當(dāng)于這個(gè)線程還沒有釋放鎖,其他線程也就無法進(jìn)入臨界區(qū)琅绅。

重入鎖能夠?qū)崿F(xiàn) synchronized 的所有功能,而且功能更為強(qiáng)大鹅巍,我們看看有哪些功能千扶。

中斷響應(yīng)

對于 synchronized 來說,如果一個(gè)線程在等待鎖骆捧,那么結(jié)果只有2種澎羞,要么他獲得這把鎖繼續(xù)運(yùn)行,要么他就保持等待敛苇。沒有第三種可能妆绞,那如果我有一個(gè)需求:需要線程在等待的時(shí)候中斷線程顺呕,synchronizded 是做不到的。而重入鎖可以做到括饶,就是 lockInterruptibly 方法株茶,該方法可以獲取鎖,并且在獲取鎖的過程種支持線程中斷图焰,也就是說启盛,如果調(diào)用了線程中斷方法,那么就會拋出異常技羔。相對于 lock 方法僵闯,是不是更為強(qiáng)大?還是寫個(gè)例子吧:

package cn.think.in.java.lock;

import java.util.concurrent.locks.ReentrantLock;

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

  /**
   * 默認(rèn)是不公平的鎖藤滥,設(shè)置為 true 為公平鎖
   *
   * 公平:在多個(gè)線程的爭用下鳖粟,這些鎖傾向于將訪問權(quán)授予等待時(shí)間最長的線程;
   * 使用公平鎖的程序在許多線程訪問時(shí)表現(xiàn)為很低的總體吞吐量(即速度很慢拙绊,常常極其慢)
   * 還要注意的是牺弹,未定時(shí)的 tryLock 方法并沒有使用公平設(shè)置
   *
   * 不公平:此鎖將無法保證任何特定訪問順序
   *
   * 拾遺:1 該類的序列化與內(nèi)置鎖的行為方式相同:一個(gè)反序列化的鎖處于解除鎖定狀態(tài),不管它被序列化時(shí)的狀態(tài)是怎樣的时呀。
   *      2.此鎖最多支持同一個(gè)線程發(fā)起的 2147483648 個(gè)遞歸鎖张漂。試圖超過此限制會導(dǎo)致由鎖方法拋出的 Error。
   */
  static ReentrantLock lock1 = new ReentrantLock(true);
  static ReentrantLock lock2 = new ReentrantLock();
  int lock;

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

  /**
   * lockInterruptibly 方法: 獲得鎖航攒,但優(yōu)先響應(yīng)中斷
   * tryLock 嘗試獲得鎖,不等待
   * tryLock(long time , TimeUnit unit) 嘗試獲得鎖趴梢,等待給定的時(shí)間
   */
  @Override
  public void run() {
    try {
      if (lock == 1) {
        // 如果當(dāng)前線程未被中斷漠畜,則獲取鎖。
        lock1.lockInterruptibly();// 即在等待鎖的過程中坞靶,可以響應(yīng)中斷憔狞。
        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 的時(shí)候,會死鎖彰阴,但被調(diào)用了 thread.interrupt 方法瘾敢,導(dǎo)致中斷。中斷會放棄鎖尿这。
        lock1.lockInterruptibly();
      }

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

      // 查詢當(dāng)前線程是否保持此鎖簇抵。
      if (lock2.isHeldByCurrentThread()) {
        lock2.unlock();
      }

      System.out.println(Thread.currentThread().getId() + ": 線程退出");
    }
  }


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

    /**
     * 這部分代碼主要是針對 lockInterruptibly 方法,該方法在線程發(fā)生死鎖的時(shí)候可以中斷線程射众。讓線程放棄鎖碟摆。
     * 而 synchronized 是沒有這個(gè)功能的, 他要么獲得鎖繼續(xù)執(zhí)行叨橱,要么繼續(xù)等待鎖典蜕。
     */

    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);
    // 中斷其中一個(gè)線程(只有線程在等待鎖的過程中才有效)
    // 如果線程已經(jīng)拿到了鎖断盛,中斷是不起任何作用的。
    // 注意:這點(diǎn) synchronized 是不能實(shí)現(xiàn)此功能的愉舔,synchronized 在等待過程中無法中斷
    t2.interrupt();
    // t2 線程中斷钢猛,拋出異常,并放開鎖屑宠。沒有完成任務(wù)
    // t1 順利完成任務(wù)厢洞。
  }
}

在上面的代碼種,我們分別啟動兩個(gè)線程典奉,制造了一個(gè)死鎖躺翻,如果是 synchronized 是無法解除這個(gè)死鎖的,這個(gè)時(shí)候重入鎖的威力就出來了卫玖,我們調(diào)用線程的 interrupt 方法公你,中斷線程,我們說假瞬,這個(gè)方法在線程 sleep陕靠,join ,wait 的時(shí)候脱茉,都會導(dǎo)致異常剪芥,這里也一羊,由于我們使用的 lock 的 lockInterruptibly 方法琴许,該方法就像我們剛說的那樣税肪,在等待鎖的時(shí)候,如果線程被中斷了榜田,就會出現(xiàn)異常益兄,同時(shí)調(diào)用了 finally 種的 unlock 方法,注意箭券,我們在 finally 中用 isHeldByCurrentThread 判斷當(dāng)前線程是否持有此鎖净捅,這是一種預(yù)防措施,放置線程沒有持有此鎖辩块,導(dǎo)致出現(xiàn) monitorState 異常蛔六。

鎖申請

除了等待通知之外,避免死鎖還有另一種方法庆捺,就是超時(shí)等待古今,如果超過這個(gè)時(shí)間,線程就放棄獲取這把鎖滔以,這點(diǎn) ,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 秒,讓下一個(gè)線程無法獲取鎖
        System.out.println("鎖住 6 秒适滓,讓下一個(gè)線程無法獲取鎖");
        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();


  }
}

上面的代碼中敦迄,我們設(shè)置鎖的等待時(shí)間是5秒,但是在同步塊中凭迹,我們設(shè)置了6秒暫停罚屋,鎖外面的線程等待了5面發(fā)現(xiàn)還是不能獲取鎖,就會放棄嗅绸。走 else 邏輯脾猛,結(jié)束執(zhí)行,注意鱼鸠,這里猛拴,我們在 finally 塊中依然做了判斷,如果不做判斷蚀狰,就會出現(xiàn) IllegalMonitorStateException 異常愉昆。

當(dāng)然了,tryLock 方法也可以不帶時(shí)間參數(shù)麻蹋,如果獲取不到鎖跛溉,立刻返回false,否則返回 true哥蔚。該方法也是應(yīng)對死鎖的一個(gè)好辦法倒谷。我們還是寫個(gè)例子:

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糙箍,他會不斷的嘗試渤愁, 當(dāng)?shù)谝淮问×耍麜艞壣詈唬缓髨?zhí)行完畢抖格,并釋放外層的鎖,這個(gè)時(shí)候就是
   * 另一個(gè)線程搶鎖的好時(shí)機(jī)咕晋。
   * @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,他會不斷的嘗試掌呜, 當(dāng)?shù)谝淮问×俗揖粒麜艞墸缓髨?zhí)行完畢质蕉,并釋放外層的鎖势篡,這個(gè)時(shí)候就是另一個(gè)線程搶鎖的好時(shí)機(jī)翩肌。

公平鎖和非公平鎖

大多數(shù)情況下,為了效率禁悠,鎖都是不公平的念祭。系統(tǒng)在選擇鎖的時(shí)候都是隨機(jī)的,不會按照某種順序碍侦,比如時(shí)間順序粱坤,公平鎖的一大特點(diǎn):他不會產(chǎn)生饑餓現(xiàn)象。只要你排隊(duì) 瓷产,最終還是可以得到資源的站玄。如果我們使用 synchronized ,得到的鎖就是不公平的拦英。因此蜒什,這也是重入鎖比 synchronized 強(qiáng)大的一個(gè)優(yōu)勢。我們同樣寫個(gè)例子:

package cn.think.in.java.lock;

import java.util.concurrent.locks.ReentrantLock;

public class FairLock implements Runnable {

  // 公平鎖和非公平鎖的結(jié)果完全不同
  /*
  * 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();
      }
    }
  }

  /**
   * 默認(rèn)是不公平的鎖灾常,設(shè)置為 true 為公平鎖
   *
   * 公平:在多個(gè)線程的爭用下,這些鎖傾向于將訪問權(quán)授予等待時(shí)間最長的線程铃拇;
   * 使用公平鎖的程序在許多線程訪問時(shí)表現(xiàn)為很低的總體吞吐量(即速度很慢钞瀑,常常極其慢)
   * 還要注意的是,未定時(shí)的 tryLock 方法并沒有使用公平設(shè)置
   *
   * 不公平:此鎖將無法保證任何特定訪問順序慷荔,但是效率很高
   *
   */
  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();
  }
}


重入鎖的構(gòu)造函數(shù)有一個(gè) boolean 參數(shù)雕什,ture 表示公平,false 表示不公平显晶,默認(rèn)是不公平的贷岸,公平鎖會降低性能。代碼中由運(yùn)行結(jié)果磷雇,可以看到偿警,公平鎖的打印順序是完全交替運(yùn)行,而不公平鎖的順序完全是隨機(jī)的唯笙。注意:如果沒有特殊需求螟蒸,請不要使用公平鎖,會大大降低吞吐量崩掘。

到這里七嫌,我們總結(jié)一下重入鎖相比 synchronized 有哪些優(yōu)勢:

  1. 可以在線程等待鎖的時(shí)候中斷線程,synchronized 是做不到的苞慢。
  2. 可以嘗試獲取鎖诵原,如果獲取不到就放棄,或者設(shè)置一定的時(shí)間,這也是 synchroized 做不到的皮假。
  3. 可以設(shè)置公平鎖鞋拟,synchronized 默認(rèn)是非公平鎖骂维,無法實(shí)現(xiàn)公平鎖惹资。

當(dāng)然,大家會說航闺, synchronized 可以通過 Object 的 wait 方法和 notify 方法實(shí)現(xiàn)線程之間的通信褪测,重入鎖可以做到嗎?樓主告訴大家潦刃,當(dāng)然可以了侮措! JDK 中的阻塞隊(duì)列就是用重入鎖加 他的搭檔 condition 實(shí)現(xiàn)的。

重入鎖的好搭檔-----Condition

還記的剛開始說 Lock 接口有一個(gè)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 方法對應(yīng)的就是 notify 方法菲饼。signalAll 方法對應(yīng)的就是 notifyAll 方法。其中還有一些時(shí)間限制的 await 方法列赎,和 Object 的 wait 方法的作用相同宏悦。注意,其中有一個(gè) awaitUninterruptibly 方法包吝,該方法從名字可以看出饼煞,并不會響應(yīng)線程的中斷,而 Object 的 wait 方法是會響應(yīng)的诗越。而 awaitUntil 方法就是等待到一個(gè)給定的絕對時(shí)間砖瞧。除非調(diào)用了 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 使當(dāng)前線程等待芭届,同時(shí)釋放當(dāng)前鎖,當(dāng)其他線程中使用 signal 或者 signalAll 方法時(shí)感耙,線程會重新獲得鎖并繼續(xù)執(zhí)行褂乍。
 *       或者當(dāng)線程被中斷時(shí),也能跳出等待即硼,這和 Object.wait 方法很相似逃片。
 * awaitUninterruptibly() 方法與 await 方法基本相同,但是它并不會在等待過程中響應(yīng)中斷。
 * singal() 該方法用于喚醒一個(gè)在等待中的線程褥实,相對的 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 的鎖损离,也就是說哥艇,一個(gè)線程想調(diào)用 condition 的方法,必須先獲取 lock 的鎖僻澎。
      // 否則就會像 object 的 wait 方法一樣貌踏,監(jiān)視器異常
      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 繼續(xù)執(zhí)行
    // main 線程必須獲取 lock 的鎖,才能調(diào)用 condition 的方法窟勃。否則就是監(jiān)視器異常祖乳,這點(diǎn)和 object 的 wait 方法是一樣的。
    lock.lock(); // IllegalMonitorStateException
    // 從 condition 的等待隊(duì)列中秉氧,喚醒一個(gè)線程眷昆。
    condition.signal();
    lock.unlock();
  }
}

可以說,condition 的使用方式和 Object 類的 wait 方法的使用方式很相似汁咏,無論在哪一個(gè)線程中調(diào)用 await 或者 signal 方法亚斋,都必須獲取對應(yīng)的鎖,否則會出現(xiàn) IllegalMonitorStateException 異常梆暖。

到這里伞访,我們可以說, Condition 的實(shí)現(xiàn)比 Object 的 wait 和 notify 還是強(qiáng)一點(diǎn)轰驳,其中就包括了等待到指定的絕對時(shí)間厚掷,并且還有一個(gè)不受線程中斷影響的 awaitUninterruptibly 方法。因此级解,我們說冒黑,只要允許,請使用重入鎖勤哗,盡量不要使用無腦的 synchronized 抡爹。雖然在 JDK 1.6 后, synchronized 被優(yōu)化了芒划,但仍然建議使用 重入鎖冬竟。

3. ReadWriteLock 讀寫鎖

偉大的 Doug Lea 不僅僅創(chuàng)造了 重入鎖,還創(chuàng)造了 讀寫鎖民逼。什么是讀寫鎖呢泵殴?我們知道,線程不安全的原因來自于多線程對數(shù)據(jù)的修改拼苍,如果你不修改數(shù)據(jù)笑诅,根本不需要鎖。我們完全可以將讀寫分離,提高性能吆你,在讀的時(shí)候不使用鎖弦叶,在寫的時(shí)候才加入鎖。這就是 ReadWriteLock 的設(shè)計(jì)原理妇多。

那么伤哺,如何使用呢?

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();
      // 模擬讀操作砌梆,讀操作的耗時(shí)越多默责,讀寫鎖的優(yōu)勢就越明顯
      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 可以返回寫鎖烂瘫,我們使用普通的的重入鎖和讀寫鎖進(jìn)行測試随闽,怎么測試呢氧卧?

兩個(gè)循環(huán):一個(gè)循環(huán)開啟18個(gè)線程去讀數(shù)據(jù)号胚,一個(gè)循環(huán)開啟兩個(gè)線程去寫玫恳。如果使用普通的重入鎖损谦,將耗時(shí)20秒刁绒,因?yàn)槠胀ǖ闹厝腈i在讀的時(shí)候依然是串行的婚惫。而如果使用讀寫鎖屡谐,只需要2秒皮仁,也就是寫的時(shí)候是串行的籍琳。讀的時(shí)候是并行的,極大的提高了性能贷祈。

注意:只要涉及到寫都是串行的趋急。比如讀寫操作,寫寫操作势誊,都是串行的呜达,只有讀讀操作是并行的。

讀寫鎖 ReadWriteLock 接口只有 2個(gè)方法:

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

他的標(biāo)準(zhǔn)實(shí)現(xiàn)類是 ReentrantReadWriteLock 類粟耻,該類和普通重入鎖一樣查近,也能實(shí)現(xiàn)公平鎖,中斷響應(yīng)挤忙,鎖申請等特性霜威。因?yàn)樗麄兎祷氐淖x鎖或者寫鎖都實(shí)現(xiàn)了 Lock 接口。

總結(jié)

到這里饭玲,我們已經(jīng)將 Java 世界的三把鎖的使用弄清楚了侥祭,從分析的過程中我們知道了,JDK 1.5 的重入鎖完全可以代替關(guān)鍵字 synchronized ,能實(shí)現(xiàn)很多 synchronized 沒有的功能矮冬。比如中斷響應(yīng)谈宛,鎖申請,公平鎖等胎署,而重入鎖的搭檔 Condition 也比 Object 的wait 和notify 強(qiáng)大吆录,比如有設(shè)置絕對時(shí)間的等待,還有忽略線程中斷的 await 方法琼牧,這些都是 synchronized 無法實(shí)現(xiàn)的恢筝。還有優(yōu)化讀性能的 讀寫鎖,在讀的時(shí)候完全是并行的巨坊,在某些場景下撬槽,比如讀很多,寫很少趾撵,性能將是幾何級別的提升侄柔。

所以,以后占调,能不用 synchronzed 就不要用暂题,用的不好就會導(dǎo)致死鎖。

今天的Java 三把鎖就介紹到這里究珊。

good luck P秸摺!=虽獭言津!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市幔虏,隨后出現(xiàn)的幾起案子纺念,更是在濱河造成了極大的恐慌,老刑警劉巖想括,帶你破解...
    沈念sama閱讀 219,110評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件陷谱,死亡現(xiàn)場離奇詭異,居然都是意外死亡瑟蜈,警方通過查閱死者的電腦和手機(jī)烟逊,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,443評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來铺根,“玉大人宪躯,你說我怎么就攤上這事∥挥兀” “怎么了访雪?”我有些...
    開封第一講書人閱讀 165,474評論 0 356
  • 文/不壞的土叔 我叫張陵详瑞,是天一觀的道長。 經(jīng)常有香客問我臣缀,道長坝橡,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,881評論 1 295
  • 正文 為了忘掉前任精置,我火速辦了婚禮计寇,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘脂倦。我一直安慰自己番宁,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,902評論 6 392
  • 文/花漫 我一把揭開白布赖阻。 她就那樣靜靜地躺著蝶押,像睡著了一般。 火紅的嫁衣襯著肌膚如雪政供。 梳的紋絲不亂的頭發(fā)上播聪,一...
    開封第一講書人閱讀 51,698評論 1 305
  • 那天,我揣著相機(jī)與錄音布隔,去河邊找鬼。 笑死稼虎,一個(gè)胖子當(dāng)著我的面吹牛衅檀,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播霎俩,決...
    沈念sama閱讀 40,418評論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼哀军,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了打却?” 一聲冷哼從身側(cè)響起杉适,我...
    開封第一講書人閱讀 39,332評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎柳击,沒想到半個(gè)月后猿推,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,796評論 1 316
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡捌肴,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,968評論 3 337
  • 正文 我和宋清朗相戀三年蹬叭,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片状知。...
    茶點(diǎn)故事閱讀 40,110評論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡秽五,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出饥悴,到底是詐尸還是另有隱情坦喘,我是刑警寧澤盲再,帶...
    沈念sama閱讀 35,792評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站瓣铣,受9級特大地震影響答朋,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜坯沪,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,455評論 3 331
  • 文/蒙蒙 一绿映、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧腐晾,春花似錦叉弦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,003評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至巨柒,卻和暖如春樱拴,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背洋满。 一陣腳步聲響...
    開封第一講書人閱讀 33,130評論 1 272
  • 我被黑心中介騙來泰國打工晶乔, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人牺勾。 一個(gè)月前我還...
    沈念sama閱讀 48,348評論 3 373
  • 正文 我出身青樓正罢,卻偏偏與公主長得像,于是被迫代替她去往敵國和親驻民。 傳聞我的和親對象是個(gè)殘疾皇子翻具,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,047評論 2 355

推薦閱讀更多精彩內(nèi)容