【Java多線程-7】閱盡Java千般鎖

1 鎖分類概述

1.1 樂觀鎖 & 悲觀鎖

根據對同步資源處理策略不同,鎖在宏觀上分爲樂觀鎖與悲觀鎖,這只是概念上的一種稱呼,Java中並沒有具體的實現類叫做樂觀鎖或者悲觀鎖。
在這裏插入圖片描述

  • 樂觀鎖:所謂樂觀鎖(Optimistic Lock),總是假設最好的情況,每次去拿數據的時候都認爲別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間這個數據是否被其他線程更新過,根據對比結果做出以下處理:
    1. 如果這個數據沒有被更新,當前線程將自己修改的數據成功寫入。
    2. 如果數據已經被其他線程更新,則根據不同的實現方式執行不同的操作,例如報錯或者重試。
  • 悲觀鎖:與樂觀鎖相反,悲觀鎖(Pessimistic Lock)總是假設最壞的情況,每次去拿數據的時候都認爲別人會修改,所以每次在拿數據的時候都會上鎖,這樣別人想拿這個數據就會阻塞直到它拿到鎖。

悲觀鎖阻塞事務,樂觀鎖回滾重試,它們各有優缺點,適應場景的不同區別,比如:

  • 實現方式不同
    1. 樂觀鎖:在Java中是通過使用無鎖編程來實現,最常採用的是CAS算法,Java原子類中的遞增操作就通過CAS自旋實現的。
    2. 悲觀鎖:依賴Java的synchronized和ReentrantLock等鎖去實現。
  • 適用場景不同
    1. 樂觀鎖:適合用於寫操作比較少的場景,因爲衝突很少發生,這樣可以省去鎖的開銷,加大了系統的整個吞吐量。
    2. 悲觀鎖:適用於寫操作比較多的場景。如果經常產生衝突,上層
      應用會不斷的進行重試,樂觀鎖反而會降低性能,悲觀鎖則更加合適。

前面提到樂觀鎖得實現是依賴於CAS,那麼何爲CAS呢?
CAS,即Compare And Swap(比較與交換),是一種無鎖算法。在不使用鎖(沒有線程被阻塞)的情況下實現多線程之間的變量同步。java.util.concurrent包中的原子類就是通過CAS來實現了樂觀鎖。
CAS通過以下機制實現變量同步:

  1. 比較:讀取到一個值 A,在將其更新爲 B 之前,檢查原值是否爲 A(未被其它線程修改過,這裏忽略 ABA 問題)。
  2. 替換:如果是,更新 A 爲 B,結束。如果不是,則不會更新。

上面兩個步驟都是原子操作,可以理解爲瞬間完成,在 CPU 看來就是一步操作。在 Java 中也是通過 native 方法實現的 CAS。

public final class Unsafe {
    
    ...
    
    public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
    
    public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
    
    public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);  
    
    ...
} 

CAS雖然很高效,但是它也存在三大問題:

  1. ABA問題。CAS需要在操作值的時候檢查內存值是否發生變化,沒有發生變化纔會更新內存值。但是如果內存值原來是A,後來變成了B,然後又變成了A,那麼CAS進行檢查時會發現值沒有發生變化,但是實際上是有變化的。ABA問題的解決思路就是在變量前面添加版本號,每次變量更新的時候都把版本號加一,這樣變化過程就從“A-B-A”變成了“1A-2B-3A”。
    JDK從1.5開始提供了AtomicStampedReference類來解決ABA問題,具體操作封裝在compareAndSet()中。compareAndSet()首先檢查當前引用和當前標誌與預期引用和預期標誌是否相等,如果都相等,則以原子方式將引用值和標誌的值設置爲給定的更新值。

  2. 循環時間長開銷大。CAS操作如果長時間不成功,會導致其一直自旋,給CPU帶來非常大的開銷。

  3. 只能保證一個共享變量的原子操作。對一個共享變量執行操作時,CAS能夠保證原子操作,但是對多個共享變量操作時,CAS是無法保證操作的原子性的。
    Java從1.5開始JDK提供了AtomicReference類來保證引用對象之間的原子性,可以把多個變量放在一個對象裏來進行CAS操作。

1.2 自旋鎖 & 適應性自旋鎖

線程狀態切換的開銷是非常大的,其原因在於:

  1. 線程狀態切換會使CPU運行狀態從用戶態轉換到內核態。
  2. 每個線程在運行時的指令是被放在CPU的寄存器中的,如果切換內存狀態,需要先把本線程的代碼和變量寫入內存,這樣經常切換會耗費時間。

因此,線程因爲無法獲取到鎖而進行掛起以及後續的恢復操作,這個時間消耗很可能大於同步資源的鎖定時間,這種情況對系統而言是得不償失的。

那麼,能不能讓獲取鎖失敗的線程先不掛起,而是“稍等一下”,如果鎖被釋放,這個線程便可以直接獲取鎖,從而避免線程切換。這個“稍等一下”依賴於自旋實現,所謂自旋,即在一個循環中不停判斷是否可以獲取鎖。獲取鎖的操作,就是通過 CAS 操作修改對象頭裏的鎖標誌位。先比較當前鎖標誌位是否爲釋放狀態,如果是,將其設置爲鎖定狀態,比較並設置是原子性操作,這個是 JVM 層面保證的。當前線程就算持有了鎖,然後線程將當前鎖的持有者信息改爲自己。

這是一種折中的思想,用短時間的忙等來換取線程切換的開銷。
在這裏插入圖片描述
自旋不是盡善盡美的,它有以下的弊端:

  • 假如佔有鎖的線程操作時間很長,那麼等待鎖的線程會長時間處於自旋狀態(稱爲“忙等”),耗費大量cpu資源。
  • Java實現的自旋鎖也是一種非公平鎖,等待時間最長的線程並不能優先獲取鎖,可能會產生“線程飢餓”問題。

因此,自旋應該是有限度的。比如說,自旋10次後,自旋線程放棄等待進入掛起狀態,同時鎖升級爲重量級鎖,其他線程獲取鎖失敗後直接掛起,不再自旋。

JVM團隊後來又推出了自適應自旋,自適應意味着自旋的時間(次數)不再固定,而是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定。

如果在同一個鎖對象上,自旋等待剛剛成功獲得過鎖,並且持有鎖的線程正在運行中,那麼虛擬機就會認爲這次自旋也是很有可能再次成功,進而它將允許自旋等待持續相對更長的時間。如果對於某個鎖,自旋很少成功獲得過,那在以後嘗試獲取這個鎖時將可能省略掉自旋過程,直接阻塞線程,避免浪費處理器資源。

1.3 公平鎖 & 非公平鎖

公平鎖是指,如果多個線程爭奪一把公平鎖,這些線程會進入一個按申請順序排序的隊列(隊列中的線程都處於阻塞狀態,等待喚醒),當鎖釋放的時候,隊列中第一個線程獲取鎖。
與之相對應,非公平鎖機制下,試圖獲取鎖的線程會嘗試直接去獲取鎖,獲取不到再進入等待隊列。

在Java中,synchronized 鎖只能是一種非公平鎖,而 ReentrantLock 鎖則可以通過構造函數指定使用公平鎖還是非公平鎖(默認是非公平鎖)。
下面從源碼來看一下,ReentrantLock 公平鎖和非公平鎖的大概實現機制。

首先,ReentrantLock類無參構造函數指定了使用非公平鎖 NonfairSync,另外又提供了有參構造方法,允許調用者自己指定使用公平鎖 FairSync 還是非公平鎖 NonfairSync(FairSync和NonfairSync是ReentrantLock 內部類 Sync 的兩個子類,而添加鎖和釋放鎖的大部分操作是 Sync 實現的)。

/**
 * Creates an instance of {@code ReentrantLock}.
 * This is equivalent to using {@code ReentrantLock(false)}.
 */
public ReentrantLock() {
    sync = new NonfairSync();
}

/**
 * Creates an instance of {@code ReentrantLock} with the
 * given fairness policy.
 *
 * @param fair {@code true} if this lock should use a fair ordering policy
 */
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

下面是公平鎖和非公平鎖加鎖方法的對比:
圖片來自網絡
通過對比可見,公平鎖相對於非公平鎖多了一個限制條件:hasQueuedPredecessors(),這個方法是判斷當前線程是否位於隊列的首位。

public final boolean hasQueuedPredecessors() {
    // The correctness of this depends on head being initialized
    // before tail and on head.next being accurate if the current
    // thread is first in queue.
    Node t = tail; // Read fields in reverse initialization order
    Node h = head;
    Node s;
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

公平鎖與非公平鎖的優劣勢比較:

  • 非公平鎖通過插隊直接獲取鎖,減少了一次線程阻塞與喚醒過程,系統整體吞吐量提升。
  • 非公平鎖不能保證等待時間最長的線程優先獲取鎖,可能導致線程飢餓。

1.4 可重入鎖 & 不可重入鎖

可重入鎖又名遞歸鎖,是指在同一個線程在外層方法獲取鎖的時候,再進入該線程的內層方法會自動獲取鎖(前提鎖對象得是同一個對象或者class),不會因爲之前已經獲取過還沒釋放而阻塞。

重入鎖與不可重入鎖實現機制的差異

我們先通過源碼對比一下兩種鎖的實現機制:
在這裏插入圖片描述
通過觀察上面的源碼,我們得知:

首先,ReentrantLock和NonReentrantLock都繼承AQS,AQS中維護了一個同步狀態 state 來計數,state 初始值爲0,隨着佔有鎖的線程的子流程佔據和釋放鎖,state進行相應增減操作。getState() 方法能獲取最新的 state 值。

當線程獲取鎖時:

  1. 可重入鎖先查詢當前 state 值,如果status 是 0,表示沒有其他線程在佔有鎖,則該線程獲取鎖並將 state 執行+1操作。如果status != 0,則判斷當前線程是否是獲取到這個鎖的線程,如果是的話執行state+1操作,如此循環,當前線程便可以重複獲得鎖。
  2. 非可重入鎖是直接去判斷當前 state 的值是否是 0 ,如果是則將其置爲1,並返回 true,從而獲取鎖,如果不是 0 ,則返回 false,獲取鎖失敗,當前線程阻塞。

當線程釋放鎖時:

  1. 可重入鎖先獲取當前 state 的值,在當前線程是持有鎖的線程的前提下。如果status-1 == 0,則表示當前線程所有重複獲取鎖的操作都已經執行完畢,然後該線程纔會真正釋放鎖。
  2. 非可重入鎖則是在確定當前線程是持有鎖的線程之後,直接將status置爲0,將鎖釋放。

非重入鎖容易導致死鎖問題:

Java中的ReentrantLock和synchronized都是可重入鎖,可重入鎖的一個優點是可一定程度避免死鎖。爲什麼非重入鎖容易導致死鎖問題呢?先看下面這段代碼:

public class Widget {
	synchronized void methodA() throws Exception{
		Thread.sleep(1000);
		methodB();
	}

	synchronized void methodB() throws Exception{
		Thread.sleep(1000);
	}
}

在上面的代碼中,類中的兩個方法都是被內置鎖synchronized修飾,methodA()方法調用methodB()方法。因爲內置鎖是可重入的,所以同一個線程在調用 methodB() 時可以直接獲得當前對象的鎖,進入methodB()進行操作。

如果是一個不可重入鎖,那麼當前線程在調用 methodB() 之前需要將執行 methodA() 時獲取當前對象的鎖釋放掉,實際上該對象鎖已被當前線程所持有,且無法釋放,此時會出現死鎖。

下面用一個網上的生動例子來描述一下可重入鎖和非重入鎖的異同。

有多個人在排隊打水,此時管理員允許鎖和同一個人的多個水桶綁定。這個人用多個水桶打水時,第一個水桶和鎖綁定並打完水之後,第二個水桶也可以直接和鎖綁定並開始打水,所有的水桶都打完水之後打水人才會將鎖還給管理員。這個人的所有打水流程都能夠成功執行,後續等待的人也能夠打到水。這就是可重入鎖。
在這裏插入圖片描述
但如果是非可重入鎖的話,此時管理員只允許鎖和同一個人的一個水桶綁定。第一個水桶和鎖綁定打完水之後並不會釋放鎖,導致第二個水桶不能和鎖綁定也無法打水。當前線程出現死鎖,整個等待隊列中的所有線程都無法被喚醒。
在這裏插入圖片描述
Java 中以 Reentrant 開頭命名的鎖都是可重入鎖,而且 JDK 提供的所有現成 Lock 的實現類,包括 synchronized 關鍵字鎖都是可重入的。不可重入鎖需要自己去實現。不可重入鎖的使用場景非常非常少。

1.5 共享鎖 & 獨享鎖 & 讀寫鎖

共享鎖是指該鎖可被多個線程所持有。獨享鎖,也叫排他鎖,是指該鎖一次只能被一個線程所持有。共享鎖與獨享鎖互斥,獨享鎖與獨享鎖互斥。

  • 對同步資源施加共享鎖後,其他線程只能對此資源再添加共享鎖而不能再添加獨享鎖。
  • 對同步資源施加獨享鎖後,其他線程不能對此資源添加任何鎖。
  • 獲得共享鎖的線程只能讀數據,不能修改數據。

Java中,Synchronized和ReentrantLock就是一種排它鎖,CountDownLatch是一種共享鎖,它們都是純粹的共享鎖或獨享鎖,不能轉換形態。
而 ReentrantReadWriteLock(讀寫鎖)就是一種特殊的鎖,它既是互斥鎖,又是共享鎖,read模式是共享,write是互斥(排它鎖)的。

寫鎖源碼:

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);
    if (c != 0) {
        // (Note: if c != 0 and w == 0 then shared count != 0)
        // 如果寫線程數(w)爲0(換言之存在讀鎖) 或者持有鎖的線程不是當前線程就返回失敗
        if (w == 0 || current != getExclusiveOwnerThread())
            return false;
        // 如果寫入鎖的數量大於最大數(65535,2的16次方-1)就拋出一個Error。
        if (w + exclusiveCount(acquires) > MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        // Reentrant acquire
        setState(c + acquires);
            return true;
    }
    // 如果當且寫線程數爲0,並且當前線程需要阻塞那麼就返回失敗;或者如果通過CAS增加寫線程數失敗也返回失敗。
    if (writerShouldBlock() ||!compareAndSetState(c, c + acquires))
        return false;
    // 如果c=0,w=0或者c>0,w>0(重入),則設置當前線程或鎖的擁有者
    setExclusiveOwnerThread(current);
    return true;
}
  • 這段代碼首先取到當前鎖的個數c,然後再通過c來獲取寫鎖的個數w。因爲寫鎖是低16位,所以取低16位的最大值與當前的c做與運算( int w = exclusiveCount©; ),高16位和0與運算後是0,剩下的就是低位運算的值,同時也是持有寫鎖的線程數目。
  • 在取到寫鎖線程的數目後,首先判斷是否已經有線程持有了鎖。如果已經有線程持有了鎖(c!=0),則查看當前寫鎖線程的數目,如果寫線程數爲0(即此時存在讀鎖)或者持有鎖的線程不是當前線程就返回失敗(涉及到公平鎖和非公平鎖的實現)。
  • 如果寫入鎖的數量大於最大數(65535,2的16次方-1)就拋出一個Error。
  • 如果當且寫線程數爲0(那麼讀線程也應該爲0,因爲上面已經處理c!=0的情況),並且當前線程需要阻塞那麼就返回失敗;如果通過CAS增加寫線程數失敗也返回失敗。
  • 如果c=0,w=0或者c>0,w>0(重入),則設置當前線程或鎖的擁有者,返回成功!
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();
        int nextc = c - SHARED_UNIT;
        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;
    }
}

可以看到在tryAcquireShared(int unused)方法中,如果其他線程已經獲取了寫鎖,則當前線程獲取讀鎖失敗,進入等待狀態。如果當前線程獲取了寫鎖或者寫鎖未被獲取,則當前線程(線程安全,依靠CAS保證)增加讀狀態,成功獲取讀鎖。讀鎖的每次釋放(線程安全的,可能有多個讀線程同時釋放讀鎖)均減少讀狀態,減少的值是“1<<16”。所以讀寫鎖才能實現讀讀的過程共享,而讀寫、寫讀、寫寫的過程互斥。

2 鎖升級

鎖升級只針對synchronized 鎖,synchronized 剛出現時性能較差,Java 6對 synchronized 進行了一系列優化,其性能也有大幅提升。
Java 6的優化主要在於引入 “偏向鎖”和“輕量級鎖”的概念,減少了獲得鎖和釋放鎖的消耗。

目前,鎖一共有4種狀態,分別是:無鎖狀態、偏向鎖狀態、輕量級鎖狀態和重量級鎖狀態。鎖只能按照下面的順序升級,鎖一旦升級,就不能降級。
在這裏插入圖片描述
各種鎖特點及適用場景:

鎖類型 優點 缺點 適用場景
偏向鎖 加鎖和解鎖不需要額外的消耗,與執行非同步方法僅存在納秒級的差距 如果線程間存在競爭,會帶來額外的鎖撤銷的消耗 適用於只有一個線程訪問同步塊的情況
輕量級鎖 競爭的線程不會堵塞,提高了程序的響應速度 始終得不到鎖的線程,使用自旋會消耗CPU 追求響應時間,同步塊執行速度非常塊
重量級鎖 線程競爭不使用自旋,不會消耗CPU 線程堵塞,響應時間緩慢 追求吞吐量,同步塊執行速度比較慢,競爭鎖的線程大於2個

2.1 偏向鎖

偏向鎖源自Java6,顧名思義,偏向鎖是指偏向一個線程的鎖,更具體點說就是,在偏向鎖機制下,一個線程一旦持有了鎖,那麼JVM默認該線程持續持有鎖,直到另一個線程加入競爭。

偏向鎖獲取鎖的過程:

  1. 首先檢查 Mark Word 是否爲可偏向鎖的狀態,爲1即表示支持可偏向鎖,爲0表示不支持可偏向鎖。
  2. 如果是可偏向鎖,則檢查Mark Word儲存的線程ID是否爲當前線程ID,如果是則執行同步塊,否則執行步驟3。
  3. 如果檢查到Mark Word的ID不是本線程的ID,則通過CAS操作去修改線程ID修改成本線程的ID,如果修改成功則執行同步代碼塊,否則執行步驟4。
  4. 當擁有該鎖的線程到達安全點之後,掛起這個線程,升級爲輕量級鎖。

偏向鎖釋放的過程:

  1. 偏向鎖的釋放採用了一種只有競爭纔會釋放鎖的機制,線程是不會主動去釋放偏向鎖,需要等待其他線程來競爭。
  2. 等待全局安全點(在這個是時間點上沒有字節碼正在執行)。
  3. 暫停擁有偏向鎖的線程,檢查持有偏向鎖的線程是否活着,如果不處於活動狀態,則將對象頭設置爲無鎖狀態,否則設置爲被鎖定狀態。如果鎖對象處於無鎖狀態,則恢復到無鎖狀態(01),以允許其他線程競爭,如果鎖對象處於鎖定狀態,則掛起持有偏向鎖的線程,並將對象頭Mark Word的鎖記錄指針改成當前線程的鎖記錄,鎖升級爲輕量級鎖狀態。

偏向鎖產生自Java 6,並且是jdk默認啓動的選項,可以通過-XX:-UseBiasedLocking 來關閉偏向鎖。另外,偏向鎖默認不是立即就啓動的,在程序啓動後,通常有幾秒的延遲,可以通過命令 -XX:BiasedLockingStartupDelay=0來關閉延遲。

2.2 輕量級鎖

輕量級鎖從偏向鎖升級而來,在輕量級鎖狀態下,當鎖被一個線程佔有時,其他線程會通過自旋的形式嘗試獲取鎖,不會阻塞,從而提高性能。

輕量級鎖獲取鎖過程:

  1. 在線程進入同步代碼的時候,先判斷如果同步對象鎖狀態爲無鎖狀態(鎖標誌位爲"01"狀態,是否爲偏向鎖爲"0"),虛擬機首先將在當前線程的棧幀中建立一個名爲鎖記錄(Lock Recored)的空間,用於儲存鎖對象目前的Mark Word的拷貝(官方把這份拷貝加了個Displaced前綴,即Displaced Mark Word)。
  2. 將對象頭的Mark Word 複製到線程的鎖記錄(Lock Recored)中。
  3. 複製成功後,虛擬機將使用 CAS 操作嘗試將對象的Mark Word更新爲指向Lock Record的指針。如果這個更新成功了,則執行步驟4,否則執行步驟5。
  4. 更新成功,這個線程就擁有了該對象的鎖,並且對象Mark Word的鎖標誌位將轉變爲"00",即表示此對象處於輕量級鎖的狀態。
  5. 更新失敗,虛擬機首先會檢查對象的Mark Word是否指向當前線程的棧幀,如果是就說明當前線程已經擁有了這個對象的鎖,可以直接進入同步塊繼續執行,否則說明這個鎖對象已經被其其它線程搶佔了。進行自旋執行步驟3,如果自旋結束仍然沒有獲得鎖,輕量級鎖就需要膨脹爲重量級鎖,鎖標誌位狀態值變爲"10",Mark Word中儲存就是指向monitor對象的指針,當前線程以及後面等待鎖的線程也要進入阻塞狀態。

輕量級鎖釋放鎖的過程:

  1. 使用CAS操作將對象當前的Mark Word和線程中複製的Displaced Mark Word替換回來(依據Mark Word中鎖記錄指針是否還指向本線程的鎖記錄),如果替換成功,則執行步驟2,否則執行步驟3。
  2. 如果替換成功,整個同步過程就完成了,恢復到無鎖的狀態(01)。
  3. 如果替換失敗,說明有其他線程嘗試獲取該鎖(此時鎖已膨脹),那就要在釋放鎖的同時,喚醒被掛起的線程。

2.3 重量級鎖

重量級鎖(heavy weight lock),是使用操作系統互斥量(mutex)來實現的傳統鎖。 當所有對鎖的優化都失效時,將退回到重量級鎖。它與輕量級鎖不同競爭的線程不再通過自旋來競爭線程, 而是直接進入堵塞狀態,此時不消耗CPU,然後等擁有鎖的線程釋放鎖後,喚醒堵塞的線程, 然後線程再次競爭鎖。但是注意,當鎖膨脹(inflate)爲重量鎖時,就不能再退回到輕量級鎖。

3 其他鎖優化

3.1 鎖消除

鎖消除即刪除不必要的加鎖操作。虛擬機即時編輯器在運行時,對一些“代碼上要求同步,但是被檢測到不可能存在共享數據競爭”的鎖進行消除。

根據代碼逃逸技術,如果判斷到一段代碼中,堆上的數據不會逃逸出當前線程,那麼可以認爲這段代碼是線程安全的,不必要加鎖。

看下面這段程序:

public class SynchronizedTest {

    public static void main(String[] args) {
        SynchronizedTest test = new SynchronizedTest();

        for (int i = 0; i < 100000000; i++) {
            test.append("abc", "def");
        }
    }

    public void append(String str1, String str2) {
        StringBuffer sb = new StringBuffer();
        sb.append(str1).append(str2);
    }
}

雖然StringBuffer的append是一個同步方法,但是這段程序中的StringBuffer屬於一個局部變量,並且不會從該方法中逃逸出去(即StringBuffer sb的引用沒有傳遞到該方法外,不可能被其他線程拿到該引用),所以其實這過程是線程安全的,可以將鎖消除。

3.2 鎖粗化

如果一系列的連續操作都對同一個對象反覆加鎖和解鎖,甚至加鎖操作是出現在循環體中的,那即使沒有出現線程競爭,頻繁地進行互斥同步操作也會導致不必要的性能損耗。

如果虛擬機檢測到有一串零碎的操作都是對同一對象的加鎖,將會把加鎖同步的範圍擴展(粗化)到整個操作序列的外部。

舉個例子:

public class StringBufferTest {
    StringBuffer stringBuffer = new StringBuffer();

    public void append(){
        stringBuffer.append("a");
        stringBuffer.append("b");
        stringBuffer.append("c");
    }
}

這裏每次調用stringBuffer.append方法都需要加鎖和解鎖,如果虛擬機檢測到有一系列連串的對同一個對象加鎖和解鎖操作,就會將其合併成一次範圍更大的加鎖和解鎖操作,即在第一次append方法時進行加鎖,最後一次append方法結束後進行解鎖。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章