併發學習筆記 (5)

tutorials sitehtml

Locks in java

Locks (and other more advanced synchronization mechanisms) are created using synchronized blocks, so it is not like we can get totally rid of the synchronized keyword.
鎖的實現是利用synchonized, wait(),notify()方法實現的。因此不能夠認爲鎖能夠徹底脫離synchonized實現。java

Java包 JUC java.util.concurrent.locks 包括了不少lock接口的實現了類,這些類足夠使用。
可是須要知道如何使用它們,以及這些類背後的理論。JUC包教程安全

用synchonized:能夠保證在同一時間只有一個線程能夠執行 return ++count:多線程

public class Counter{
  private int count = 0;

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

如下的Counter類用Lock代替synchronized 達到一樣的目的:
lock() 方法會對 Lock 實例對象進行加鎖,所以全部其餘對該對象調用 lock() 方法的線程都會被阻塞,直到該 Lock 對象的 unlock() 方法被調用。函數

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類是怎麼設計的?this


Lock 類的設計

一個Lock類的簡單實現:線程

javapublic 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自旋鎖。當 isLockedtrue 時,調用 lock() 的線程在 wait() 調用上阻塞等待。爲防止該線程沒有收到 notify() 調用也從 wait() 中返回(也稱做虛假喚醒),這個線程會從新去檢查 isLocked 條件以決定當前是否能夠安全地繼續執行仍是須要從新保持等待,而不是認爲線程被喚醒了就能夠安全地繼續執行了。若是 isLocked 爲 false,當前線程會退出 while(isLocked) 循環,並將 isLocked 設回 true,讓其它正在調用 lock() 方法的線程可以在 Lock 實例上加鎖。設計

當線程完成了臨界區(位於 lock() 和 unlock() 之間)中的代碼,就會調用 unlock()。執行 unlock() 會從新將 isLocked 設置爲 false,而且通知(喚醒)其中一個(如有的話)在 lock() 方法中調用了 wait() 函數而處於等待狀態的線程。code

鎖的可重入性

synchronized 同步塊是可重入的。這意味着: 若是一個java線程進入了代碼中的同步塊synchonzied block,並所以得到了該同步塊使用的同步對象對應的管程monitor object上的鎖那麼這個線程能夠進入由同一個管程對象所同步的另外一個 java 代碼塊htm

前面的Lock的設計就不是可重入的:

javapublic class Reentrant2{
    Lock lock = new Lock();

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

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

一個線程是否被容許退出 lock() 方法是由 while 循環(自旋鎖)中的條件決定的。當前的判斷條件是隻有當 isLocked 爲 false 時 lock 操做才被容許,而沒有考慮是哪一個線程鎖住了它。
因此須要對Lock的設計作出以下修改,才能可重入。

javapublic 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();
            }
        }
    }

    ...
}

注意到如今的 while 循環(自旋鎖)也考慮到了已鎖住該 Lock 實例的線程。若是當前的鎖對象沒有被加鎖 (isLocked = false),或者當前調用線程已經對該 Lock 實例加了鎖,那麼 while 循環就不會被執行,調用 lock() 的線程就能夠退出該方法(譯者注:「被容許退出該方法」 在當前語義下就是指不會調用 wait() 而致使阻塞)。

除此以外,咱們須要記錄同一個線程重複對一個鎖對象加鎖的次數。不然,一次 unblock() 調用就會解除整個鎖,即便當前鎖已經被加鎖過屢次。在 unlock() 調用沒有達到對應 lock() 調用的次數以前,咱們不但願鎖被解除。

如今這個 Lock 類就是可重入的了。

鎖的公平性

Starvation and Fairness 飢餓和公平

一個線程由於其餘線程長期佔有CPU而本身得到不到,這種狀態稱爲Starvation. 解決線程飢餓的方法是公平機制fairness公平機制,讓全部線程都能公平的有機會去得到CPU。

致使飢餓的緣由

  1. 高優先級的線程佔有了全部CPU處理時間,這樣低優先級的線程得到不到;
  2. 處於阻塞狀態的線程無限期被阻塞
    Java 的同步代碼區也是一個致使飢餓的因素。Java 的同步代碼區對哪一個線程容許進入的次序沒有任何保障。這就意味着理論上存在一個試圖進入該同步區的線程處於被永久堵塞的風險,由於其餘線程老是能持續地先於它得到訪問,這便是 「飢餓」 問題,而一個線程被 「飢餓致死」 正是由於它得不到 CPU 運行時間的機會

Java's synchronized code blocks can be another cause of starvation.

  1. 處於等待狀態的對象無限期等待
    若是多個線程處在 wait() 方法執行上,而對其調用 notify() 不會保證哪個線程會得到喚醒,任何線程都有可能處於繼續等待的狀態。所以存在這樣一個風險:一個等待線程歷來得不到喚醒,由於其餘等待線程老是能被得到喚醒。

這裏細說一下:多線程經過共享一個object對象,來調用對象的wait/notifyAll 來致使線程等待或者喚醒; 每次一個線程進入同步塊,其餘全部線程陷入等待狀態;而後active線程調用notifyALL()函數喚醒全部等待線程,全部線程競爭,只有一個線程競爭成功,得到CPU執行。競爭失敗的線程處於就緒狀態,長期競爭失敗的線程就會飢餓。

線程之間的對資源(object)競爭致使的飢餓,爲了不競爭,因此想辦法一次喚醒一個線程。也就是下面講的FairLock 公平鎖機制。

Implementing Fairness in Java

使用鎖lock來代替同步塊synchonized block

每個調用 lock() 的線程都會進入一個隊列,當解鎖後,只有隊列裏的第一個線程 (隊首)被容許鎖住 Fairlock 實例,全部其它的線程都將處於等待狀態,直到他們處於隊列頭部。
公平鎖實現機制:爲每個線程建立一個專屬鎖對象(而非多個線程共享一個對象,來wait/notify()),而後用一個隊列來管理這些鎖對象,嘗試加鎖的線程會在各自的對象上等待,當一個線程unlock的時候,只通知隊列頭的鎖對象,以喚醒其對應的線程

爲了讓這個 Lock 類具備可重入性,咱們須要對它作一點小的改動:

javapublic class FairLock {
    private boolean           isLocked       = false;
    private Thread            lockingThread  = null;
    private List<QueueObject> waitingThreads =
            new ArrayList<QueueObject>();

  public void lock() throws InterruptedException{
    QueueObject queueObject           = new QueueObject();
    boolean     isLockedForThisThread = true;
    synchronized(this){
        waitingThreads.add(queueObject);
    }

    while(isLockedForThisThread){
      synchronized(this){
        isLockedForThisThread =
            isLocked || waitingThreads.get(0) != queueObject;
        if(!isLockedForThisThread){
          isLocked = true;
           waitingThreads.remove(queueObject);
           lockingThread = Thread.currentThread();
           return;
         }
      }
      try{
        queueObject.doWait();
      }catch(InterruptedException e){
        synchronized(this) { waitingThreads.remove(queueObject); }
        throw e;
      }
    }
  }

  public synchronized void unlock(){
    if(this.lockingThread != Thread.currentThread()){
      throw new IllegalMonitorStateException(
        "Calling thread has not locked this lock");
    }
    isLocked      = false;
    lockingThread = null;
    if(waitingThreads.size() > 0){
      waitingThreads.get(0).doNotify();
    }
  }
}
相關文章
相關標籤/搜索