首頁 >Java >java教程 >java 多執行緒-鎖詳解及範例程式碼

java 多執行緒-鎖詳解及範例程式碼

高洛峰
高洛峰原創
2017-01-05 15:30:301269瀏覽

自 Java 5 開始,java.util.concurrent.locks 套件中包含了一些鎖定的實現,因此你不用去實現自己的鎖定了。但是你仍然需要去了解怎麼使用這些鎖。

一個簡單的鎖

讓我們從 java 中的一個同步區塊開始:

public class Counter{
  private int count = 0;
 
  public int inc(){
    synchronized(this){
      return ++count;
    }
  }
}

可以看到在 inc()方法中有一個 synchronized(this)程式碼區塊。該程式碼區塊可以保證在同一時間只有一個執行緒可以執行 return ++count。雖然在 synchronized 的同步區塊中的程式碼可以更加複雜,但是++count 這種簡單的操作已經足以表達出線程同步的意思。

以下的Counter 類別以Lock 取代synchronized 達到了同樣的目的:

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 實例物件進行加鎖,因此所有對該物件呼叫lock()方法的執行緒都會被阻塞,直到該Lock 物件的unlock()方法被呼叫。

這裡有一個 Lock 類別的簡單實作:

public class Counter{
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)循環,它又被叫做「自旋鎖」。當 isLocked 為 true 時,呼叫 lock()的執行緒在 wait()呼叫上阻塞等待。為防止該執行緒沒有收到notify()呼叫也從wait()中傳回(也稱為虛假喚醒),這個執行緒會重新去檢查isLocked 條件以決定目前是否可以安全地繼續執行還是需要重新保持等待,而不是認為執行緒被喚醒了就可以安全地繼續執行了。如果 isLocked 為 false,目前執行緒會退出 while(isLocked)循環,並將 isLocked 設回 true,讓其它正在呼叫 lock()方法的執行緒能夠在 Lock 實例上加鎖。

當執行緒完成了臨界區(位於 lock()和 unlock()之間)中的程式碼,就會呼叫 unlock()。執行 unlock()會重新將 isLocked 設為 false,並且通知(喚醒)其中一個(若有的話)在 lock()方法中呼叫了 wait()函數而處於等待狀態的執行緒。

鎖的可重入性

Java 中的 synchronized 同步區塊是可重入的。這意味著如果一個java 執行緒進入了程式碼中的synchronized 同步區塊,並因此獲得了該同步區塊所使用的同步物件對應的管程上的鎖,那麼這個執行緒可以進入由同一個管程物件所同步的另一個java 程式碼塊。以下是一個例子:

public class Reentrant{
  public synchronized outer(){
    inner();
  }
 
  public synchronized inner(){
    //do something
  }
}

注意 outer()和 inner()都被宣告為 synchronized,這在 Java 中和 synchronized(this)區塊等效。如果一個執行緒呼叫了 outer(),在 outer()裡呼叫 inner()就沒有什麼問題,因為這兩個方法(程式碼區塊)都由同一個管程物件(”this”)所同步。如果一個執行緒已經擁有了一個管程物件上的鎖,那麼它就有權存取被這個管程物件同步的所有程式碼區塊。這就是可重入。執行緒可以進入任何一個它已經擁有的鎖所同步著的程式碼區塊。

前面給出的鎖實現不是可重入的。如果我們像下面這樣重寫 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 實例已經在 outer()方法中被鎖定了。

兩次lock()之間沒有呼叫unlock(),第二次呼叫lock 就會阻塞,看過lock()實作後,會發現原因很明顯:

public class Lock{
  boolean isLocked = false;
 
  public synchronized void lock()
    throws InterruptedException{
    while(isLocked){
      wait();
    }
    isLocked = true;
  }
 
  ...
}

一個執行緒是否被允許退出lock()方法是由while 循環(自旋鎖)中的條件決定的。目前的判斷條件是只有當 isLocked 為 false 時 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();
      }
    }
  }
 
  ...
}

注意到現在的 while 循環(自旋鎖)也考慮到了已鎖住該 Lock 實例的線程。如果目前的鎖定物件沒有被加鎖(isLocked = false),或是目前呼叫執行緒已經對該Lock 實例加了鎖,那麼while 迴圈就不會被執行,而呼叫lock()的執行緒就可以退出該方法(譯者註:「被允許退出該方法」在目前語意下就是指不會呼叫wait()而導致阻塞)。

除此之外,我們需要記錄同一個執行緒重複對一個鎖定物件加鎖的次數。否則,一次 unblock()呼叫就會解除整個鎖,即使目前鎖已經被加鎖過多次。在 unlock()呼叫沒有達到對應 lock()呼叫的次數之前,我們不希望鎖被解除。

現在這個 Lock 類別就是可重入的了。

鎖的公平性

Java 的 synchronized 區塊並不保證嘗試進入它們的執行緒的順序。因此,如果多個執行緒不斷競爭存取相同的 synchronized 同步區塊,就存在一種風險,其中一個或多個執行緒永遠也無法獲得存取權 —— 也就是說存取權總是分配給了其它執行緒。這種情況被稱作線程飢餓。為了避免這種問題,鎖需要實現公平性。本文所展現的鎖在內部是用 synchronized 同步塊實現的,因此它們也不保證公平性。

在 finally 语句中调用 unlock()

如果用 Lock 来保护临界区,并且临界区有可能会抛出异常,那么在 finally 语句中调用 unlock()就显得非常重要了。这样可以保证这个锁对象可以被解锁以便其它线程能继续对其加锁。以下是一个示例:

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

这个简单的结构可以保证当临界区抛出异常时 Lock 对象可以被解锁。如果不是在 finally 语句中调用的 unlock(),当临界区抛出异常时,Lock 对象将永远停留在被锁住的状态,这会导致其它所有在该 Lock 对象上调用 lock()的线程一直阻塞。

以上就是关于 java 多线程锁的资料整理,后续继续补充相关资料,谢谢大家对本站的支持!


更多java 多线程-锁详解及示例代码相关文章请关注PHP中文网!


陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn