鍍金池/ 教程/ Java/ Java 中的鎖
Slipped Conditions
阻塞隊列
無阻塞算法
嵌套管程鎖死
Java 并發(fā)性和多線程介紹
死鎖
線程安全及不可變性
并發(fā)編程模型
Java 中的讀/寫鎖
剖析同步器
競態(tài)條件與臨界區(qū)
多線程的優(yōu)點
CAS
線程通信
如何創(chuàng)建并運行 java 線程
阿姆達爾定律
避免死鎖
信號量
多線程的代價
饑餓和公平
線程池
重入鎖死
Java 中的鎖
Java 內(nèi)存模型
線程安全與共享資源
Java 同步塊

Java 中的鎖

鎖像 synchronized 同步塊一樣,是一種線程同步機制,但比 Java 中的 synchronized 同步塊更復(fù)雜。因為鎖(以及其它更高級的線程同步機制)是由 synchronized 同步塊的方式實現(xiàn)的,所以我們還不能完全擺脫 synchronized 關(guān)鍵字(譯者注:這說的是 Java 5 之前的情況)。

自 Java 5 開始,java.util.concurrent.locks 包中包含了一些鎖的實現(xiàn),因此你不用去實現(xiàn)自己的鎖了。但是你仍然需要去了解怎樣使用這些鎖,且了解這些實現(xiàn)背后的理論也是很有用處的。可以參考我對 java.util.concurrent.locks.Lock 的介紹,以了解更多關(guān)于鎖的信息。

以下是本文所涵蓋的主題:

  1. 一個簡單的鎖
  2. 鎖的可重入性
  3. 鎖的公平性
  4. 在 finally 語句中調(diào)用 unlock()

一個簡單的鎖

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

public class Counter{
    private int count = 0;

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

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

以下的 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 實例對象進行加鎖,因此所有對該對象調(diào)用 lock()方法的線程都會被阻塞,直到該 Lock 對象的 unlock()方法被調(diào)用。

這里有一個 Lock 類的簡單實現(xiàn):

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)循環(huán),它又被叫做“自旋鎖”。自旋鎖以及 wait()和 notify()方法在線程通信這篇文章中有更加詳細的介紹。當(dāng) isLocked 為 true 時,調(diào)用 lock()的線程在 wait()調(diào)用上阻塞等待。為防止該線程沒有收到 notify()調(diào)用也從 wait()中返回(也稱作虛假喚醒),這個線程會重新去檢查 isLocked 條件以決定當(dāng)前是否可以安全地繼續(xù)執(zhí)行還是需要重新保持等待,而不是認為線程被喚醒了就可以安全地繼續(xù)執(zhí)行了。如果 isLocked 為 false,當(dāng)前線程會退出 while(isLocked)循環(huán),并將 isLocked 設(shè)回 true,讓其它正在調(diào)用 lock()方法的線程能夠在 Lock 實例上加鎖。

當(dāng)線程完成了臨界區(qū)(位于 lock()和 unlock()之間)中的代碼,就會調(diào)用 unlock()。執(zhí)行 unlock()會重新將 isLocked 設(shè)置為 false,并且通知(喚醒)其中一個(若有的話)在 lock()方法中調(diào)用了 wait()函數(shù)而處于等待狀態(tài)的線程。

鎖的可重入性

Java 中的 synchronized 同步塊是可重入的。這意味著如果一個 java 線程進入了代碼中的 synchronized 同步塊,并因此獲得了該同步塊使用的同步對象對應(yīng)的管程上的鎖,那么這個線程可以進入由同一個管程對象所同步的另一個 java 代碼塊。下面是一個例子:

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

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

注意 outer()和 inner()都被聲明為 synchronized,這在 Java 中和 synchronized(this)塊等效。如果一個線程調(diào)用了 outer(),在 outer()里調(diào)用 inner()就沒有什么問題,因為這兩個方法(代碼塊)都由同一個管程對象(”this”)所同步。如果一個線程已經(jīng)擁有了一個管程對象上的鎖,那么它就有權(quán)訪問被這個管程對象同步的所有代碼塊。這就是可重入。線程可以進入任何一個它已經(jīng)擁有的鎖所同步著的代碼塊。

前面給出的鎖實現(xiàn)不是可重入的。如果我們像下面這樣重寫 Reentrant 類,當(dāng)線程調(diào)用 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();
    }
}

調(diào)用 outer()的線程首先會鎖住 Lock 實例,然后繼續(xù)調(diào)用 inner()。inner()方法中該線程將再一次嘗試鎖住 Lock 實例,結(jié)果該動作會失敗(也就是說該線程會被阻塞),因為這個 Lock 實例已經(jīng)在 outer()方法中被鎖住了。

兩次 lock()之間沒有調(diào)用 unlock(),第二次調(diào)用 lock 就會阻塞,看過 lock()實現(xiàn)后,會發(fā)現(xiàn)原因很明顯:

public class Lock{
    boolean isLocked = false;

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

    ...
}

一個線程是否被允許退出 lock()方法是由 while 循環(huán)(自旋鎖)中的條件決定的。當(dāng)前的判斷條件是只有當(dāng) 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();
            }
        }
    }

    ...
}

注意到現(xiàn)在的 while 循環(huán)(自旋鎖)也考慮到了已鎖住該 Lock 實例的線程。如果當(dāng)前的鎖對象沒有被加鎖(isLocked = false),或者當(dāng)前調(diào)用線程已經(jīng)對該 Lock 實例加了鎖,那么 while 循環(huán)就不會被執(zhí)行,調(diào)用 lock()的線程就可以退出該方法(譯者注:“被允許退出該方法”在當(dāng)前語義下就是指不會調(diào)用 wait()而導(dǎo)致阻塞)。

除此之外,我們需要記錄同一個線程重復(fù)對一個鎖對象加鎖的次數(shù)。否則,一次 unblock()調(diào)用就會解除整個鎖,即使當(dāng)前鎖已經(jīng)被加鎖過多次。在 unlock()調(diào)用沒有達到對應(yīng) lock()調(diào)用的次數(shù)之前,我們不希望鎖被解除。

現(xiàn)在這個 Lock 類就是可重入的了。

鎖的公平性

Java 的 synchronized 塊并不保證嘗試進入它們的線程的順序。因此,如果多個線程不斷競爭訪問相同的 synchronized 同步塊,就存在一種風(fēng)險,其中一個或多個線程永遠也得不到訪問權(quán) —— 也就是說訪問權(quán)總是分配給了其它線程。這種情況被稱作線程饑餓。為了避免這種問題,鎖需要實現(xiàn)公平性。本文所展現(xiàn)的鎖在內(nèi)部是用 synchronized 同步塊實現(xiàn)的,因此它們也不保證公平性。饑餓和公平中有更多關(guān)于該內(nèi)容的討論。

在 finally 語句中調(diào)用 unlock()

如果用 Lock 來保護臨界區(qū),并且臨界區(qū)有可能會拋出異常,那么在 finally 語句中調(diào)用 unlock()就顯得非常重要了。這樣可以保證這個鎖對象可以被解鎖以便其它線程能繼續(xù)對其加鎖。以下是一個示例:

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

這個簡單的結(jié)構(gòu)可以保證當(dāng)臨界區(qū)拋出異常時 Lock 對象可以被解鎖。如果不是在 finally 語句中調(diào)用的 unlock(),當(dāng)臨界區(qū)拋出異常時,Lock 對象將永遠停留在被鎖住的狀態(tài),這會導(dǎo)致其它所有在該 Lock 對象上調(diào)用 lock()的線程一直阻塞。

上一篇:信號量下一篇:線程池