1. 程式人生 > 程式設計 >Java Lock介面實現原理及例項解析

Java Lock介面實現原理及例項解析

1、概述

JUC中locks包下常用的類與介面圖如下:

Java Lock介面實現原理及例項解析

圖中,Lock和ReadWriteLock是頂層鎖的介面,Lock代表實現類是ReentrantLock(可重入鎖),ReadWriteLock(讀寫鎖)的代表實現類是ReentrantReadWriteLock。

ReadWriteLock 介面以類似方式定義了讀鎖而寫鎖。此包只提供了一個實現,即 ReentrantReadWriteLock。

Condition 介面描述了可能會與鎖有關聯的條件變數。這些變數在用法上與使用 Object.wait 訪問的隱式監視器類似,但提供了更強大的功能。需要特別指出的是,單個 Lock 可能與多個 Condition 物件關聯。

2、lock與synchronized比較

synchronized是java中的一個關鍵字,也就是說是Java語言內建的特性。那麼為什麼會出現Lock呢?

1、Lock不是Java語言內建的,synchronized是Java語言的關鍵字,因此是內建特性。Lock是一個類,通過這個類可以實現同步訪問;

2、Lock和synchronized有一點非常大的不同,採用synchronized不需要使用者去手動釋放鎖,當synchronized方法或者synchronized程式碼塊執行完之後,系統會自動讓執行緒釋放對鎖的佔用;而Lock則必須要使用者去手動釋放鎖,如果沒有主動釋放鎖,就有可能導致出現死鎖現象。

synchronized 的侷限性與Lock的優點 

如果一個程式碼塊被synchronized關鍵字修飾,當一個執行緒獲取了對應的鎖,並執行該程式碼塊時,其他執行緒便只能一直等待直至佔有鎖的執行緒釋放鎖。事實上,佔有鎖的執行緒釋放鎖一般會是以下三種情況之一:

1:佔有鎖的執行緒執行完了該程式碼塊,然後釋放對鎖的佔有;

2:佔有鎖執行緒執行發生異常,此時JVM會讓執行緒自動釋放鎖;

3:佔有鎖執行緒進入WAITING狀態從而釋放鎖,例如在該執行緒中呼叫wait()方法等。

下列三種情況: 

1 、在使用synchronized關鍵字的情形下,假如佔有鎖的執行緒由於要等待IO或者其他原因(比如呼叫sleep方法)被阻塞了,但是又沒有釋放鎖,那麼其他執行緒就只能一直等待,別無他法。這會極大影響程式執行效率。因此,就需要有一種機制可以不讓等待的執行緒一直無期限地等待下去(比如只等待一定的時間 (解決方案:tryLock(long time,TimeUnit unit))或者能夠響應中斷(解決方案:lockInterruptibly())),這種情況可以通過 Lock 解決。

2、當多個執行緒讀寫檔案時,讀操作和寫操作會發生衝突現象,寫操作和寫操作也會發生衝突現象,但是讀操作和讀操作不會發生衝突現象。但是如果採用synchronized關鍵字實現同步的話,就會導致一個問題,即當多個執行緒都只是進行讀操作時,也只有一個執行緒在可以進行讀操作,其他執行緒只能等待鎖的釋放而無法進行讀操作。因此,需要一種機制來使得當多個執行緒都只是進行讀操作時,執行緒之間不會發生衝突。同樣地,Lock也可以解決這種情況 (解決方案:ReentrantReadWriteLock) 。

3、通過Lock得知執行緒有沒有成功獲取到鎖 (解決方案:ReentrantLock) ,但這個是synchronized無法辦到的。

上面提到的三種情形,我們都可以通過Lock來解決,但 synchronized 關鍵字卻無能為力。事實上,Lock 是 java.util.concurrent.locks包 下的介面,Lock 實現提供了比 synchronized 關鍵字更廣泛的鎖操作,它能以更優雅的方式處理執行緒同步問題。也就是說,Lock提供了比synchronized更多的功能。

3、Lock介面實現類的使用

// 獲取鎖
void lock()
// 如果當前執行緒未被中斷,則獲取鎖,可以響應中斷
void lockInterruptibly()
// 返回繫結到此 Lock 例項的新 Condition 例項
Condition newCondition()
// 僅在呼叫時鎖為空閒狀態才獲取該鎖,可以響應中斷
boolean tryLock()
// 如果鎖在給定的等待時間內空閒,並且當前執行緒未被中斷,則獲取鎖
boolean tryLock(long time,TimeUnit unit)
// 釋放鎖
void unlock()

3.1、在Lock中聲明瞭四個方法來獲取鎖,那麼這四個方法有何區別呢?首先,lock()方法是平常使用得最多的一個方法,就是用來獲取鎖。如果鎖已被其他執行緒獲取,則進行等待。在前面已經講到,如果採用Lock,必須主動去釋放鎖,並且在發生異常時,不會自動釋放鎖。因此,一般來說,使用Lock必須在try…catch…塊中進行,並且將釋放鎖的操作放在finally塊中進行,以保證鎖一定被被釋放,防止死鎖的發生。通常使用Lock來進行同步的話,是以下面這種形式去使用的:

Lock lock = ...;
lock.lock();
try{
  //處理任務
}catch(Exception ex){
}finally{
  lock.unlock();  //釋放鎖
}

3.2、tryLock() & tryLock(long time,TimeUnit unit)

  tryLock()方法是有返回值的,它表示用來嘗試獲取鎖,如果獲取成功,則返回true;如果獲取失敗(即鎖已被其他執行緒獲取),則返回false,也就是說,這個方法無論如何都會立即返回(在拿不到鎖時不會一直在那等待)。

  tryLock(long time,TimeUnit unit)方法和tryLock()方法是類似的,只不過區別在於這個方法在拿不到鎖時會等待一定的時間,在時間期限之內如果還拿不到鎖,就返回false,同時可以響應中斷。如果一開始拿到鎖或者在等待期間內拿到了鎖,則返回true。

一般情況下,通過tryLock來獲取鎖時是這樣使用的:

Lock lock = ...;
if(lock.tryLock()) {
   try{
     //處理任務
   }catch(Exception ex){
   }finally{
     lock.unlock();  //釋放鎖
   }
}else {
  //如果不能獲取鎖,則直接做其他事情
}

3.3、lockInterruptibly() 
  lockInterruptibly()方法比較特殊,當通過這個方法去獲取鎖時,如果執行緒 正在等待獲取鎖,則這個執行緒能夠響應中斷,即中斷執行緒的等待狀態。例如,當兩個執行緒同時通過lock.lockInterruptibly()想獲取某個鎖時,假若此時執行緒A獲取到了鎖,而執行緒B只有在等待,那麼對執行緒B呼叫threadB.interrupt()方法能夠中斷執行緒B的等待過程。

  由於lockInterruptibly()的宣告中丟擲了異常,所以lock.lockInterruptibly()必須放在try塊中或者在呼叫lockInterruptibly()的方法外宣告丟擲 InterruptedException,但推薦使用後者,原因稍後闡述。因此,lockInterruptibly()一般的使用形式如下:

public void method() throws InterruptedException {
  lock.lockInterruptibly();
  try { 
   //.....
  }
  finally {
    lock.unlock();
  } 
}

注意,當一個執行緒獲取了鎖之後,是不會被interrupt()方法中斷的。因為interrupt()方法只能中斷阻塞過程中的執行緒而不能中斷正在執行過程中的執行緒。因此,當通過lockInterruptibly()方法獲取某個鎖時,如果不能獲取到,那麼只有進行等待的情況下,才可以響應中斷的。與 synchronized 相比,當一個執行緒處於等待某個鎖的狀態,是無法被中斷的,只有一直等待下去。
範例,執行起來後,Thread2能夠被正確中斷。

public class Test {
  private Lock lock = new ReentrantLock(); 
  public static void main(String[] args) {
    Test test = new Test();
    MyThread thread1 = new MyThread(test);
    MyThread thread2 = new MyThread(test);
    thread1.start();
    thread2.start();
     
    try {
      Thread.sleep(2000);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    thread2.interrupt();
  } 
   
  public void insert(Thread thread) throws InterruptedException{
    lock.lockInterruptibly();  //注意,如果需要正確中斷等待鎖的執行緒,必須將獲取鎖放在外面,然後將InterruptedException丟擲
    try { 
      System.out.println(thread.getName()+"得到了鎖");
      long startTime = System.currentTimeMillis();
      for(  ;   ;) {
        if(System.currentTimeMillis() - startTime >= Integer.MAX_VALUE)
          break;
        //插入資料
      }
    }
    finally {
      System.out.println(Thread.currentThread().getName()+"執行finally");
      lock.unlock();
      System.out.println(thread.getName()+"釋放了鎖");
    } 
  }
}
class MyThread extends Thread {
  private Test test = null;
  public MyThread(Test test) {
    this.test = test;
  }
  @Override
  public void run() {
     
    try {
      test.insert(Thread.currentThread());
    } catch (InterruptedException e) {
      System.out.println(Thread.currentThread().getName()+"被中斷");
    }
  }
}

3.4 具體的鎖實現

Lock的實現類

ReentrantLock :即 可重入鎖。ReentrantLock是唯一實現了Lock介面的類,並且ReentrantLock提供了更多的方法。

ReadWriteLock鎖:介面只有兩個方法:

//返回用於讀取操作的鎖
Lock readLock()
//返回用於寫入操作的鎖
Lock writeLock()

ReadWriteLock維護了一對相關的鎖,一個用於只讀操作,另一個用於寫入操作。範例

class ReadWriteLockQueue {
  //共享資料,只能有一個執行緒 寫資料,但可以多個執行緒讀資料
  private Object data = null;
  private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
  //讀資料
  public void get() {
    try {
      rwl.readLock().lock();//上讀鎖,其他執行緒只能讀。
      System.out.print(Thread.currentThread().getName() + "讀取 data!");
      Thread.sleep((long) (Math.random() * 1000));
        System.out.println(Thread.currentThread().getName() + "讀取到的資料:"+ data);
    } catch (Exception e) {
      e.printStackTrace();
    } finally {
      rwl.readLock().unlock();//釋放讀鎖
    }
  }
  //寫資料
  public void put(Object data) {
    try {
      rwl.writeLock().lock();//加上寫鎖,不允許其他執行緒 讀寫
      System.out.print(Thread.currentThread().getName() + "寫入資料,");
      Thread.sleep((long) (Math.random() * 1000));
      this.data = data;
      System.out.println(Thread.currentThread().getName() + "已經寫好資料" + data);
    } catch (Exception e) {
      e.printStackTrace();
    } finally {
      rwl.writeLock().unlock();//釋放鎖
    }
  }
}
public class TestReentrantReadWriteLock {
  public static void main(String[] args) {
    final ReadWriteLockQueue readWriteLockQueue = new ReadWriteLockQueue();
    for (int i = 0; i < 2 ; i++) {
      new Thread(new Runnable() {
        @Override
        public void run() {
          while (true) {
            readWriteLockQueue.put(new Random().nextInt(10000));
          }
        }
      },"寫執行緒").start();
      new Thread(new Runnable() {
        @Override
        public void run() {
          while(true) {
            readWriteLockQueue.get();
          }
        }
      },"讀執行緒").start();
    }
  }
}

4、鎖的相關概念

  • 可重入鎖 : 如果鎖具備可重入性,則稱作為 可重入鎖 。像 synchronized和ReentrantLock都是可重入鎖,可重入性在我看來實際上表明瞭 鎖的分配機制:基於執行緒的分配,而不是基於方法呼叫的分配。舉個簡單的例子,當一個執行緒執行到某個synchronized方法時,比如說method1,而在method1中會呼叫另外一個synchronized方法method2,此時執行緒不必重新去申請鎖,而是可以直接執行方法method2。
  • 可中斷鎖:顧名思義,可中斷鎖就是可以響應中斷的鎖。在Java中,synchronized就不是可中斷鎖,而Lock是可中斷鎖。如果某一執行緒A正在執行鎖中的程式碼,另一執行緒B正在等待獲取該鎖,可能由於等待時間過長,執行緒B不想等待了,想先處理其他事情,我們可以讓它中斷自己或者在別的執行緒中中斷它,這種就是可中斷鎖。在前面演示tryLock(long time,TimeUnit unit)和lockInterruptibly()的用法時已經體現了Lock的可中斷性。
  • 公平鎖:公平鎖即儘量以請求鎖的順序來獲取鎖。比如,同是有多個執行緒在等待一個鎖,當這個鎖被釋放時,等待時間最久的執行緒(最先請求的執行緒)會獲得該所,這種就是公平鎖。而非公平鎖則無法保證鎖的獲取是按照請求鎖的順序進行的,這樣就可能導致某個或者一些執行緒永遠獲取不到鎖。在Java中,synchronized就是非公平鎖,它無法保證等待的執行緒獲取鎖的順序。而對於ReentrantLock 和 ReentrantReadWriteLock,它預設情況下是非公平鎖,但是可以設定為公平鎖。
  • 樂觀鎖:總是假設最好的情況,每次去拿資料的時候都認為別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個資料,可以使用版本號機制和CAS演算法實現。樂觀鎖適用於多讀的應用型別,這樣可以提高吞吐量,像資料庫提供的類似於write_condition機制,其實都是提供的樂觀鎖。在Java中java.util.concurrent.atomic包下面的原子變數類就是使用了樂觀鎖的一種實現方式CAS實現的。

鎖主要存在四中狀態,依次是:無鎖狀態、偏向鎖狀態、輕量級鎖狀態、重量級鎖狀態,他們會隨著競爭的激烈而逐漸升級。注意鎖可以升級不可降級,這種策略是為了提高獲得鎖和釋放鎖的效率。

4.1、偏向鎖

引入偏向鎖的目的和引入輕量級鎖的目的很像,他們都是為了沒有多執行緒競爭的前提下,減少傳統的重量級鎖使用作業系統互斥量產生的效能消耗。但是不同是:輕量級鎖在無競爭的情況下使用 CAS (Compare and Swap)操作去代替使用互斥量。而偏向鎖在無競爭的情況下會把整個同步都消除掉。

偏向鎖的“偏”就是偏心的偏,它的意思是會偏向於第一個獲得它的執行緒,如果在接下來的執行中,該鎖沒有被其他執行緒獲取,那麼持有偏向鎖的執行緒就不需要進行同步!關於偏向鎖的原理可以檢視《深入理解Java虛擬機器:JVM高階特性與最佳實踐》第二版的13章第三節鎖優化。

但是對於鎖競爭比較激烈的場合,偏向鎖就失效了,因為這樣場合極有可能每次申請鎖的執行緒都是不相同的,因此這種場合下不應該使用偏向鎖,否則會得不償失,需要注意的是,偏向鎖失敗後,並不會立即膨脹為重量級鎖,而是先升級為輕量級鎖。

4.2、 輕量級鎖

倘若偏向鎖失敗,虛擬機器並不會立即升級為重量級鎖,它還會嘗試使用一種稱為輕量級鎖的優化手段(1.6之後加入的)。輕量級鎖不是為了代替重量級鎖,它的本意是在沒有多執行緒競爭的前提下,減少傳統的重量級鎖使用作業系統互斥量產生的效能消耗,因為使用輕量級鎖時,不需要申請互斥量。另外,輕量級鎖的加鎖和解鎖都用到了CAS操作。 關於輕量級鎖的加鎖和解鎖的原理可以檢視

《深入理解Java虛擬機器:JVM高階特性與最佳實踐》第二版的13章第三節鎖優化。
輕量級鎖能夠提升程式同步效能的依據是“對於絕大部分鎖,在整個同步週期內都是不存在競爭的”,這是一個經驗資料。如果沒有競爭,輕量級鎖使用 CAS 操作避免了使用互斥操作的開銷。但如果存在鎖競爭,除了互斥量開銷外,還會額外發生CAS操作,因此在有鎖競爭的情況下,輕量級鎖比傳統的重量級鎖更慢!如果鎖競爭激烈,那麼輕量級將很快膨脹為重量級鎖!

4.3、自旋鎖和自適應自旋鎖

輕量級鎖失敗後,虛擬機器為了避免執行緒真實地在作業系統層面掛起,還會進行一項稱為自旋鎖的優化手段。
互斥同步對效能最大的影響就是阻塞的實現,因為掛起執行緒/恢復執行緒的操作都需要轉入核心態中完成(使用者態轉換到核心態會耗費時間)。

一般執行緒持有鎖的時間都不是太長,所以僅僅為了這一點時間去掛起執行緒/恢復執行緒是得不償失的。 所以,虛擬機器的開發團隊就這樣去考慮:“我們能不能讓後面來的請求獲取鎖的執行緒等待一會而不被掛起呢?看看持有鎖的執行緒是否很快就會釋放鎖”。為了讓一個執行緒等待,我們只需要讓執行緒執行一個忙迴圈(自旋),這項技術就叫做自旋。
何謂自旋鎖?它是為實現保護共享資源而提出一種鎖機制。其實,自旋鎖與互斥鎖比較類似,它們都是為了解決對某項資源的互斥使用。無論是互斥鎖,還是自旋鎖,在任何時刻,最多隻能有一個保持者,也就說,在任何時刻最多隻能有一個執行單元獲得鎖。但是兩者在排程機制上略有不同。對於互斥鎖,如果資源已經被佔用,資源申請者只能進入睡眠狀態。但是自旋鎖不會引起呼叫者睡眠,如果自旋鎖已經被別的執行單元保持,呼叫者就一直迴圈在那裡看是否該自旋鎖的保持者已經釋放了鎖,"自旋"一詞就是因此而得名。

JDK1.6及1.6之後,自旋鎖就改為預設開啟的了。需要注意的是:自旋等待不能完全替代阻塞,因為它還是要佔用處理器時間。如果鎖被佔用的時間短,那麼效果當然就很好了!反之,相反!自旋等待的時間必須要有限度。如果自旋超過了限定次數任然沒有獲得鎖,就應該掛起執行緒。自旋次數的預設值是10次,但是使用者可以修改。
在 JDK1.6 中引入了自適應的自旋鎖。自適應的自旋鎖帶來的改進就是:自旋的時間不在固定了,而是和前一次同一個鎖上的自旋時間以及鎖的擁有者的狀態來決定,虛擬機器變得越來越“聰明”了。

4.4、鎖消除

鎖消除理解起來很簡單,它指的就是虛擬機器即使編譯器在執行時,如果檢測到那些共享資料不可能存在競爭,那麼就執行鎖消除。鎖消除可以節省毫無意義的請求鎖的時間。

4.5、鎖粗化

原則上在編寫程式碼的時候,總是推薦將同步快的作用範圍限制得儘量小——只在共享資料的實際作用域才進行同步,這樣是為了使得需要同步的運算元量儘可能變小,如果存在鎖競爭,那等待執行緒也能儘快拿到鎖。

以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支援我們。