線程安全與鎖優化

線程安全與鎖優化



線程安全

  • Brian Goetz定義的線程安全:當多個線程訪問一個對象時,如果不用考慮這些線程在運行時環境下的調度和交替執行,也不需要進行額外的同步,或者在調用方進行任何其他的協調操作,調用這個對象的行爲都可以獲得正確的結果,那這個對象是線程安全的。—- 它要求線程安全的代碼都必須具備一個特徵:代碼本身封裝了所有必要的正確性保障手段(如互斥同步等),令調用者無須關心多線程的問題,更無須自己採取任何措施來保證多線程的正確調用。

Java語言中的線程安全

  • 在Java語言中,線程安全具體是如何體現的?有哪些操作是線程安全的?討論的前提:多個線程之間存在共享數據訪問。
  • Java語言中各種操作共享的數據分爲以下5類:不可變、絕對線程安全、相對線程安全、線程兼容和線程對立。

不可變

  • 在Java語言中(特指JDK 1.5以後Java內存模型被修正之後的Java語言),不可變(Immutable)的對象一定是線程安全的,無論是對象的方法實現還是方法的調用者,都不需要再採取任何的線程安全保障措施,只要一個不可變的對象被正確地構建出來(沒有發生this引用逃逸的情況),那其外部的可見狀態永遠也不會改變,永遠也不會看到它在多個線程之中處於不一致的狀態。“不可變”帶來的安全性是最簡單和最純粹的。
  • 共享數據是一個基本數據類型,那麼只要在定義時使用final關鍵字修飾它就可以保證它是不可變的。 如果共享數據是一個對象,那就需要保證對象的行爲不會對其狀態產生任何影響才行。– java.lang.String類的對象是不可變對象,調用它的substring()、replace()和concat()等方法只會返回一個新構造的字符串對象。保證對象行爲不影響自己狀態最簡單的就是把對象中帶有狀態的變量都聲明爲final。
  • Java API中不可變的類型: String,枚舉類型,以及java.lang.Number的部分子類,如Long和Double等數值包裝類型,BigInteger和BigDecimal等大數據類型;但同爲Number的子類型的原子類AtomicInteger和AtomicLong則並非不可變的。

絕對線程安全

  • 絕對的線程安全完全滿足Brian Goetz給出的線程安全的定義(很嚴格),一個類要達到“不管運行時環境如何,調用者都不需要任何額外的同步措施”通常需要付出很大的,甚至有時候是不切實際的代價。 在Java API中標註自己是線程安全的類,大多數都不是絕對的線程安全。
  • java.util.Vector是一個線程安全的容器,因爲它的add()、get()和size()這類方法都是被synchronized修飾的,儘管這樣效率很低,但確實是安全的。但是,即使它所有的方法都被修飾成同步,也不意味着調用它的時候永遠都不再需要同步手段。
# 對Vector線程安全的測試
public class VectorTest1 {
    private static Vector<Integer> vector = new Vector<Integer>();

    public static void main(String[] args) {
        while (true) {
            for (int i = 0; i < 10; i++) {
                vector.add(i);
            }

            Thread removeThread = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < vector.size(); i++) {
                        vector.remove(i);
                    }
                }
            });

            Thread printThread = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < vector.size(); i++) {
                        System.out.println((vector.get(i)));
                    }
                }
            });

            removeThread.start();
            printThread.start();

            //不要同時產生過多的線程,否則會導致操作系統假死
            while (Thread.activeCount() > 20);
        }
    }
}
# 如果另一個線程恰好在錯誤的時間裏刪除了一個元素,導致序號i已經不再可用的話,再用i訪問數組就會拋出一個ArrayIndexOutOfBoundsException。 
...
9
Exception in thread "Thread-138207" java.lang.ArrayIndexOutOfBoundsException: Array index out of range: 14
    at java.util.Vector.get(Vector.java:748)
    at ch13.VectorTest1$2.run(VectorTest1.java:68)
    at java.lang.Thread.run(Thread.java:745)
0
...
# 必須加入同步以保證Vector訪問的線程安全性
Thread removeThread = new Thread(new Runnable() {
    @Override
    public void run() {
        // 顯示的進行同步;
        synchronized(vecotr) {
            for (int i = 0; i < vector.size(); i++) {
                vector.remove(i);
           }
        }
    }
});

Thread printThread = new Thread(new Runnable() {
    @Override
    public void run() {
        // 顯示的進行同步。
       synchronized(vecotr) {
            for (int i = 0; i < vector.size(); i++) {
                System.out.println(vector.get(i))
           }
        }
    }
});

相對的線程安全

  • 保證對這個對象單獨的操作是線程安全的,我們在調用的時候不需要做額外的保障措施,但是對於一些特定順序的連續調用,就可能需要在調用端使用額外的同步手段來保證調用的正確性。
  • Java語言中,大部分的線程安全類都屬於這種類型,例如Vector、HashTable、Collections的synchronizedCollection()方法包裝的集合。

線程兼容

  • 對象本身並不是線程安全的,但是可以通過在調用端正確地使用同步手段來保證對象在併發環境中可以安全地使用,我們平常說一個類不是線程安全的,絕大多數時候指的是這一種情況。 Java API中大部分的類都是屬於線程兼容的,包括集合類ArrayList和HashMap等。

線程對立

  • 線程對立是指無論調用端是否採取了同步措施,都無法在多線程環境中併發使用的代碼。
  • 線程對立的例子是Thread類的suspend()和resume()方法,如果有兩個線程同時持有一個線程對象,一個嘗試去中斷線程,另一個嘗試去恢復線程,如果併發進行的話,無論調用時是否進行了同步,目標線程都是存在死鎖風險的,如果suspend()中斷的線程就是即將要執行resume()的那個線程,那就肯定要產生死鎖了。也正是由於這個原因,suspend()和resume()方法已經被JDK聲明廢棄(@Deprecated)了。 常見的線程對立的操作還有System.setIn()、Sytem.setOut()和System.runFinalizersOnExit()等。

線程安全的實現方法

  • 應該如何實現線程安全?代碼編寫 & 虛擬機提供的同步和鎖機制

互斥同步

  • 互斥同步(Mutual Exclusion&Synchronization)是常見的一種併發正確性保障手段。同步是指在多個線程併發訪問共享數據時,保證共享數據在同一個時刻只被一個(或者是一些,使用信號量的時候)線程使用。 而互斥是實現同步的一種手段,臨界區(Critical Section)、互斥量(Mutex)和信號量(Semaphore)都是主要的互斥實現方式。因此,在這4個字裏面,互斥是因,同步是果;互斥是方法,同步是目的。
  • Java中,最基本的互斥同步手段就是synchronized關鍵字,synchronized關鍵字經過編譯之後,會在同步塊的前後分別形成monitorenter和monitorexit這兩個字節碼指令,這兩個字節碼都需要一個reference類型的參數來指明要鎖定和解鎖的對象。如果Java程序中的synchronized明確指定了對象參數,那就是這個對象的reference;如果沒有明確指定,那就根據synchronized修飾的是實例方法還是類方法,去取對應的對象實例或Class對象來作爲鎖對象
  • JVM中指定:執行monitorenter指令時,首先要嘗試獲取對象的鎖。如果這個對象沒被鎖定,或者當前線程已經擁有了那個對象的鎖,把鎖的計數器加1,相應的,在執行monitorexit指令時會將鎖計數器減1,當計數器爲0時,鎖就被釋放。如果獲取對象鎖失敗,那當前線程就要阻塞等待,直到對象鎖被另外一個線程釋放爲止。
  • JVM中對monitorenter和monitorexit的行爲描述:首先,synchronized同步塊對同一條線程來說是可重入的,不會出現自己把自己鎖死的問題。其次,同步塊在已進入的線程執行完之前,會阻塞後面其他線程的進入
  • Java的線程是映射到操作系統的原生線程之上的,如果要阻塞或喚醒一個線程,都需要操作系統來幫忙完成,這就需要從用戶態轉換到核心態中,因此狀態轉換需要耗費很多的處理器時間。對於代碼簡單的同步塊(如被synchronized修飾的getter()或setter()方法),狀態轉換消耗的時間有可能比用戶代碼執行的時間還要長。 所以synchronized是Java語言中一個重量級(Heavyweight)的操作。而虛擬機本身也會進行一些優化,譬如在通知操作系統阻塞線程之前加入一段自旋等待過程,避免頻繁地切入到核心態之中。
  • 使用java.util.concurrent(下文稱J.U.C)包中的重入鎖(ReentrantLock)來實現同步,在基本用法上ReentrantLock與synchronized很相似,都具備一樣的線程重入特性;代碼寫法上區別 – 一個表現爲API層面的互斥鎖(lock()和unlock()方法配合try/finally語句塊來完成),另一個表現爲原生語法層面的互斥鎖。不過,相比synchronized,ReentrantLock增加了一些高級功能,主要有以下3項:等待可中斷、 可實現公平鎖,以及鎖可以綁定多個條件。
    • 等待可中斷是指當持有鎖的線程長期不釋放鎖的時候,正在等待的線程可以選擇放棄等待,改爲處理其他事情,可中斷特性對處理執行時間非常長的同步塊很有幫助。
    • 公平鎖是指多個線程在等待同一個鎖時,必須按照申請鎖的時間順序來依次獲得鎖;而非公平鎖則不保證這一點,在鎖被釋放時,任何一個等待鎖的線程都有機會獲得鎖。synchronized中的鎖是非公平的,ReentrantLock默認情況下也是非公平的,但可以通過帶布爾值的構造函數要求使用公平鎖。
    • 鎖綁定多個條件是指一個ReentrantLock對象可以同時綁定多個Condition對象,而在synchronized中,鎖對象的wait()和notify()或notifyAll()方法可以實現一個隱含的條件,如果要和多於一個的條件關聯的時候,就不得不額外地添加一個鎖,而ReentrantLock則無須這樣做,只需要多次調用newCondition()方法即可。

ch13-synchronized-vs-reetranlock.png-106.9kB

ch13-sychroinzed-vs-reetrantlock-2.png-99.6kB

  • 上圖是根據JDK1.5做的測試,JDk1.6優化後,synchronized與ReentrantLock的性能基本上是完全持平;提倡在synchronized能實現需求的情況下,優先考慮使用synchronized來進行同步。

非阻塞同步

  • 互斥同步最主要的問題就是進行線程阻塞和喚醒所帶來的性能問題 – 阻塞同步(Blocking Synchronization)。 從處理問題的方式上說,互斥同步屬於一種悲觀的併發策略,總是認爲只要不去做正確的同步措施(例如加鎖),那就肯定會出現問題,無論共享數據是否真的會出現競爭,它都要進行加鎖(這裏討論的是概念模型,實際上虛擬機會優化掉很大一部分不必要的加鎖)、用戶態核心態轉換、維護鎖計數器和檢查是否有被阻塞的線程需要喚醒等操作。
  • 隨着硬件指令集的發展,我們可以使用基於衝突檢測的樂觀併發策略,通俗地說,就是先進行操作,如果沒有其他線程爭用共享數據,那操作就成功了;如果共享數據有爭用,產生了衝突,那就再採取其他的補償措施(最常見的補償措施就是不斷地重試,直到成功爲止),這種樂觀的併發策略的許多實現都不需要把線程掛起,因此這種同步操作稱爲非阻塞同步(Non-Blocking Synchronization)。
  • 樂觀併發策略需要操作和衝突檢測這兩個步驟具備原子性,如果這裏再使用互斥同步來保證就失去意義了,所以我們只能靠硬件來完成這件事情(硬件指令集的提升),硬件保證一個從語義上看起來需要多次操作的行爲只通過一條處理器指令就能完成。這類指令常用的有:

    • 測試並設置(Test-and-Set)。
    • 獲取並增加(Fetch-and-Increment)。
    • 交換(Swap)。
    • 比較並交換(Compare-and-Swap,簡稱CAS,新增)。
    • 加載鏈接/條件存儲(Load-Linked/Store-Conditional,簡稱LL/SC,新增)。
  • 後面的兩條是現代處理器新增的,而且這兩條指令的目的和功能是類似的。 在IA64、 x86指令集中有
    cmpxchg指令完成CAS功能,在sparc-TSO也有casa指令實現,而在ARM和PowerPC架構下,則需要使用一對ldrex/strex指令來完成LL/SC的功能。

  • CAS指令需要有3個操作數,分別是內存位置(在Java中可以簡單理解爲變量的內存地址,用V表示)、 舊的預期值(用A表示)和新值(用B表示)。CAS指令執行時,當且僅當V符合舊預期值A時,處理器用新值B更新V的值,否則它就不執行更新,但是無論是否更新了V的值,都會返回V的舊值,上述的處理過程是一個原子操作。
  • JDK 1.5之後,Java程序中纔可以使用CAS操作,該操作由sun.misc.Unsafe類裏面的compareAndSwapInt()compareAndSwapLong()等幾個方法包裝提供,虛擬機在內部對這些方法做了特殊處理,即時編譯出來的結果就是一條平臺相關的處理器CAS指令,沒有方法調用的過程,或者可以認爲是無條件內聯進去了。
  • 由於Unsafe類不是提供給用戶程序調用的類(Unsafe.getUnsafe()的代碼中限制了只有啓動類加載器(Bootstrap ClassLoader)加載的Class才能訪問它),因此,如果不採用反射手段,我們只能通過其他的Java API來間接使用它,如J.U.C包裏面的整數原子類,其中的compareAndSet()和getAndIncrement()等方法都使用了Unsafe類的CAS操作。
public class AtomicTest {

    public static AtomicInteger race = new AtomicInteger(0);

    public static void increase() {
        // incrementAndGet()方法的原子性;
        race.incrementAndGet();
    }

    private static final int THREADS_COUNT = 20;

    public static void main(String[] args) throws Exception {
        Thread[] threads = new Thread[THREADS_COUNT];
        for (int i = 0; i < THREADS_COUNT; i++) {
            threads[i] = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 10000; i++) {
                        increase();
                    }
                }
            });
            threads[i].start();
        }

        while (Thread.activeCount() > 1)
            Thread.yield();

        System.out.println(race);
    }
}

# output:
darcy@darcy-pc:~/IdeaProjects/jvm_in_action/src$ javac ch13/AtomicTest.java 
darcy@darcy-pc:~/IdeaProjects/jvm_in_action/src$ java ch13.AtomicTest 
200000
# incrementAndGet()方法的JDK源碼
/**
 * Atomically increments by one the current value.
 *
 * @return the updated value
 */
public final int incrementAndGet() {
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
  • CAS無法涵蓋互斥同步的所有使用場景,並且CAS從語義上來說並不是完美的,存在這樣的一個邏輯漏洞:如果一個變量V初次讀取的時候是A值,並且在準備賦值的時候檢查到它仍然爲A值,那我們就能說它的值沒有被其他線程改變過了嗎?如果在這段期間它的值曾經被改成了B,後來又被改回爲A,那CAS操作就會誤認爲它從來沒有被改變過。這個漏洞稱爲CAS操作的“ABA”問題。J.U.C包爲了解決這個問題,提供了一個帶有標記的原子引用類“AtomicStampedReference”,它可以通過控制變量值的版本來保證CAS的正確性。 不過目前來說這個類比較“雞肋”,大部分情況下ABA問題不會影響程序併發的正確性,如果需要解決ABA問題,改用傳統的互斥同步可能會比原子類更高效。

無同步方案

  • 同步只是保證共享數據爭用時的正確性的手段,如果一個方法本來就不涉及共享數據,那它自然就無須任何同步措施去保證正確性,因此會有一些代碼天生就是線程安全的。
    • 可重入代碼(Reentrant Code):這種代碼也叫做純代碼(Pure Code),可以在代碼執行的任何時刻中斷它,轉而去執行另外一段代碼(包括遞歸調用它本身),而在控制權返回後,原來的程序不會出現任何錯誤。可重入代碼有一些共同的特徵,例如不依賴存儲在堆上的數據和公用的系統資源、 用到的狀態量都由參數中傳入、不調用非可重入的方法等。我們可以通過一個簡單的原則來判斷代碼是否具備可重入性:如果一個方法,它的返回結果是可以預測的,只要輸入了相同的數據,就都能返回相同的結果,那它就滿足可重入性的要求,當然也就是線程安全的。
    • 線程本地存儲(Thread Local Storage):如果一段代碼中所需要的數據必須與其他代碼共享,那就看看這些共享數據的代碼是否能保證在同一個線程中執行?如果能保證,我們就可以把共享數據的可見範圍限制在同一個線程之內,這樣,無須同步也能保證線程之間不出現數據爭用的問題。– 消費隊列的架構模式,Web交互模型中的“一個請求對應一個服務器線程”(Thread-per-Request)的處理方式。
  • Java語言中,如果一個變量要被多線程訪問,可以使用volatile關鍵字聲明它爲“易變的”;如果一個變量要被某個線程獨享,可以通過java.lang.ThreadLocal類來實現線程本地存儲的功能。每一個線程的Thread對象中都有一個ThreadLocalMap對象,這個對象存儲了一組以ThreadLocal.threadLocalHashCode爲鍵,以本地線程變量爲值的K-V值對。

鎖優化

  • 各種鎖優化技術,如適應性自旋(Adaptive Spinning)、鎖消除(Lock Elimination)、鎖粗化(Lock Coarsening)、輕量級鎖(Lightweight Locking)和偏向鎖(Biased Locking)等,這些技術都是爲了在線程之間更高效地共享數據,以及解決競爭問題

自旋鎖與自適應自旋

  • 互斥同步對性能最大的影響是阻塞的實現,掛起線程和恢復線程的操作都需要轉入內核態中完成,這些操作給系統的併發性能帶來了很大的壓力。
  • 在許多應用上,共享數據的鎖定狀態只會持續很短的一段時間,爲了這段時間去掛起和恢復線程並不值得。 如果物理機器有一個以上的處理器,能讓兩個或以上的線程同時並行執行,我們就可以讓後面請求鎖的那個線程“稍等一下”,同時不放棄處理器的執行時間,看看持有鎖的線程是否很快就會釋放鎖。爲了讓線程等待,我們只需讓線程執行一個忙循環(自旋),這項技術就是所謂的自旋鎖
  • 自旋鎖在JDK 1.6中默認開啓。自旋等待不能代替阻塞,自旋鎖對處理器數量有要求,自旋等待本身雖然避免了線程切換的開銷,但它是要佔用處理器時間的,因此,如果鎖被佔用的時間很短,自旋等待的效果就會非常好,反之,如果鎖被佔用的時間很長,那麼自旋的線程只會白白消耗處理器資源,而不會做任何有用的工作,反而會帶來性能上的浪費。因此,自旋等待的時間必須要有一定的限度,如果自旋超過了限定的次數仍然沒有成功獲得鎖,就應當使用傳統的方式去掛起線程了。自旋次數的默認值是10次,用戶可以使用參數-XX:PreBlockSpin來更改。
  • JDK 1.6中引入了自適應的自旋鎖。自適應意味着自旋的時間不再固定了,而是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定。如果在同一個鎖對象上,自旋等待剛剛成功獲得過鎖,並且持有鎖的線程正在運行中,那麼虛擬機就會認爲這次自旋也很有可能再次成功,進而它將允許自旋等待持續相對更長的時間,比如100個循環。另外,如果對於某個鎖,自旋很少成功獲得過,那在以後要獲取這個鎖時將可能省略掉自旋過程,以避免浪費處理器資源。有了自適應自旋,隨着程序運行和性能監控信息的不斷完善,虛擬機對程序鎖的狀況預測就會越來越準確,虛擬機就會變得越來越“聰明”了。

鎖消除

  • 鎖消除是指虛擬機即時編譯器在運行時,對一些代碼上要求同步,但是被檢測到不可能存在共享數據競爭的鎖進行消除。 鎖消除的主要判定依據來源於逃逸分析的數據支持,如果判斷在一段代碼中,堆上的所有數據都不會逃逸出去從而被其他線程訪問到,那就可以把它們當做棧上數據對待,認爲它們是線程私有的,同步加鎖自然就無須進行。(方法內的字符串+操作在JDK1.5以前轉爲StringBuffer的實現)

鎖粗化

  • 一般推薦將同步塊的作用範圍限制得儘量小 – 只在共享數據的實際作用域中才進行同步,這樣是爲了使得需要同步的操作數量儘可能變小,如果存在鎖競爭,那等待鎖的線程也能儘快拿到鎖。但是如果一系列的連續操作都對同一個對象反覆加鎖和解鎖,甚至加鎖操作是出現在循環體中的,那即使沒有線程競爭,頻繁地進行互斥同步操作也會導致不必要的性能損耗。如果虛擬機探測到有這樣一串零碎的操作都對同一個對象加鎖,將會把加鎖同步的範圍擴展(粗化)到整個操作序列的外部,這樣只需要加鎖一次

輕量級鎖

  • 輕量級鎖是JDK 1.6之中加入的新型鎖機制,“輕量級”是相對於使用操作系統互斥量來實現的傳統鎖而言的。輕量級鎖並不是用來代替重量級鎖的,它的本意是在沒有多線程競爭的前提下,減少傳統的重量級鎖使用操作系統互斥量產生的性能消耗。
  • HotSpot虛擬機的對象頭(Object Header)分爲兩部分信息,第一部分用於存儲對象自身的運行時數據,如哈希碼(HashCode)、GC分代年齡(Generational GC Age)等,這部分數據的長度在32位和64位的虛擬機中分別爲32bit和64bit,官方稱它爲“Mark Word”,它是實現輕量級鎖和偏向鎖的關鍵。在32位的HotSpot虛擬機中對象未被鎖定的狀態下,Mark Word的32bit空間中的25bit用於存儲對象哈希碼(HashCode),4bit用於存儲對象分代年齡,2bit用於存儲鎖標誌位,1bit固定爲0。

ch13-markword.png-130.8kB

  • 在代碼進入同步塊的時候,如果此同步對象沒有被鎖定(鎖標誌位爲“01”狀態),虛擬機首先將在當前線程的棧幀中建立一個名爲鎖記錄(Lock Record)的空間,用於存儲鎖對象目前的Mark Word的拷貝(官方把這份拷貝加了一個Displaced前綴,即Displaced Mark Word)
  • 然後,虛擬機將使用CAS操作嘗試將對象的Mark Word更新爲指向Lock Record的指針。如果這個更新動作成功了,那麼這個線程就擁有了該對象的鎖,並且對象Mark Word的鎖標誌位(Mark Word的最後2bit)將轉變爲“00”,即表示此對象處於輕量級鎖定狀態。

ch13-light-weighted-lock-cas.png-139.9kB

  • 如果更新操作失敗了,虛擬機首先會檢查對象的Mark Word是否指向當前線程的棧幀,如果是說明當前線程已經擁有了這個對象的鎖,那就可以直接進入同步塊繼續執行,否則說明這個鎖對象已經被其他線程搶佔了。如果有兩條以上的線程爭用同一個鎖,那輕量級鎖就不再有效,要膨脹爲重量級鎖,鎖標誌的狀態值變爲“10”,Mark Word中存儲的就是指向重量級鎖(互斥量)的指針,後面等待鎖的線程也要進入阻塞狀態。
  • 上面描述的是輕量級鎖的加鎖過程,它的解鎖過程也是通過CAS操作來進行的,如果對象的Mark Word仍然指向着線程的鎖記錄,那就用CAS操作把對象當前的Mark Word和線程中複製的Displaced Mark Word替換回來,如果替換成功,整個同步過程就完成了。如果替換失敗,說明有其他線程嘗試過獲取該鎖,那就要在釋放鎖的同時,喚醒被掛起的線程。

  • 輕量級鎖能提升程序同步性能的依據是“對於絕大部分的鎖,在整個同步週期內都是不存在競爭的”,這是一個經驗數據。 如果沒有競爭,輕量級鎖使用CAS操作避免了使用互斥量的開銷,但如果存在鎖競爭,除了互斥量的開銷外,還額外發生了CAS操作,因此在有競爭的情況下,輕量級鎖會比傳統的重量級鎖更慢。

偏向鎖

  • 偏向鎖是JDK 1.6中引入的鎖優化,它的目的是消除數據在無競爭情況下的同步原語,進一步提高程序的運行性能。 如果說輕量級鎖是在無競爭的情況下使用CAS操作去消除同步使用的互斥量,那偏向鎖就是在無競爭的情況下把整個同步都消除掉,連CAS操作都不做了。“偏”,就是偏心的“偏”、它的意思是這個鎖會偏向於第一個獲得它的線程,如果在接下來的執行過程中,該鎖沒有被其他的線程獲取,則持有偏向鎖的線程將永遠不需要再進行同步。
  • 假設當前虛擬機啓用了偏向鎖(啓用參數-XX:+UseBiasedLocking,這是JDK 1.6的默認值),那麼,當鎖對象第一次被線程獲取的時候,虛擬機將會把對象頭中的標誌位設爲“01”,即偏向模式。 同時使用CAS操作把獲取到這個鎖的線程的ID記錄在對象的Mark Word之中,如果CAS操作成功,持有偏向鎖的線程以後每次進入這個鎖相關的同步塊時,虛擬機都可以不再進行任何同步操作(例如Locking、 Unlocking 及對Mark Word的Update等)。
  • 有另外一個線程去嘗試獲取這個鎖時,偏向模式就宣告結束。根據鎖對象目前是否處於被鎖定的狀態,撤銷偏向(Revoke Bias)後恢復到未鎖定(標誌位爲“01”)或輕量級鎖定(標誌位爲“00”)的狀態,後續的同步操作就如輕量級鎖那樣執行。 偏向鎖、 輕量級鎖的狀態轉化及對象Mark Word的關係如下:

ch13-biased-lock-lwl-markword.png-266.7kB

  • 偏向鎖可以提高帶有同步但無競爭的程序性能。 它同樣是一個帶有效益權衡(Trade Off)性質的優化,也就是說,它並不一定總是對程序運行有利,如果程序中大多數的鎖總是被多個不同的線程訪問,那偏向模式就是多餘的。在具體問題具體分析的前提下,有時候使用參數-XX:-UseBiasedLocking來禁止偏向鎖優化反而可以提升性能。

ref

深入理解Java虛擬機(第二版)

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