【19】Java中的Locks


Lock是一個相似同步代碼塊(synchronized block)的線程同步機制。同步代碼塊而言,Lock能夠作到更細粒度的控制。 Lock(或者其餘高級同步機制)也是基於同步代碼塊(synchronized block),因此還不能徹底摒棄synchronized關鍵字。html

從Java 5開始,java.util.concurrent.locks包提供了幾個Lock的實現類。你能夠直接使用,前提是你知道如何使用它們,並且只有瞭解它們的內部機制,才能更好的使用它們。 更多的內容能夠參考原做者的java.util.concurrent.locks.Lock相關文章,以及JDK API。java

一個簡單的Lock

先來看看同步塊的代碼:安全

public class Counter{

  private int count = 0;

  public int inc(){
    synchronized(this){
      return ++count;
    }
  }
}

注意inc()方法中的synchronized(this)塊。 這個代碼塊,能夠確保同一時間,只有一個線程能夠執行return ++count。 固然,同步塊還有不少高級的用法,這裏只是簡單的用於保障++count的安全。併發

對於上面的Counter類,能夠用Lock進行改寫:this

public class Counter{

  private Lock lock = new Lock();
  private int count = 0;

  public int inc(){
    lock.lock();
    int newCount = ++count;
    lock.unlock();
    return newCount;
  }
}

Lock的lock()會讓沒有獲得鎖的線程進入等待,直到unlock()方法被調用。.net

下面看看一個簡單的Lock實現:線程

public class Lock{

  private boolean isLocked = false;

  public synchronized void lock()
  throws InterruptedException{
    while(isLocked){
      wait();
    }
    isLocked = true;
  }

  public synchronized void unlock(){
    isLocked = false;
    notify();
  }
}

注意:,這裏使用了一個while(isLocked)循環,起到了一個「自旋鎖(spin lock)」的效果。 有關自旋鎖以及wait()notify()的信息,能夠去看看《線程間的信號處理》code

isLocked爲true時,若是有線程調用lock()方法,那就會進入wait()等待。 此時,線程可能會被意外喚醒,從而退出wait()方法,因此,須要用循環來再次檢查isLocked的條件。(這就是:僞喚醒htm

若是,isLocked爲false,則線程不會進入while(isLocked)等待,而是會直接修改isLocked爲true,從而達到加鎖的效果。對象

當線程完成了臨界區的代碼,而且調用了unlock()。接着將isLocked條件置回false。而且經過notify()方法,喚醒等待線程,從而達到釋放鎖的效果。

重入鎖

Java中的同步塊是能夠重入的(Reentrance)。 這就意味着,Java線程進入一個同步代碼塊,就能夠得到對象上的監控器鎖(monitor),當這個線程去訪問同一個監控器鎖保護的同步代碼塊時,就能夠直接進入了。

來看看這個例子:

public class Reentrant{

  public synchronized outer(){
    inner();
  }

  public synchronized inner(){
    //do something
  }
}

注意,outer()inner()都聲明爲synchronized,這在Java中,就等價於synchronized(this)。 若是一個線程在outer()方法中調用了inner()方法。那麼,因爲這兩個方法,實際都是由同一個監控器(monitor)管理的(this),因此,能夠直接進入inner()方法。

若是一個線程已經持有了某個監控器,那麼它就能夠訪問這個監控器保護的全部同步代碼塊。而這種特性就稱之爲「重入」,線程能夠從新進入已經得到的鎖所保護的代碼塊。

以前,展現的Lock實現,是一個不可重入鎖。 而若是,咱們重寫一個像下面這樣的Reentrant實現,那麼當線程調用outer()方法將會被inner()內的lock.lock()阻塞住。

public class Reentrant2{

  Lock lock = new Lock();

  public outer(){
    lock.lock();
    inner();
    lock.unlock();
  }

  public synchronized inner(){
    lock.lock();
    //do something
    lock.unlock();
  }
}

當某個線程調用outer()方法,會首先鎖住Lock實例。 接着,調用inner()方法。而inner()會再次對Lock實例進行加鎖。 因爲Lock實例已經在out()方法中加鎖了,因此這裏就會失敗,並且會致使線程一直阻塞下去。

線程第二次調用lock()方法期間沒有調用unlock()從而致使上述問題。 因此,咱們再來看看lock()方法的實現:

public class Lock{

  boolean isLocked = false;

  public synchronized void lock()
  throws InterruptedException{
    while(isLocked){
      wait();
    }
    isLocked = true;
  }

  ...
}

內部循環的條件,決定着是否容許退出lock()方法。 這裏僅僅是判斷是否處於加鎖狀態,而不關心是哪一個線程持有着鎖。

因此,須要對Lock作一些改造:

public class Lock{

  boolean isLocked = false;
  Thread  lockedBy = null;
  int     lockedCount = 0;

  public synchronized void lock()
  throws InterruptedException{
    Thread callingThread = Thread.currentThread();
    while(isLocked && lockedBy != callingThread){
      wait();
    }
    isLocked = true;
    lockedCount++;
    lockedBy = callingThread;
  }


  public synchronized void unlock(){
    if(Thread.curentThread() == this.lockedBy){
      lockedCount--;

      if(lockedCount == 0){
        isLocked = false;
        notify();
      }
    }
  }

  ...
}

如今,自旋循環的條件能夠直接放行已經持有鎖的線程了。 若是鎖是自由狀態(isLocked = false),或者執行線程就是持有鎖的線程,循環都不會執行,線程能夠順利的退出lock()方法。

另外,咱們須要記錄同一個線程加鎖的次數。 由於,在unlock()釋放鎖時,咱們須要知道多少次後才能真正釋放鎖。 而這也將決定,unlock()須要執行與lock()對應的次數,才能釋放鎖。

如今,Lock就是一個可重入鎖了。

鎖的公平性

Java的synchronized是不保障線程進入的順序的。 所以,若是有多個線程不斷的競爭同一個同步塊,那就有可能某些線程永遠也沒法獲得訪問權(比較悲催的線程每次都沒有被喚醒)。 這就造成了飢餓。 爲了不這個問題,就須要把Lock實現爲公平性的。 因爲這裏的Lock都是基於synchronized的,因此就沒法保障公平性。 更多有關公平性的問題,能夠參見:《併發中的飢餓問題以及公平性》

在finally塊中釋放鎖

使用Lock來保護臨界區時,可能會因爲異常,致使沒有機會執行unlock()方法。 因此,須要經過finally來釋放鎖,這樣才能確保安全。

lock.lock();
try{
  //do critical section code, which may throw exception
} finally {
  lock.unlock();
}

這樣一個範式,能夠確保Lock能夠獲得有效釋放。

補充幾點

不論對於什麼類型的Lock,都須要考慮幾點:

  1. 競爭激烈程度
  2. 臨界區執行時常
    • 持有鎖的時間
  3. 鎖的粒度
    • 臨界區中的邏輯儘可能簡潔
    • 無必要的邏輯移出臨界區
  4. 能夠獲得釋放
    • 如非必要,不要永久阻塞等待
    • 儘量設置等待時間
  5. 鎖順序
    • 避免死鎖
  6. 鎖嵌套
    • 避免嵌套鎖死
相關文章
相關標籤/搜索