Basic Of Concurrency(十四: Java中的鎖)

Lock跟Java中的synchronized關鍵字同樣,都是用於線程的同步機制。不一樣的是Lock相比synchronized關鍵字提供更加豐富的功能和靈活性。html

從Java5開始,java.util.concurrent.locks包中提供了幾種不一樣的Lock實現,所以咱們不須要本身去實現鎖。但咱們仍然須要知道怎麼使用它們,以及它們的底層原理。java

一個簡單的Lock

咱們來看一下這樣一個同步塊:安全

public class Counter {
    private int count = 0;

    public int increment() {
        synchronized (this) {
            return ++count;
        }
    }
}
複製代碼

咱們能夠注意到increment()方法中的同步塊。這個同步塊確保了每次只會有一個線程執行++count;同步塊的中的代碼相對比較簡單,如今咱們只須要關注++count();這一行代碼便可。post

上面的例子用Lock來替換同步塊:this

public class Counter {
    private Lock lock = new Lock();
    private int count = 0;

    public int increment() {
        lock.lock();
        return ++count;
        lock.unlock();
    }
}
複製代碼

increment()方法中,當有線程取得Lock實例後,其餘線程都會在調用lock()後被阻塞,直到取得Lock實例的線程調用了unlock()爲止。spa

下面是一個簡單的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),咱們稱這種循環爲"旋轉鎖"。旋轉鎖以及wait()和notify()方法調用在線程通信一文中有說起。當isLocked爲true時,線程會進入循環內部,從而調用wait()方法進入等待狀態。實例中,在其餘線程沒有調用notify()發送信號喚醒的狀況下,線程如果意外喚醒會從新檢查isLocked條件是否爲false以此來肯定線程是否能夠安全的執行,若仍然爲true則線程從新調用wait()方法進入等待狀態。所以旋轉鎖能夠保護意外喚醒帶來的潛在風險。若線程檢查isLocked爲false則退出while循環,將isLocked置換爲true,以此來標記得到當前Lock實例。讓其餘線程阻塞在lock()調用上。code

當線程執行完臨界區代碼(位於lock()和unlock()調用之間的代碼)後,會調用unlock()方法釋放Lock()實例。調用unlock()方法會將isLocked置換爲false.並喚醒調用了lock()方法中的wait()方法進入等待狀態的線程,若是有的話。htm

可重入鎖

Java中的synchronized同步塊是可重入的。這意味着,若是一個Java線程進入一個同步代碼塊取得當前對象鎖,那麼它能夠再進入其餘所用與之相同對象鎖的其餘同步代碼塊。以下所示:對象

public class Reentrant{
    public synchronized void outer() {
        inner();
    }

    public synchronized void inner() {
        // do something you like
    }
}
複製代碼

咱們能夠注意到outer()和inner()方法簽名中都有synchronized聲明,這在Java中等同於synchronized(this)同步代碼塊。當多個方法都是以"this"即當前對象做爲監控對象時,那麼一個線程在調用完outer()方法後,天然能夠在outer()方法內部調用inner()方法。一個線程將一個對象做爲監控對象即取得該對象鎖後,它能夠進入其餘使用相同對象做爲對象鎖的同步代碼塊。這種狀況咱們稱之爲可重入。線程能夠反覆進入它所取得監控對象的所有同步代碼塊。

上文中給出的Lock實現並非可重入的。若是咱們將上文的Reentrant.class改寫成以下代碼所示的Reentrant2.class,那麼線程在調用完outer()方法後,將在調用inner()方法中的lock.lock()方法後阻塞。

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

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

    public synchronized void inner() {
        lock.lock();
        //do something
        lock.unlock();
    }
}
複製代碼

線程在調用outer()時會先鎖住Lock實例。而後再調用inner()方法。在inner()方法內部,線程會再一次嘗試鎖住Lock實例,但註定會失敗。由於lock實例在調用outer()方法時已經被鎖住。

在尚未調用unlock()方法釋放Lock()實例的時候,再次調用lock()的時候會讓線程阻塞在調用lock()方法上。以下lock()方法:

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

旋轉鎖中的條件,決定了線程允不容許退出lock()方法。當isLocked爲false時,意味着線程容許退出lock()方法,即容許鎖住Lock實例。

讓Lock.class支持可重入,咱們須要做出一點改動:

public class Lock {
    private boolean isLocked = false;
    private Thread lockingThread;
    int lockedCount = 0;

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

    public void unlock() {
        Thread callingThread = Thread.currentThread();
        if (lockingThread != null && lockingThread == callingThread) {
            lockedCount--;
            if (lockedCount == 0) {
                isLocked = false;
                notify();
            }
        }
    }
}
複製代碼

咱們能夠注意到while循環(旋轉鎖)中多了一個條件,即當前線程是否爲持有鎖實例的線程。當只有兩個條件都符合時線程才能退出循環和退出lock()方法調用。

咱們須要對線程鎖住Lock實例的次數進行記錄。不然的話,調用一次unlock()方法就會讓當前線程釋放掉鎖儘管實際上它調用了屢次lock()。所以咱們須要保障lock()和unlock()方法的調用次數是一致的,即每調用一次lock()即必須調用一次unlock()。

如今的Lock.class已是可重入的了。

公平鎖

Java的synchronized同步代碼塊不能保證線程按照必定的順序進入同步代碼塊。所以存在必定的風險,有一或者幾個線程一直沒法進入同步代碼塊,由於其餘線程一直代替它們進入到同步代碼塊。咱們稱這種狀況爲肌餓現象。

爲了解決這個問題,咱們實現的鎖須要保證必定的公平性,但上文給出的Lock實現並不能保證線程的公平性。飢餓和公平一文中詳細討論了這種狀況。

在finally語句中調用unlock()

當使用Lock來保證臨界區代碼的同步時,臨界區中的代碼可能會拋出異常。因此頗有必要在finally語句中來調用unlock()方法。不管線程執行的代碼異常與否,始終可以釋放它所持有的鎖,以讓其餘線程能正常執行。以下所示:

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

咱們使用這個小結構來保證即便臨界區代碼拋出異常,線程也能正常釋放掉所持有的鎖。若是咱們沒有這麼作,一旦臨界區代碼出現異常,線程將沒法釋放鎖,以致於其餘調用了該鎖的lock()方法的線程將會永遠等待下去。

該系列博文爲筆者複習基礎所著譯文或理解後的產物,複習原文來自Jakob Jenkov所著Java Concurrency and Multithreading Tutorial

上一篇: Slipped Conditions
下一篇: Java中的讀寫鎖

相關文章
相關標籤/搜索