聊聊 Java 多線程(4)- 鎖的分類有這么多

目前,多線程編程可以說是在大部分平臺和應(yīng)用上都需要實現(xiàn)的一個基本需求旭咽。本系列文章就來對 Java 平臺下的多線程編程知識進(jìn)行講解,從概念入門赌厅、底層實現(xiàn)上層應(yīng)用都會涉及到穷绵,預(yù)計一共會有五篇文章,希望對你有所幫助 ????

本篇文章是第四篇特愿,來介紹 Java 平臺下的鎖機制仲墨,鎖是 Java 開發(fā)者實現(xiàn)線程同步最為簡單的一種方式

鎖是 Java 開發(fā)者實現(xiàn)線程同步最為簡單的一種方式,最簡單的情形下我們只需要添加一個 synchronized 關(guān)鍵字就可以實現(xiàn)線程同步揍障,但鎖的分類細(xì)數(shù)下來也不少目养,JVM 自動為代碼中的鎖所做的優(yōu)化措施也有很多,這里來對其詳細(xì)講一講

一毒嫡、悲觀鎖癌蚁、樂觀鎖

悲觀鎖與樂觀鎖兩者體現(xiàn)了多個線程在對共享數(shù)據(jù)進(jìn)行并發(fā)操作時的不同看法

對于多個線程間的共享數(shù)據(jù),悲觀鎖認(rèn)為自己在使用數(shù)據(jù)的時候很有可能會有其它線程也剛好前來修改數(shù)據(jù),因為在使用數(shù)據(jù)前都會加上鎖努释,確保在使用過程中數(shù)據(jù)不會被其它線程修改碘梢。synchronized 關(guān)鍵字和 Lock 接口的實現(xiàn)類都屬于悲觀鎖

樂觀鎖則認(rèn)為在使用數(shù)據(jù)的過程中其它線程也剛好前來修改數(shù)據(jù)的可能性很低,所以在使用數(shù)據(jù)前不會加鎖伐蒂,而只是在更新數(shù)據(jù)的時候判斷數(shù)據(jù)之前是否有被別的線程更新了煞躬。如果數(shù)據(jù)沒有被更新,當(dāng)前線程就可以將自己修改后的數(shù)據(jù)成功寫入逸邦。而如果數(shù)據(jù)已經(jīng)被其它線程更新過了恩沛,則根據(jù)不同的實現(xiàn)方式來執(zhí)行不同的補救操作(報錯或者重復(fù)嘗試)。樂觀鎖在 Java 中是通過使用無鎖編程來實現(xiàn)的缕减,最常采用的是 CAS 算法复唤,java.util.concurrent 包中的原子類就是通過 CAS 自旋來實現(xiàn)的

總的來說,悲觀鎖適合寫操作較多的場景烛卧,加鎖可以保證執(zhí)行寫操作時數(shù)據(jù)的正確性佛纫。樂觀鎖適合讀操作較多的場景,不加鎖能夠使讀操作的性能大幅度提升


synchronized 關(guān)鍵字和 Lock 接口所代表的悲觀鎖比較常見总放,這里主要來看下樂觀鎖

樂觀鎖采用的 CAS 算法全稱是 Compare And Swap(比較與交換)呈宇,是一種無鎖算法,在不使用鎖(所以也不會導(dǎo)致線程被阻塞)的情況下實現(xiàn)在多線程之間的變量同步

CAS 算法涉及到三個操作數(shù):

  • 需要讀寫的內(nèi)存值 V
  • 進(jìn)行比較的值 A
  • 要寫入的新值 B

當(dāng)且僅當(dāng) V 的值等于 A 時局雄,CAS 才會用新值 B 來更新 V 的值甥啄,且保證了“比較+更新”這整個操作的原子性。當(dāng) V 的值不等于 A 時則不會執(zhí)行任何操作炬搭。一般情況下蜈漓,“更新”是一個會不斷重試的操作

這里來看下 AtomicInteger 類的用于自增加一的方法 incrementAndGet() 是如何實現(xiàn)的。

public class AtomicInteger extends Number implements java.io.Serializable {
  
  private static final Unsafe unsafe = Unsafe.getUnsafe();
  
  /**
    * Atomically increments by one the current value.
    *
    * @return the updated value
   */
  public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
  }
  
}

incrementAndGet() 方法是通過 unsafe.getAndAddInt() 來實現(xiàn)的宫盔。getAndAddInt() 方法會循環(huán)獲取給定對象 o 中的偏移量 offset 處的值 v融虽,然后判斷內(nèi)存值是否等于 v。如果相等則將內(nèi)存值修改為 v + delta灼芭,否則就繼續(xù)整個循環(huán)進(jìn)行重復(fù)嘗試有额,直到修改成功才退出循環(huán),并且將舊值返回彼绷。整個“比較+更新”操作封裝在 compareAndSwapInt() 方法中巍佑,在 JNI 里是借助于一個 CPU 指令完成的,屬于原子操作寄悯,可以保證多個線程都能夠看到同一個變量的修改值

public final int getAndAddInt(Object o, long offset, int delta) {
    int v;
    do {
        v = getIntVolatile(o, offset);
    } while (!compareAndSwapInt(o, offset, v, v + delta));
    return v;
}

public native int getIntVolatile(Object var1, long var2);

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

CAS 雖然很高效萤衰,但是也存在ABA問題,且如果 CAS 操作長時間不成功的話猜旬,會導(dǎo)致其一直自旋脆栋,給處理器帶來非常大的開銷

二胳螟、可重入鎖、非可重入鎖

鎖是否可重入表示的是這么一種特性:鎖的持有線程在不釋放鎖的前提下筹吐,是否能夠再次申請到同一個鎖糖耸。例如,如果 synchronized 是可重入鎖丘薛,那么 doSomething1() 是可以正常執(zhí)行的嘉竟。如果 synchronized 是不可重入鎖,那么 doSomething1() 就會導(dǎo)致死鎖洋侨。而在現(xiàn)實情況下舍扰,Java 中 synchronized 和 ReentrantLock 都是可重入鎖

private synchronized void doSomething1() {
    doSomething2();
}

private synchronized void doSomething2() {

}

這里也以 ReentrantLock 作為例子來從看下其重入流程

我們知道,在使用 ReentrantLock 時希坚,我們調(diào)用了 Lock.lock() N 次后就要相應(yīng)調(diào)用 Lock.unlock() N 次才可以使得持有線程真正地釋放了鎖边苹,那么這里自然就需要有個狀態(tài)值來記錄 ReentrantLock 申請了幾次鎖(即重入了幾次)。ReentrantLock 包含一個內(nèi)部類 Sync裁僧,Sync 繼承自 AQS(AbstractQueuedSynchronizer)

AQS 中維護(hù)了一個 int 類型的同步狀態(tài)值 status个束,其初始值為 0,在不同的場景下具有不同的含義聊疲,對于 ReentrantLock 來說就是用來標(biāo)記重入次數(shù)茬底。當(dāng)線程嘗試獲取鎖時,ReentrantLock 就會嘗試獲取并更新 status 值

  • 如果 status 等于 0获洲。表示鎖沒有被其它線程搶占阱表,則把 status 置為 1,同時當(dāng)前線程成功搶占到鎖
  • 如果 status 不等于 0贡珊。判斷當(dāng)前線程是否是該鎖的持有線程最爬,如果是的話則執(zhí)行 status + 1,表示當(dāng)前線程再次重入了一次门岔。如果當(dāng)前線程不是該鎖的持有線程爱致,則意味著搶占失敗

當(dāng)持有線程釋放鎖時,ReentrantLock 同樣會先獲取當(dāng)前 status 值固歪。如果 status - 1 等于 0蒜鸡,表示當(dāng)前線程已經(jīng)撤銷了所有的申請操作胯努,此時線程才會真正釋放鎖牢裳,否則持有線程就還是依然占用著鎖

三、公平鎖叶沛、非公平鎖

當(dāng)多個線程同時申請同一個排他性資源蒲讯,申請資源失敗的線程往往是會存入一個等待隊列中,當(dāng)后續(xù)資源被其持有線程釋放時灰署,如果剛好有一個活躍線程來申請資源判帮,此時選擇哪一個線程來獲取資源的獨占權(quán)就是一個資源調(diào)度的過程局嘁,資源調(diào)度策略的一個重要屬性就是能否保證公平性。所謂公平性晦墙,是指資源的申請者是否嚴(yán)格按照申請順序而被授予資源的獨占權(quán)悦昵。如果資源的任何一個先申請者總是能夠被比任何一個后申請者先獲得資源的獨占權(quán),那么該策略就被稱為公平調(diào)度策略晌畅。如果資源的后申請者可能比先申請者先獲得資源的獨占權(quán)但指,那么該策略就被稱為非公平調(diào)度策略。注意抗楔,非公平調(diào)度策略往往只是不保證資源調(diào)度的公平性棋凳,即它只是允許不公平的資源調(diào)度現(xiàn)象,而不是表示它刻意造就不公平的資源調(diào)度

公平的資源調(diào)度策略不允許插隊現(xiàn)象的出現(xiàn)连躏,資源申請者總是按照先來后到的順序獲得資源的獨占權(quán)剩岳。如果當(dāng)前等待隊列為空,則來申請資源的線程可以直接獲得資源的獨占權(quán)入热。如果等待隊列不為空拍棕,那么每個新到來的線程就被插入等待隊列的隊尾。公平的資源調(diào)度策略的優(yōu)點是:每個資源申請者從開始申請資源到獲得相應(yīng)資源的獨占權(quán)所需時間的偏差會比較小勺良,即每個申請者成功申請到資源所需的時間基本相同莫湘,且可以避免出現(xiàn)線程饑餓現(xiàn)象。缺點是吞吐率較低郑气,為了保證 FIFO 加大了發(fā)生線程上下文切換的可能性

非公平的資源調(diào)度策略則允許插隊現(xiàn)象幅垮。新到來的線程會直接嘗試申請資源,只有當(dāng)申請失敗時才會將線程插入等待隊列的隊尾尾组。假設(shè)兩種多個線程一起競爭同一個排他性資源的場景:

  1. 當(dāng)資源被釋放時忙芒,如果剛好有一個活躍線程來申請資源,該線程就可以直接搶占到資源讳侨,而無需去喚醒等待隊列中的線程呵萨。這種場景相對公平調(diào)度策略就少了將新到來的線程暫停將等待隊列隊頭的線程喚醒的兩個操作,而資源也一樣有被得到使用
  2. 即使等待隊列中的某個線程已經(jīng)被喚醒來試圖搶占資源的獨占權(quán)跨跨,如果新到來的活躍線程占用資源的時間不長的話潮峦,那么就有可能在被喚醒的線程開始申請資源之前,新到來的活躍線程已經(jīng)釋放了對資源的獨占權(quán)勇婴,從而不妨礙被喚醒的線程申請資源忱嘹。這種場景也一樣避免了將新到來的線程暫停這么一個操作

因此,非公平調(diào)度策略的優(yōu)點主要有兩點:

  1. 吞吐率一般來說會比公平調(diào)度策略高耕渴,即單位時間內(nèi)它可以為更多的申請者調(diào)配資源
  2. 降低了發(fā)生上下文切換的概率

非公平調(diào)度策略的缺點主要有兩點:

  1. 由于允許插隊現(xiàn)象拘悦,極端情況下可能導(dǎo)致等待隊列中的線程永遠(yuǎn)也無法獲得其所需的資源,即出現(xiàn)線程饑餓的活性故障現(xiàn)象
  2. 每個資源申請者從開始申請資源到獲得相應(yīng)資源的獨占權(quán)所需時間的偏差可能較大橱脸,即有的線程可能很快就能申請到資源础米,而有的線程則要經(jīng)歷若干次暫停與喚醒才能成功申請到資源

綜上所訴分苇,公平調(diào)度策略適用于資源被持有的時間較長或者線程申請資源的平均時間間隔較長的情形,或者要求申請資源所需的時間偏差較小的情況屁桑∫绞伲總的來說使用公平調(diào)度策略的開銷會比使用非公平調(diào)度策略的開銷要大,因此在沒有特別需求的情況下蘑斧,應(yīng)該默認(rèn)使用非公平調(diào)度策略

公平鎖就是指采用了公平調(diào)度策略的鎖糟红,非公平鎖就是指采用了非公平調(diào)度策略的鎖。Java 中的 synchronized 就是非公平鎖乌叶;而 ReentrantLock 既支持公平調(diào)度策略也支持非公平調(diào)度策略盆偿,且默認(rèn)使用的也是非公平調(diào)度策略

這里來簡單看下 ReentrantLock 的源碼來了解下公平鎖和非公平鎖的實現(xiàn)區(qū)別

ReentrantLock 申請和釋放鎖的大部分邏輯都是在其內(nèi)部類 Sync 里實現(xiàn)的。Sync 包含公平鎖 FairSync 和非公平鎖 NonfairSync 兩個不同的子類實現(xiàn)准浴,ReentrantLock 默認(rèn)使用的是 NonfairSync

FairSyncNonfairSync 在申請鎖時的會分別調(diào)用以下兩個方法事扭,兩者的唯一的區(qū)別只在于公平鎖在獲取同步狀態(tài)時多了一個限制條件:hasQueuedPredecessors()

1.jpg

hasQueuedPredecessors() 方法用于判斷等待隊列中是否有排在當(dāng)前線程之前的線程,如果有返回 true乐横,否則返回 false求橄。所以說,ReentrantLock 的公平調(diào)度策略只有在等待隊列為空時才允許當(dāng)前的活躍線程執(zhí)行申請鎖的操作葡公,而非公平調(diào)度策略則是直接就進(jìn)行申請

四罐农、互斥鎖、共享鎖

互斥鎖也稱為排他鎖催什,是指該鎖一次只能被一個線程持有涵亏,當(dāng)某個線程已經(jīng)在持有鎖的時候其它來申請同個鎖實例的線程只能進(jìn)行等待,以此來保證臨界區(qū)內(nèi)共享數(shù)據(jù)的安全性蒲凶。Java 中的 synchronized 和 java.util.concurrent.locks.Lock 接口的實現(xiàn)類就屬于互斥鎖

互斥鎖使得多個線程無法以線程安全的方式在同一時刻對共享數(shù)據(jù)進(jìn)行只讀取而不更新的操作气筋,這在共享數(shù)據(jù)讀取頻繁但更新頻率較低的情況下降低了系統(tǒng)的并發(fā)性,共享鎖就是為了應(yīng)對這種問題而誕生的旋圆。共享鎖是一種改進(jìn)型的排他鎖宠默,也稱為共享/排他鎖。共享鎖允許多個線程同時讀取共享變量灵巧,但是一次只允許一個線程對共享變量進(jìn)行更新搀矫。任何線程讀取共享變量的時候,其它線程無法更新這些變量刻肄;一個線程更新共享變量的時候瓤球,其它線程都無法讀取和更新這些變量

Java 平臺中的讀寫鎖就是對共享鎖這個概念的實現(xiàn),由 java.util.concurrent.locks.ReadWriteLock 接口來定義肄方,其默認(rèn)實現(xiàn)類是 java.util.concurrent.locks.ReentrantReadWriteLock

ReadWriteLock 接口定義了兩個方法冰垄,分別用來獲取讀鎖(ReadLock)和寫鎖(WriteLock)。ReadLock 是共享的权她,WriteLock 是排他的虹茶,ReadLock 和 WriteLock 的操作最終都要轉(zhuǎn)交由內(nèi)部類 Sync 來完成

上面在講“可重入鎖與非可重入鎖”這一節(jié)內(nèi)容的時候,有提到:AQS 中維護(hù)了一個 int 類型的同步狀態(tài)值 status隅要,其初始值為 0蝴罪,在不同的場景下具有不同的含義。對于 ReentrantReadWriteLock 來說步清,status 就用來標(biāo)記當(dāng)前持有讀鎖和寫鎖的線程分別是多少

而為了在一個 32 位的 int 類型整數(shù)上來存儲兩種不同含義的數(shù)據(jù)要门,就需要將 status 進(jìn)行分段切割,高 16 位用來存儲讀鎖當(dāng)前被獲取的次數(shù)廓啊,低 16 位用來存儲寫鎖當(dāng)前被獲取的次數(shù)

Sync 類內(nèi)部就提供了兩個分別用來計算讀線程和寫線程個數(shù)的方法

1欢搜、共享流程

這里先來看下線程在獲取讀鎖時的申請流程,這里主要是要先前置判斷下讀寫鎖的寫鎖是否已經(jīng)被持有了

protected final int tryAcquireShared(int unused) {
    Thread current = Thread.currentThread();
    int c = getState();
    if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current)
        //如果當(dāng)前已經(jīng)有線程持有了寫鎖谴轮,且該線程并非當(dāng)前線程
        //則返回 -1炒瘟,表示讀鎖獲取失敗
        //這里之所以要判斷線程是否相等,是因為 ReentrantReadWriteLock 支持鎖的降級第步,可以在已經(jīng)持有寫鎖的時候申請讀鎖
        return -1;

    //下面就是多個線程前來申請讀鎖或者是同個線程多次申請讀鎖的流程了
    int r = sharedCount(c);
    if (!readerShouldBlock() &&
        r < MAX_COUNT &&
        compareAndSetState(c, c + SHARED_UNIT)) {
        if (r == 0) {
            firstReader = current;
            firstReaderHoldCount = 1;
        } else if (firstReader == current) {
            firstReaderHoldCount++;
        } else {
            HoldCounter rh = cachedHoldCounter;
            if (rh == null || rh.tid != getThreadId(current))
                cachedHoldCounter = rh = readHolds.get();
            else if (rh.count == 0)
                readHolds.set(rh);
            rh.count++;
        }
        return 1;
    }
    return fullTryAcquireShared(current);
}

而線程在釋放讀鎖時,主要就是更新 state 值,將讀線程數(shù)量減一褒颈,寫線程數(shù)量不做改動

protected final boolean tryReleaseShared(int unused) {
    Thread current = Thread.currentThread();
    if (firstReader == current) {
        // assert firstReaderHoldCount > 0;
        if (firstReaderHoldCount == 1)
            firstReader = null;
        else
            firstReaderHoldCount--;
    } else {
        HoldCounter rh = cachedHoldCounter;
        if (rh == null || rh.tid != getThreadId(current))
            rh = readHolds.get();
        int count = rh.count;
        if (count <= 1) {
            readHolds.remove();
            if (count <= 0)
                throw unmatchedUnlockException();
        }
        --rh.count;
    }
    for (;;) {
        int c = getState();
        //SHARED_UNIT 等于 1 << 16
        //c - SHARED_UNIT 就想當(dāng)于 c 的高16位減1甥温,低16位保持不變
        //從而使得讀線程數(shù)量減1,寫線程數(shù)量不變
        int nextc = c - SHARED_UNIT;
        //通過 CAS 來更新 state
        if (compareAndSetState(c, nextc))
            // Releasing the read lock has no effect on readers,
            // but it may allow waiting writers to proceed if
            // both read and write locks are now free.
            return nextc == 0;
    }
}

2翩隧、互斥流程

再來看下線程在獲取寫鎖時的流程樊展。主要是要考慮寫鎖的可重入性以及讀寫鎖的公平性與否

protected final boolean tryAcquire(int acquires) {
    /*
     * Walkthrough:
     * 1. If read count nonzero or write count nonzero
     *    and owner is a different thread, fail.
     * 2. If count would saturate, fail. (This can only
     *    happen if count is already nonzero.)
     * 3. Otherwise, this thread is eligible for lock if
     *    it is either a reentrant acquire or
     *    queue policy allows it. If so, update state
     *    and set owner.
     */
    Thread current = Thread.currentThread();
    int c = getState();
    int w = exclusiveCount(c); //獲取當(dāng)前寫線程數(shù)量
    if (c != 0) {
        // (Note: if c != 0 and w == 0 then shared count != 0)
        //1. 如果 c != 0 && w == 0 成立,說明當(dāng)前存在讀線程堆生,返回 false滚局,寫鎖獲取失敗
        //2. 如果 c != 0 && w != 0 && current != getExclusiveOwnerThread() 成立
        //說明當(dāng)前寫鎖已經(jīng)被持有了,且持有寫鎖的線程并非當(dāng)前線程顽频,返回 false藤肢,寫鎖獲取失敗
        if (w == 0 || current != getExclusiveOwnerThread())
            return false;
        if (w + exclusiveCount(acquires) > MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        // Reentrant acquire
        //能走到這一步,說明當(dāng)前線程已經(jīng)持有了寫鎖糯景,由于寫鎖是可重入的
        //所以這里這里更新下寫鎖被持有的次數(shù)后就返回了 true
        setState(c + acquires);
        return true;
    }
    //能走到這一步嘁圈,說明當(dāng)前寫鎖還未被持有,則根據(jù)讀寫鎖的公平性與否來完成寫鎖的申請
    if (writerShouldBlock() ||
        !compareAndSetState(c, c + acquires))
        return false;
    setExclusiveOwnerThread(current);
    return true;
}

線程在釋放寫鎖時蟀淮,由于 state 值的高 16 位肯定全是 0 (即讀線程數(shù)量為 0)最住,而低 16 位肯定不全是 0,所以主要就是來更新當(dāng)前寫鎖被持有的次數(shù)

protected final boolean tryRelease(int releases) {
    if (!isHeldExclusively())
         //如果當(dāng)前線程并非寫鎖的持有線程怠惶,則拋出異常
        throw new IllegalMonitorStateException();
    int nextc = getState() - releases;
    //由于寫鎖是可重入的涨缚,所以這里也要判斷線程是否已經(jīng)撤銷了所有的申請操作
    boolean free = exclusiveCount(nextc) == 0;
    if (free)
        //只有在寫鎖已經(jīng)撤銷了所有的申請操作后才會真正釋放鎖
        setExclusiveOwnerThread(null);
    setState(nextc);
    return free;
}

五、自旋鎖策治、適應(yīng)性自旋鎖

在一個排他鎖已經(jīng)被持有且鎖的持有線程只會占用鎖一小段時間的情況下脓魏,如果此時將來申請同個鎖實例的線程均進(jìn)行暫停運行處理的話兰吟,鎖在暫停和后續(xù)喚醒過程中所需的時間耗時甚至可能會長于鎖被占用的時間,此時暫停線程就顯得很不值得了茂翔。而自旋鎖的實現(xiàn)出發(fā)點就基于這么一種觀測結(jié)果:在很多時候鎖的持有線程只需要占用鎖一小段時間

自旋鎖的實現(xiàn)前提是當(dāng)前物理機器包含一個以上的處理器混蔼,即支持同時運行一個以上的線程,此時就可以讓后面來申請鎖的線程不放棄處理器時間片而是稍微“等一等”珊燎,看看鎖是否很快就會被釋放惭嚣。讓線程“等一等”,就是通過讓線程反復(fù)執(zhí)行忙循環(huán)(也稱自旋悔政,可以理解為執(zhí)行空操作晚吞,實現(xiàn)原理也是 CAS)來實現(xiàn)的。如果鎖被占用的時間很短谋国,此時采用自旋鎖的效果就會非常好槽地,不會導(dǎo)致上下文切換。而如果鎖被占用的時間比較長烹卒,自旋鎖就會浪費很多處理器時間闷盔,因此也必須為自旋操作限定一個最大次數(shù),當(dāng)達(dá)到限定的最大次數(shù)后如果仍然沒有獲得鎖的話就還是需要將線程進(jìn)行暫停運行處理

因此旅急,自旋鎖適用于絕大多數(shù)線程對鎖的持有時間比較短的情況逢勾,這樣能夠避免上下文切換的資源開銷和過多的處理器時間開銷。而對于系統(tǒng)中絕大多數(shù)線程對鎖的持有時間比較長的情況藐吮,就還是采用直接暫停線程的策略比較適合

在自旋鎖出現(xiàn)的一開始溺拱,只能對 JVM 中的所有鎖設(shè)定一個固定的最大自旋次數(shù)。而在后續(xù)也引入了適應(yīng)性自旋鎖谣辞。適應(yīng)性意味著自旋的時間(次數(shù))不再是固定的迫摔,而是由前一次在該鎖上的自旋時間及其當(dāng)前持有線程的狀態(tài)來決定。對于某個鎖泥从,如果其當(dāng)前正在被某個已經(jīng)通過自旋成功獲得鎖的線程持有的話句占,那么 JVM 就會認(rèn)為其它來申請同個鎖的線程再次使用自旋也很能再次成功,進(jìn)而將允許自旋等待相對更長的時間躯嫉。如果對于某個鎖自旋很少成功獲得過纱烘,那么在以后嘗試獲取這個鎖時將可能省略掉自旋過程,而是直接阻塞線程祈餐,避免浪費處理器資源

總的來說擂啥,通過采用自旋鎖,鎖的申請就并不一定會導(dǎo)致上下文切換了帆阳,自旋鎖的自適應(yīng)性也進(jìn)一步降低了發(fā)生線程上下文切換的概率

六哺壶、偏向鎖、輕量級鎖、重量級鎖

偏向鎖山宾、輕量級鎖至扰、重量級鎖可以看做是三種狀態(tài)值或者說是操作手段,用于描述 synchronized 所對應(yīng)的內(nèi)部鎖所處的狀態(tài)塌碌,在不同的狀態(tài)下獲取內(nèi)部鎖的實現(xiàn)步驟也各不相同渊胸,在理解這三種狀態(tài)前需要先了解下什么是對象頭

1旬盯、對象頭

在 HotSpot 虛擬機里台妆,對象在堆內(nèi)存中的存儲布局可以劃分為三個部分:對象頭(Header)、實例數(shù)據(jù)(Instance Data)和對齊填充(Padding) 胖翰。當(dāng)中接剩,對象頭包含著對象自身的運行時數(shù)據(jù),如 HashCode萨咳、GC 分代年齡懊缺、鎖狀態(tài)標(biāo)志、線程持有的鎖培他、偏向線程ID鹃两、偏向時間戳等,這部分?jǐn)?shù)據(jù)的長度在 32 位和 64 位的虛擬機(未開啟壓縮指針)中分別為 32 個比特和 64 個比特舀凛,官方稱它為“Mark Word”俊扳,它是實現(xiàn)偏向鎖和輕量級鎖的關(guān)鍵。Mark Word 被設(shè)計成一個有著動態(tài)定義的數(shù)據(jù)結(jié)構(gòu)猛遍,在運行期間 Mark Word 里存儲的數(shù)據(jù)會隨著鎖標(biāo)志位的變化而變化馋记,即在不同的狀態(tài)下會分別存儲具有不同含義的數(shù)據(jù)

例如,在 32 位的 HotSpot 虛擬機中懊烤,如果對象處于未被鎖定狀態(tài)梯醒,Mark Word 的 32 個比特存儲空間中的 25 個比特會用于存儲對象哈希碼,4 個比特用于存儲對象分代年齡腌紧,1個比特固定為 0茸习,2 個比特用于存儲鎖標(biāo)志位

我們在使用 synchronized 時會顯式或隱式地指定關(guān)聯(lián)的同步對象(實例變量或者是 Class 對象),而 Java 平臺中的任何一個對象都有一個唯一與之關(guān)聯(lián)的鎖壁肋,被稱為監(jiān)視器(Monitor)或者內(nèi)部鎖号胚。同步對象的對象頭所包含的運行時數(shù)據(jù)的變化過程,就是其內(nèi)部鎖在偏向鎖墩划、輕量級鎖涕刚、重量級鎖這四種狀態(tài)下的切換過程

2、偏向鎖

JVM 在實現(xiàn) monitorenter(申請鎖) 和 monitorexit(釋放鎖) 這兩個字節(jié)碼指令時需要借助一個原子操作(CAS 操作)乙帮,這個操作代價相對來說比較昂貴杜漠。而如果在一段時間內(nèi)一個鎖實例先后只會由同一個線程來申請并使用的話,那么該線程每次申請和釋放鎖的代價就會被放大,顯得很不值得了驾茴。而偏向鎖的實現(xiàn)出發(fā)點就基于這么一種觀測結(jié)果:大多數(shù)鎖并沒有被爭用盼樟,并且在其整個生命周期內(nèi)總是同一個線程來進(jìn)行申請

偏向鎖的執(zhí)行流程是這樣的:

  1. 當(dāng)某個鎖第一次被線程申請到時,JVM 會把同步對象的 Mark Word 中的標(biāo)志位設(shè)置為“01”锈至,偏向模式設(shè)置為“1”晨缴,表示該鎖進(jìn)入了偏向模式。同時使用 CAS 操作把當(dāng)前線程的 ID 記錄在對象的 Mark Word 之中峡捡,如果 CAS 操作成功击碗,該線程就會被記錄為同步對象的偏好線程(Biased Thread),然后執(zhí)行步驟 4们拙。如果 CAS 操作失敗稍途,則直接步驟 3
  2. 當(dāng)又有線程前來申請鎖時,如果判斷到偏向鎖指向的 Thread ID 即為當(dāng)前線程砚婆,則直接執(zhí)行步驟 4械拍,即偏向線程以后每次進(jìn)入這個鎖相關(guān)的同步塊時,都不用再進(jìn)行任何加鎖操作装盯。如果偏向鎖指向的 Thread ID 并非當(dāng)前線程坷虑,說明當(dāng)前系統(tǒng)存在多個線程競爭偏向鎖,則通過 CAS 來競爭鎖埂奈。如果競爭成功迄损,則將Mark Word 中的線程 ID 設(shè)置為當(dāng)前線程 ID,然后執(zhí)行步驟 4挥转;如果競爭失敗海蔽,則執(zhí)行步驟 3
  3. 因為線程不會主動去釋放偏向鎖,所以如果 CAS 獲取偏向鎖失敗绑谣,則表示當(dāng)前存在多個線程一個競爭偏向鎖党窜。當(dāng)?shù)竭_(dá)全局安全點(safepoint)時,會首先暫停擁有偏向鎖的線程借宵,然后檢查持有偏向鎖的線程是否活著(因為持有偏向鎖的線程可能已經(jīng)執(zhí)行完畢幌衣,但該線程并不會主動去釋放偏向鎖),如果線程不處于活動狀態(tài)壤玫,則將對象頭設(shè)置成無鎖狀態(tài)(標(biāo)志位為“01”)豁护,然后重新偏向新的線程;如果線程仍然活躍欲间,則撤銷偏向鎖楚里,將其升級到輕量級鎖狀態(tài)(標(biāo)志位變?yōu)椤?0”),此時輕量級鎖由原持有偏向鎖的線程繼續(xù)持有猎贴,讓其繼續(xù)執(zhí)行同步代碼班缎,而正在申請鎖的線程則通過自旋等待獲得該輕量級鎖
  4. 執(zhí)行同步代碼

引入偏向鎖是為了提高帶有 synchronized 同步操作但實際上無爭用的代碼塊的性能蝴光,因為偏向線程在獲取到偏向鎖之后,每次進(jìn)入這個鎖相關(guān)的同步塊時达址,都不用再進(jìn)行任何同步操作(例如加鎖蔑祟、解鎖及對 Mark Word 的更新操作等)。而且輕量級鎖的獲取及釋放依賴多次 CAS 原子指令沉唠,而偏向鎖只需要在置換 Thread ID 的時候執(zhí)行一次 CAS 原子指令即可

偏向鎖適用于存在相當(dāng)大一部分鎖并沒有被爭用的系統(tǒng)疆虚,如果系統(tǒng)中存在大量被爭用的鎖而沒有被爭用的鎖僅占小部分,那么就可以考慮關(guān)閉偏向鎖

3满葛、輕量級鎖

輕量級鎖是 JDK 6 時加入的新型鎖機制径簿。當(dāng)某個鎖是偏向鎖時,如果該鎖被其它線程訪問了纱扭,此時偏向鎖就會升級為輕量級鎖牍帚,其它線程會通過自旋的方式來嘗試獲取鎖儡遮,此時該線程不會阻塞乳蛾,從而提高了性能

在線程進(jìn)入同步塊的時候,如果同步對象鎖沒有被鎖定(鎖標(biāo)志位為“01”)鄙币,JVM 首先會在當(dāng)前線程的棧幀中建立一個名為鎖記錄(Lock Record)的空間肃叶,然后拷貝對象頭中的 Mark Word 到鎖記錄中∈伲拷貝完成后因惭,JVM 將使用 CAS 操作嘗試將對象的 Mark Word 值更新為指向 Lock Record 的指針,并將 Lock Record 里的 owner 指針指向?qū)ο蟮?Mark Word绩衷。如果這個更新動作成功了蹦魔,即代表這個線程擁有了該對象鎖,并且 Mark Word 的鎖標(biāo)志位將變更為為 “00”咳燕,表示此對象處于輕量級鎖定狀態(tài)勿决。如果這個更新操作失敗了,那就意味著至少存在一個線程與當(dāng)前線程競爭獲取該對象鎖招盲。JVM 會先檢查對象的 Mark Word 是否指向當(dāng)前線程的棧幀低缩,如果是,說明當(dāng)前線程已經(jīng)擁有了這個對象的鎖曹货,那直接進(jìn)入同步塊繼續(xù)執(zhí)行就可以了咆繁,否則就說明這個鎖對象已經(jīng)被其他線程搶占了。如果出現(xiàn)兩個以上的線程爭用同一個鎖的情況顶籽,那輕量級鎖就不再有效玩般,必須要升級為重量級鎖,鎖標(biāo)志的狀態(tài)值變?yōu)椤?0”礼饱,此時 Mark Word 中存儲的就是指向重量級鎖(互斥量)的指針坏为,此后等待鎖的線程也必須進(jìn)入阻塞狀態(tài)

輕量級鎖的實現(xiàn)出發(fā)點是基于這么一種觀測結(jié)果:大多數(shù)鎖在整個同步周期內(nèi)都是不存在競爭的设拟。這里需要和偏向鎖區(qū)分開,偏向鎖的理論基礎(chǔ)是:大多數(shù)鎖總是在其整個生命周期內(nèi)被同一個線程所使用久脯。而輕量級鎖的理論基礎(chǔ)是:鎖可能會先后被多個線程使用纳胧,但由于線程間的交叉使用,所以大多數(shù)線程在使用同步資源時是不存在競爭的帘撰。偏向鎖相對輕量級鎖會更加“樂觀”跑慕,所以輕量級鎖就需要比偏向鎖多出更多的“安全保障措施”

如果沒有競爭,輕量級鎖便通過 CAS 操作成功避免了使用互斥量的開銷摧找;但如果確實存在鎖競爭核行,除了互斥量的本身開銷外,還額外產(chǎn)生了 CAS 操作的開銷蹬耘。因此在有競爭的情況下輕量級鎖會比傳統(tǒng)的重量級鎖更加消耗資源

4芝雪、重量級鎖

升級為重量級鎖時,Mark Word 中前 30 位存儲的是指向重量級鎖(互斥量)的指針综苔,鎖標(biāo)志的狀態(tài)值變?yōu)椤?0”惩系,此后等待鎖的線程就必須進(jìn)入阻塞狀態(tài)。重量級鎖是實現(xiàn)鎖申請操作最為消耗資源的一種做法

5如筛、概述

綜上堡牡,偏向鎖通過對比 Mark Word 解決了加鎖問題,避免執(zhí)行 CAS 操作杨刨。而輕量級鎖是通過用 CAS 操作和自旋來解決加鎖問題晤柄,避免線程阻塞和喚醒而影響性能。重量級鎖則是直接將除了擁有鎖的線程以外的線程都阻塞

七妖胀、鎖消除

鎖消除是 JIT 編譯器對內(nèi)部鎖的具體實現(xiàn)所做的一種優(yōu)化芥颈。在動態(tài)編譯同步塊的時候,編譯器會借助逃逸分析技術(shù)來判斷同步塊所使用的鎖對象是否只會被一個線程使用赚抡。如果判斷出該鎖對象的確只能被一個線程訪問爬坑,編譯器在編譯這個同步塊的時候就不會生成 synchronize 所表示的 monitorenter 和 monitorexit 兩個機器碼,而僅生成臨界區(qū)內(nèi)的代碼所對應(yīng)的機器碼怕品,從而消除了鎖的使用妇垢。這種優(yōu)化措施就稱為鎖消除,鎖消除使得在特定情況下可以完全消除鎖的開銷

例如肉康,對于以下方法闯估。StringBuffer 類本身是線程安全的,其內(nèi)部多個方法(例如吼和,append 方法)都使用到了內(nèi)部鎖涨薪,而在toJson()方法里 StringBuffer 是作為一個局部變量存在的,并不會存在多個線程同時訪問的情況炫乓,此時 append 方法所使用到的內(nèi)部鎖就成了一種無謂的消耗刚夺。所以献丑,編譯器在編譯 toJson 方法的時候就會將其調(diào)用的 StringBuffer.append 方法內(nèi)聯(lián)到該方法之中,相當(dāng)于把 StringBuffer.append 方法的方法體中的指令復(fù)制到 toJson 方法體中侠姑,此時就可以避免 append 方法所聲明的內(nèi)部鎖所帶來的消耗

public String toJson() {
    StringBuffer stringBuffer = new StringBuffer();
    stringBuffer.append("xxx");
    return stringBuffer.toString();
}

鎖消除不一定會被編譯器實施创橄,這和同步代碼塊是否能被內(nèi)聯(lián)有關(guān),所以雖然鎖消除技術(shù)可以使得編譯器為我們消除一部分的鎖開銷莽红,但是這也不意味著開發(fā)者就可以隨意使用內(nèi)部鎖了

八妥畏、鎖粗化

鎖粗化是指 JIT 編譯器會將相鄰的幾個同步代碼塊合并為一個大的同步代碼塊的一種優(yōu)化措施。通過鎖粗化安吁,可以避免一個線程反復(fù)申請和釋放同一個鎖所導(dǎo)致的開銷醉蚁,相應(yīng)的也會導(dǎo)致一個線程持有一個鎖的時間變長,從而使得鎖的等待線程申請鎖所需要的時間也相應(yīng)變長

例如鬼店,對于以下代碼网棍。通過鎖粗化技術(shù)就可以將多個同步代碼塊合并為一個,且同步代碼塊之間的代碼也會被合并在一起妇智,使得臨界區(qū)的長度變長滥玷。而原本在鎖 lockX 的持有線程執(zhí)行完第一個同步代碼塊之后(即釋放 lockX 后),其它等待線程是有機會獲得 lockX 的俘陷,但是經(jīng)過鎖粗化后使得 lockX 的持有線程只有執(zhí)行完全部同步代碼塊之后才會釋放 lockX罗捎,使得等待線程申請 lockX 的時間相應(yīng)變長了。因此拉盾,為了避免一個線程持有鎖的時間過長,鎖粗化不會被應(yīng)用到循環(huán)體內(nèi)的相鄰?fù)酱a塊

//鎖粗化前
public void test() {
    synchronized (lockX) {
        doSomethind1();
    }
    x = 10;
    synchronized (lockX) {
        doSomethind2();
    }
    y = 10;
    synchronized (lockX) {
        doSomethind3();
    }
}

//鎖粗化后
public void test() {
    synchronized (lockX) {
        doSomethind1();
        x = 10;
        doSomethind2();
        y = 10;
        doSomethind3();
    }
}

九豁状、優(yōu)化對鎖的使用

以上所講的大部分優(yōu)化措施都是在編譯器這個層次實施的捉偏,這一節(jié)再來介紹下如何在代碼層次對鎖進(jìn)行優(yōu)化

1、降低鎖的爭用程度

在之前的文章中有介紹過使用鎖帶來的主要開銷泻红,而如果必須使用鎖且鎖帶來的開銷很難避免夭禽,那么要降低鎖的開銷的思路之一就是降低鎖的爭用程度。鎖的爭用程度和程序中需要同時使用到該鎖實例的線程數(shù)量有關(guān)谊路,如果可以盡量降低每個線程來申請鎖時該鎖實例還被其它線程持有的情況讹躯,那么就可以降低鎖的爭用程度。降低鎖的爭用程度可以用兩種方式來實現(xiàn):減少鎖被持有的時間降低鎖的申請頻率

  • 減少鎖被持有的時間即讓每個線程持有鎖的時間盡量短缠劝,從而減少當(dāng)某個線程申請鎖時而鎖的持有線程還未執(zhí)行完臨界區(qū)代碼的情況潮梯,而且也有利于 Java 虛擬機的適應(yīng)性鎖發(fā)揮作用〔夜В可以通過減少臨界區(qū)長度來縮減鎖被持有的時間秉馏,例如:將不會導(dǎo)致競態(tài)的代碼(局部變量的訪問等)放到臨界區(qū)之外執(zhí)行,使得每個線程在持有鎖的過程中需要執(zhí)行的指令盡量少脱羡。此外萝究,也需要避免在臨界區(qū)中執(zhí)行阻塞式 IO 等阻塞操作免都,阻塞操作會導(dǎo)致線程被暫停和上下文切換,而在線程被暫停的過程中其持有的鎖也不會被釋放帆竹,這樣會增大鎖被爭用的可能性

  • 降低鎖的申請頻率可以通過減小鎖的粒度來實現(xiàn)绕娘。例如,多個線程間存在多個共享變量栽连,而共享變量之間并沒有特定的關(guān)聯(lián)關(guān)系业舍,此時就可以分別使用不同的鎖對象來保障不同的共享變量在多個線程間的線程安全性。假設(shè)多個線程間存在兩個共享變量 A 和 B升酣,如果變量 A 和變量 B 之間并沒有關(guān)聯(lián)關(guān)系舷暮,那么在訪問共享變量的時候就可以使用不同的鎖,線程 A 在訪問變量 A 的時候可以使用 Lock A 來保障安全性噩茄,而在線程 A 持有 Lock A 的過程中也不妨礙線程 B 申請 Lock B 對變量 B 進(jìn)行訪問下面。通過這種使用不同的鎖來保障不同共享數(shù)據(jù)的安全性,從而減少鎖的爭用程度绩聘。但如果鎖的粒度過細(xì)也會增加鎖調(diào)度的開銷沥割,需要在實際開發(fā)中衡量使用

2、使用可參數(shù)化鎖

如果一個方法或者類的內(nèi)部所使用的鎖實例可以由其使用者來指定的話凿菩,那么就可以說這個鎖是可參數(shù)化的机杜,相應(yīng)的這個鎖就被稱為可參數(shù)化的鎖。使用可參數(shù)化鎖有助于減少線程需要申請的鎖實例的個數(shù)衅谷,從而減少鎖的開銷

例如椒拗,對于以下例子。假設(shè) Printer 類是由第三方提供的工具類获黔,其內(nèi)部需要保障自身的線程安全性蚀苛,所以使用到了內(nèi)部鎖,其鎖實例默認(rèn)是其本身變量實例(即 this) 玷氏。LogPrinter 類作為客戶端/使用者堵未,其內(nèi)部也需要保障自身的線程安全性(例如:line++; ),所以也使用到了內(nèi)部鎖盏触。但由于 Printer 的所有方法均由 LogPrinter 已經(jīng)保障了線程安全性的方法進(jìn)行調(diào)用渗蟹,此時 Printer 內(nèi)部使用到的內(nèi)部鎖就成了多余配置,增加了無謂的鎖開銷

由于 Java 平臺中的鎖都是可重入的赞辩,且鎖的持有線程在未釋放鎖的情況下重復(fù)申請該鎖的開銷時所需要的開銷比較小雌芽,所以此時就可以依靠 Printer 類提供的可參數(shù)化鎖配置,將 LogPrinter 聲明的鎖實例 lock 作為構(gòu)造參數(shù)傳給 Printer诗宣,從而減少了鎖開銷

class Printer {

    private final Object lock;

    public Printer(Object lock) {
        this.lock = lock;
    }

    public Printer() {
        this.lock = this;
    }

    public void print(String msg) {
        synchronized (lock) {
            System.out.println(msg);
        }
    }

}

class LogPrinter {

    private final Object lock = new Object();

    private final Printer printer = new Printer(lock);

    private int line;

    public void print(String msg) {
        synchronized (lock) {
            line++;
            printer.print(msg);
        }
    }

}

十膘怕、參考資料

  1. 不可不說的Java“鎖”事
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市召庞,隨后出現(xiàn)的幾起案子岛心,更是在濱河造成了極大的恐慌来破,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件忘古,死亡現(xiàn)場離奇詭異徘禁,居然都是意外死亡,警方通過查閱死者的電腦和手機髓堪,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進(jìn)店門送朱,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人干旁,你說我怎么就攤上這事驶沼。” “怎么了争群?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵回怜,是天一觀的道長。 經(jīng)常有香客問我换薄,道長玉雾,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任轻要,我火速辦了婚禮复旬,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘冲泥。我一直安慰自己驹碍,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布柏蘑。 她就那樣靜靜地躺著幸冻,像睡著了一般。 火紅的嫁衣襯著肌膚如雪咳焚。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天庞溜,我揣著相機與錄音革半,去河邊找鬼。 笑死流码,一個胖子當(dāng)著我的面吹牛又官,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播漫试,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼六敬,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了驾荣?” 一聲冷哼從身側(cè)響起外构,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤普泡,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后审编,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體撼班,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年垒酬,在試婚紗的時候發(fā)現(xiàn)自己被綠了砰嘁。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡勘究,死狀恐怖矮湘,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情口糕,我是刑警寧澤缅阳,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布,位于F島的核電站走净,受9級特大地震影響券时,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜伏伯,卻給世界環(huán)境...
    茶點故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一橘洞、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧说搅,春花似錦炸枣、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至候引,卻和暖如春侯养,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背澄干。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工逛揩, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人麸俘。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓辩稽,卻偏偏與公主長得像,于是被迫代替她去往敵國和親从媚。 傳聞我的和親對象是個殘疾皇子逞泄,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,722評論 2 345

推薦閱讀更多精彩內(nèi)容