Synchronized關鍵字和鎖優化


多線程編程中,我們往往使用synchronized關鍵字以及ReentrantLock類來實現線程安全,二者都是基於互斥同步的方式來保障併發安全性,且都可重入。最基本的互斥同步手段是synchronized關鍵字,它是原生語法層面的互斥鎖,而ReentrantLock是API層面的互斥鎖;但其實在JDK1.6之前,synchronized關鍵字的性能遠遠不如ReentrantLock。在1.6以及之後的版本中,synchronized關鍵字因爲加入了很多的優化措施,性能基本與ReentrantLock持平。synchronized是原生的,未來可能會得到進一步的優化,所以推薦使用synchronized關鍵字來進行同步。本篇論文主要討論synchronized的底層實現原理以及JAVA虛擬機針對synchronized所做的鎖優化。

synchronized的底層原理

Java 虛擬機中的互斥同步基於進入和退出管程(Monitor)對象實現, 無論是顯式同步(對代碼塊進行同步)還是隱式同步(對方法進行同步)都是如此。synchronized修飾代碼塊來實現同步(顯示同步)其實是通過兩個指令來完成,分別是monitorenter 和 monitorexit 指令。同步方法(隱式同步) 並不是由 monitorenter 和 monitorexit 指令來實現同步的,而是由方法調用指令讀取運行時常量池中方法的 ACC_SYNCHRONIZED 標誌來隱式實現的。所以想要理解synchronized的底層原理先了解Monitor是什麼以及理解Monitor與對象的關係,再者我們需要知道synchronized是如何去用以及應用場景,然後再詳細的解析底層原理(分爲修飾代碼塊以及方法的底層實現),最後談論下爲什麼說synchronized是一個重量級鎖,爲什麼需要優化。

Monitor

什麼是Monitor?Monitor又可被稱作管程或者監視器,它實現同步是依賴於底層的操作系統的Mutex Lock來實現的。我們可以把它理解爲一個同步工具,也可以描述爲一種同步機制,它通常被描述爲一個對象。每一個對象都與Monitor對象相關聯。
Monitor 是線程私有的數據結構,每一個線程都有一個可用monitor record列表,同時還有一個全局的可用列表。每一個被鎖住的對象都會和一個monitor關聯(對象頭的MarkWord中的LockWord指向monitor的起始地址),同時monitor中有一個Owner字段存放擁有該鎖的線程的唯一標識,表示該鎖被這個線程佔用。其數據結構如下:
在這裏插入圖片描述
Owner:初始時爲NULL表示當前沒有任何線程擁有該monitor record,當線程成功擁有該鎖後保存線程唯一標識,當鎖被釋放時又設置爲NULL;
EntryQ:關聯一個系統互斥鎖(semaphore),阻塞所有試圖鎖住monitor record失敗的線程。
RcThis:表示blocked或waiting在該monitor record上的所有線程的個數。
Nest:用來實現重入鎖的計數。
HashCode:保存從對象頭拷貝過來的HashCode值(可能還包含GC age)。
Candidate:用來避免不必要的阻塞或等待線程喚醒,因爲每一次只有一個線程能夠成功擁有鎖,如果每次前一個釋放鎖的線程喚醒所有正在阻塞或等待的線程,會引起不必要的上下文切換(從阻塞到就緒然後因爲競爭鎖失敗又被阻塞)從而導致性能嚴重下降。Candidate只有兩種可能的值0表示沒有需要喚醒的線程1表示要喚醒一個繼任線程來競爭鎖。
(此部分摘自:Java中synchronized的實現原理與應用

理解對象頭與Monitor的關係

在JVM中,對象在內存中的佈局分爲三塊區域:對象頭、實例數據和對齊填充。
對象頭:對象頭包括兩部分信息,第一部分用於存儲對象自身的運行時數據,被稱作"Mark Word",包括哈希碼、GC分代年齡、鎖狀態標誌、線程持有的鎖、偏向線程ID、偏向時間戳等。第二部分是類型指針,指向給對象所屬的類(Java數組的話,對象頭還包括了記錄數組長度的數據)
實例數據:對象真正存儲的有效信息,程序中定義的各種類型的字段內容。
對齊填充:起着佔位符的作用,虛擬機的自動內存管理系統要求對象的大小必須是8的倍數,對象頭部分滿足,但實例數據卻不一定滿足,此時可通過對齊填充來補全。

這裏主要討論對象頭,因爲synchronized的鎖對象的引用存儲在對象頭中。jvm中採用2個字來存儲對象頭(如果對象是數組則會分配3個字,多出來的1個字記錄的是數組長度),synchronized使用的鎖是存放在Java對象頭裏面,具體位置是對象頭裏面的MarkWord,Mark Word 被設計成爲一個非固定的數據結構,MarkWord裏默認數據是存儲對象的HashCode等信息,但是會隨着對象的運行改變而發生變化,不同的鎖狀態對應着不同的記錄存儲方式,可能值如下所示
在這裏插入圖片描述
重量級鎖也就是通常說synchronized的對象鎖,鎖標識位爲10,其中指針指向的是monitor對象(管程/監視器)。每個對象都存在着一個 monitor 與之關聯,對象與其 monitor 之間的關係有存在多種實現方式,如monitor可以與對象一起創建銷燬或當線程試圖獲取對象鎖時自動生成,但當一個 monitor 被某個線程持有後,它便處於鎖定狀態。

在Java虛擬機(HotSpot)中,monitor是由ObjectMonitor實現的,ObjectMonitor中有兩個隊列,_WaitSet 和 _EntryList,用來保存ObjectWaiter對象列表( 每個等待鎖的線程都會被封裝成ObjectWaiter對象),_owner指向持有ObjectMonitor對象的線程,當多個線程同時訪問一段同步代碼時,首先會進入 _EntryList 集合,當線程獲取到對象的monitor 後進入 _Owner 區域並把monitor中的owner變量設置爲當前線程同時monitor中的計數器count加1,若線程調用 wait() 方法,將釋放當前持有的monitor,owner變量恢復爲null,count自減1,同時該線程進入 WaitSe t集合中等待被喚醒。若當前線程執行完畢也將釋放monitor(鎖)並復位變量的值,以便其他線程進入獲取monitor(鎖)。

綜上所述,當對象處於重量鎖狀態,我們常說的鎖其實是指的Monitor,常說的鎖存儲在對象中,其實指的是該對象頭的MarkWord中存儲着指向Monitor對象的指針。至於如何去獲得Monitor以及爲什麼會爭奪失敗則會在下面詳述。

synchronized的使用場景

synchronized關鍵字可以用來修飾實例方法、靜態方法、代碼塊。但無論是哪種,其實鎖是加到了整個對象上,而不僅僅是其中的一個方法。
修飾實例方法:鎖的是當前實例對象
修飾靜態方法:–也就是給當前類Class加鎖,會作用於類的所有對象實例,因爲靜態成員不屬於任何一個實例對象,是類成員。所以如果一個線程A調用一個實例對象的非靜態 synchronized 方法,而線程B需要調用這個實例對象所屬類的靜態 synchronized 方法,是允許的,不會發生互斥現象,因爲訪問靜態 synchronized 方法佔用的鎖是當前類(類名.class)的鎖,而訪問非靜態 synchronized 方法佔用的鎖是當前實例對象鎖,鎖兩個不同的對象當然不會衝突。
修飾代碼塊:鎖的是Synchronized括號裏配置的對象,進入同步代碼庫前要獲得給定對象的鎖。

底層原理

這裏分別講述顯式同步(同步代碼塊)以及隱式同步(同步方法)的底層原理。

顯式同步

synchronized修飾代碼塊來實現同步其實是通過兩個指令來完成。分別是monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代碼塊的開始位置,monitorexit 指令則指明同步代碼塊的結束位置(二者必須成對存在)。當程序執行monitorenter指令,該線程便開始嘗試獲取該對象對應的monitor(監視器),當monitor的計數器爲0時,表示目前沒有線程獲得該對象的同步鎖,因此可以獲得該對象的monitor,成功獲取後,monitor計數器變爲1(其它線程阻塞,等擁有者釋放鎖才能獲得對象的鎖)。當monitorexit 指令被執行,執行線程將釋放 monitor 並設置計數器值爲 0。其他線程將有機會持有 monitor。

值得注意的是編譯器將會確保無論方法通過何種方式完成,方法中調用過的每條 monitorenter 指令都有執行其對應 monitorexit 指令,而無論這個方法是正常結束還是異常結束。爲了保證在方法異常完成時 monitorenter 和 monitorexit 指令依然可以正確配對執行,編譯器會自動產生一個異常處理器,這個異常處理器聲明可處理所有的異常,它的目的就是用來執行 monitorexit 指令。從字節碼中也可以看出多了一個monitorexit指令,它就是異常結束時被執行的釋放monitor 的指令。

        if(singleinstance==null){
            synchronized(singleInstance.class){
             if(singleinstance==null){
                 singleinstance=new singleInstance();
             }
            }
        }

反編譯分析(黃線處)
在這裏插入圖片描述

隱式同步

當synchronized修飾方法來實現同步時,底層不是使用的monitorenter 和 monitorexit 指令來實現同步,而是通過隱式的同步機制。當方法調用時,調用指令將會檢查方法的 ACC_SYNCHRONIZED 訪問標誌是否被設置,如果設置了,則會去嘗試去獲取該對象的對應的Monitor,當Monitor的計數器爲0,則線程持有Monitor,然後再去執行方法,方法完成之後(無論是否正常完成)釋放持有的Monitor。如果一個同步方法執行期間拋出了異常並且在方法內部無法處理此異常,那這個同步方法所持有的monitor將在異常拋到同步方法之外時自動釋放。
(仔細觀察以下示例,出現的是ACC_SYNCHRONIZED 而不是monitorenter 和 monitorexit 指令)

  public synchronized  void decrease() {
        //i--;
        System.out.println("decrease method is executed");
    }
 public synchronized void decrease();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED //注意該標識
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #4                  // String decrease method is executed
         5: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 11: 0
        line 12: 8

synchronized需要優化的原因

JVM中Monitor依賴於底層的操作系統的Mutex Lock來實現同步,但是由於使用Mutex Lock需要將當前線程掛起並從用戶態切換到內核態來執行。Java 的線程是映射到操作系統的原生線程之上的,如果要掛起或者喚醒一個線程,都需要操作系統幫忙完成,而操作系統實現線程之間的切換時需要從用戶態轉換到內核態,這個狀態之間的轉換需要相對比較長的時間,甚至超過代碼本身執行所需要的時間,沒經過優化之前的synchronized關鍵字是一種重量級鎖,因爲使用它會加鎖解鎖的耗時長,且會導致線程之間的頻繁切換,吞吐量較差。而經過自旋鎖等鎖優化機制減少加鎖解鎖、減少線程切換帶來了吞吐量的很大提高。

鎖優化

JDK1.6以及之後,虛擬機加入了自適應自旋鎖、鎖消除、鎖粗化、偏向鎖、輕量級鎖等鎖優化技術。經過鎖優化,鎖的狀態總共有四種,無鎖狀態、偏向鎖、輕量級鎖和重量級鎖。隨着線程對對象的競爭加劇,鎖會從無鎖狀態逐漸升級到重量級鎖,這個過程是單向的,只會從低級到高級(或者回到未鎖定狀態)。虛擬機通過加入這樣的鎖升級技術,在低競爭度時,用偏向鎖和輕量級鎖替代重量級鎖,避免了不必要的資源消耗。

自旋鎖與自適應自旋鎖

自旋鎖使用CAS實現,核心思想是減少線程切換。當多個線程爭奪同一個對象時,一個線程佔有對象鎖,其它線程得阻塞,當擁有者線程使用完畢,則喚醒某個線程。掛起和恢復線程都需要轉入到內核態完成,用戶態和內核態的切換等操作很大的降低了系統的併發性能。線程對共享數據(對象)的鎖定往往只會持續很短一段時間,因此爲了少等這點時間而去掛起、恢復線程並不划算;因此我們可以讓請求鎖的第一個線程,進行一個自旋(忙循環),來等待鎖的釋放,這就是自旋鎖。(JDK1.4自旋鎖出現,但默認關閉,JDK1.6默認開啓)

自旋鎖目的是爲了避免線程切換的開銷,但天下沒有免費的午餐,自旋鎖自旋時需要佔用CPU的時間,且多線程執行時,若多個線程自旋,會對CPU的數量有要求。若鎖長時間不釋放,自旋鎖不可能一直自旋,所以自旋超過一定次數仍然沒有獲得鎖,則掛起該線程,自旋次數默認值爲10。對象鎖被佔用的時間短,則自旋等待的效果就非常好,但反之,對象鎖長時間得不到釋放,則自旋的線程就在白白的消耗CPU資源,帶來了額外的浪費。

JDK1.6引入了自適應的自旋鎖,自適應意味着自旋的時間不再固定,而是由前一次在同一個鎖上的自旋時間以及鎖的擁有者的狀態來決定。比如,在B線程請求對象x鎖之前,有線程剛剛自旋成功獲得鎖,則認爲B線程很有可能自旋成功獲得該對象鎖,給予更多的自旋時間;反之,若對於該對象,很少自旋成功,則減少自旋時間甚至省略自旋過程。有了自適應自旋鎖,隨着程序運行和性能監控信息的不斷完善,虛擬機對程序鎖的狀況預測會越來越準確,虛擬機會變得越來越聰明。

應用場景: 自旋鎖往往應用於線程獲取輕量級鎖失敗之後,通過自旋來等待獲取輕量級鎖,若還是失敗則阻塞線程,此時多條線程爭奪一個對象鎖,該對象鎖膨脹爲重量級鎖。

輕量級鎖升級條件: 在JDK1.6之前,某個線程的自旋次數超過設定的閾值時(JVM可調優)或者等待的自旋的線程數(JVM可調優)超過了CPU核數的二分之一時會升級爲重量級鎖。在JDK1.6之後,出現了自適應自旋,JVM根據運行的情況和每個線程運行的情況決定要不要升級。

鎖消除

鎖消除是指虛擬機即時編譯器在運行時,對一些代碼要求同步,但是被檢測到不可能存在共享數據競爭的鎖進行消除。鎖消除的主要判斷依據是逃逸分析技術。若一段代碼中,該堆上的所有對象都不可能發生線程逃逸,不會被其他線程訪問到,那就可以把它們當作棧上數據對待,認爲它們是線程私有的,因此無需進行同步加鎖,此時就無視同步直接執行代碼。

數據是否逃逸,程序員本身應該十分清楚,那我們自己就可以避免不必要的同步,鎖消除優化豈不是多此一舉。其實並不是,在JAVA中同步措施十分的多,很多時候會進行隱式加鎖,例如JDK1.5之前的String類的+以及vector、StringBuffer等線程安全的容器。

//此處StringBuffer爲線程安全的可變字符串類,但由於這個方法中sb對象不會逃逸到方法之外,更不會發生線程逃逸,
//因此此處進行了同步消除
    public String  concatString(String s,String s1){
        StringBuffer sb=new StringBuffer();
        for(int i=0;i<10;i++)
        sb.append(s).append(s1);
        return sb.toString();
    }

鎖粗化

原則上,編碼時,需要讓同步塊的作用範圍儘可能小,只在共享數據的實際作用域中才進行同步,這樣做的目的是爲了使需要同步的操作數量儘可能縮小,如果存在鎖競爭,那麼等待鎖的線程也能儘快拿到鎖。
在大多數的情況下,上述觀點是正確的。但是如果一系列的連續加鎖解鎖操作,可能會導致不必要的性能損耗,所以引入鎖粗話的概念。
鎖粗化概念比較好理解,如果虛擬機檢測到有一串零碎的操作都是對同一個對象就行加鎖,就會將多個連續的加鎖、解鎖操作連接在一起,擴展成一個範圍更大的鎖,以避免頻繁的加鎖解鎖。例如上面中的StringBuffer對象的append()操作,當虛擬機檢測到一串對sb對象的加鎖,就將鎖的範圍粗化到第一個append()之前,以及最後一個append()之後,只需要一次加鎖即可。

偏向鎖

偏向鎖的目的是在無競爭情況,消除同步操作,提高程序的運行性能。偏向鎖偏向第一個獲取對象鎖的線程,在接下來的執行過程,只要該對象鎖沒被其它線程獲取,則持有對象鎖的線程永遠不需要同步。當鎖對象第一次被線程獲取,對象進入偏向模式,通過CAS操作把獲得鎖的線程ID記錄在Mark Word中,CAS操作成功之後,該線程獲得偏向鎖。當有另外一個線程嘗試獲取該鎖(無論是否成功獲取),偏向模式就結束了,會變成未鎖定或者輕量級鎖狀態。偏向鎖通過消除同步以此省去了線程每次申請鎖消去鎖的步驟,可以提高帶有同步但無競爭的程序性能,但對於鎖競爭比較激烈的場合,偏向鎖就失效了,因爲這樣場合極有可能每次申請鎖的線程都是不相同的,偏向模式就是多餘的,其中的CAS操作以及鎖撤銷操作會帶來時耗,反而降低了性能。偏向鎖適用於存在同步但一直只有一個線程在使用的情況。偏向鎖在Java 1.6之後是默認啓用的,但在應用程序啓動幾秒鐘之後才激活,可以使用-XX:BiasedLockingStartupDelay=0參數關閉延遲,如果確定應用程序中所有鎖通常情況下處於競爭狀態,可以通過XX:-UseBiasedLocking=false參數關閉偏向鎖,避免偏向鎖撤銷帶來開銷。(只需要在第一次進入偏向模式的時候進行CAS操作即可,之後處於偏向模式,可不進行CAS直接進入代碼塊)

加鎖過程:
1、獲取對象的對象頭中的Mark Word;
2、判斷mark是否爲可偏向狀態,即mark的偏向鎖標誌位爲 1,鎖標誌位爲 01;
3、判斷mark中JavaThread的狀態:如果爲空,則進入步驟(4);如果指向當前線程,則執行同步代碼塊;如果指向其它線程,進入步驟(5);
4、通過CAS原子指令設置Mark Word中JavaThread爲當前線程ID,如果執行CAS成功,則執行同步代碼塊,否則進入步驟(5);
5、如果執行CAS失敗,表示當前存在多個線程競爭鎖,當達到全局安全點(safepoint),獲得偏向鎖的線程被掛起,撤銷偏向鎖,並升級爲輕量級,升級完成後被阻塞在安全點的線程繼續執行同步代碼塊;

輕量級鎖

輕量級鎖的目的是在競爭不劇烈的情況下,用CAS操作來替代互斥量的使用,減少傳統的重量級鎖的使用操作系統互斥量產生性能消耗。

加鎖過程:
1、判斷對象當前是否是未鎖定狀態,若是則轉到2,否則轉到3
2、當對象處於不可偏向時,對象沒有被鎖定,線程去嘗試獲取該鎖,虛擬機會在當前線程的棧幀中建立一個名爲鎖記錄(Lock Record)的空間,用於存儲對象目前的Mark Word的拷貝,官方稱作Displaced Mark Word,然後虛擬機將使用CAS操作嘗試將對象的Mark Word更新爲指向Lock Record的指針,更新成功後,就表明線程擁有了該對象的鎖,鎖標識位變成00(Mark Word的最後兩位),此時對象處於輕量級鎖定狀態。
3、對象鎖定,虛擬機就會檢查對象的Mark Word是否指向當前線程的棧幀,若是,則直接進入同步塊繼續執行,若不是則說明這個對象鎖被其它線程搶佔。此時搶奪者線程會進行自旋等待佔有者線程釋放鎖,若自旋等待成功,則還是輕量級鎖,否則該鎖就膨脹位重量級鎖,鎖標誌變成10,此時MarkWord存儲的就是指向Monitor的指針,搶奪者線程進入阻塞狀態。
釋放鎖過程:
1、用CAS操作將線程棧中的Displaced Mark Word替換當前對象的Mark Word中,如果成功,則說明釋放鎖成功。
2、如果CAS操作替換失敗,說明有其他線程嘗試獲取該鎖,則需要在釋放鎖的同時需要喚醒被掛起的線程。
(每次線程去獲得鎖時,都需要用進行CAS操作,再進入同步塊)
在這裏插入圖片描述
(輕量級鎖CAS操作之前堆棧與對象的狀態)
在這裏插入圖片描述
(輕量級鎖CAS操作之後堆棧與對象的狀態)

輕量級鎖升級條件: 在JDK1.6之前,某個線程的自旋次數超過設定的閾值時(JVM可調優)或者等待的自旋的線程數(JVM可調優)超過了CPU核數的二分之一時會升級爲重量級鎖。在JDK1.6之後,出現了自適應自旋,JVM根據運行的情況和每個線程運行的情況決定要不要升級。

輕量級鎖能夠提升程序性能的依據是“對絕大部分的鎖,在整個同步週期內都不存在競爭”,注意這是經驗數據。輕量級鎖所適應的場景是線程交替執行同步塊的場合,如果存在同一時間訪問同一鎖的場合,鎖競爭將導致除了互斥量的開銷,還發生了額外的CAS操作,導致輕量鎖比重量級鎖更慢。

重量級鎖

重量級鎖通過對象內部的監視器(monitor)實現,其中monitor的本質是依賴於底層操作系統的Mutex Lock實現,操作系統實現線程之間的切換需要從用戶態到內核態的切換,切換成本非常高。

鎖升級過程

初始狀態,偏向模式關閉

初始狀態,對象沒有被線程的synchronized加鎖。若偏向模式關閉,則此時對象是未鎖定、不可偏向的對象,對象頭後三位標識爲001,當鎖對象第一次被線程獲取,對象直接被輕量鎖鎖定(00);當對象鎖爲輕量級鎖,當有線程去爭奪該對象的鎖時,搶奪者線程會進行自旋等待佔有者線程釋放鎖,若自旋等待成功,則還是輕量級鎖,否則輕量級鎖膨脹爲重量級鎖(10),從此之後該對象就只是重量級鎖(當所有線程都不爭奪鎖,該對象回到未鎖定狀態),儘管只有一個線程在訪問,還是重量級鎖。

初始狀態,偏向模式未關閉

初始狀態,對象沒有被線程的synchronized加鎖。若偏向模式未關閉,則此時對象是未鎖定、未偏向但是可偏向的對象,對象頭後三位標識爲101,當鎖對象第一次被線程獲取,對象進入偏向模式,通過CAS操作把獲得鎖的線程ID記錄在Mark Word中,CAS操作成功之後,該線程獲得偏向鎖,該線程每次進入該鎖相關的同步塊時,虛擬機不進行任何同步操作。此時對象是已偏向的、鎖定的對象,後三位標識還是爲101。當有另外一個線程去嘗試獲得該對象的鎖時(無論是否獲得),該對象的鎖偏向模式結束,此時撤銷偏向,根據對象是否被鎖定,轉化成未被鎖定、不可偏向的對象(001)和被輕量級鎖定的對象(00);當對象鎖爲輕量級鎖,當有線程去爭奪該對象的鎖時,搶奪者線程會進行自旋等待佔有者線程釋放鎖,若自旋等待成功,則還是輕量級鎖,否則輕量級鎖膨脹爲重量級鎖(10),從此之後該對象就只是重量級鎖(當所有線程都不爭奪鎖,該對象回到未鎖定狀態),儘管只有一個線程在訪問,還是重量級鎖。
(這個過程中對象頭的數據結構在變化,過程如下圖)
在這裏插入圖片描述

總結

1、當對象處於重量鎖狀態,我們常說的鎖其實是指的Monitor,常說的鎖存儲在對象中,其實指的是該對象頭的MarkWord中存儲着指向Monitor對象的指針,monitorenter指令 和 ACC_SYNCHRONIZED 標誌的作用只是讓線程去嘗試獲取該對象的Monitor。

2、synchronized在JDK1.6之前實現互斥同步其實是依靠底層的操作系統的Mutex Lock來實現的,但是由於使用Mutex Lock需要將當前線程掛起並從用戶態切換到內核態來執行。用戶態與內核態之間的切換耗能過大,因此synchronized在沒優化前稱作重量級鎖,而輕量級鎖沒有使用Monitor,而是通過CAS操作來使線程獲得對象的鎖(Lock Record),消除了不必要的底層同步,在無競爭的情況下,提高了性能,而偏向鎖其實相當於沒有用到鎖(利用了絕大部分鎖,在同步週期內都是不存在競爭的原理)

3、注意偏向鎖與輕量級鎖的區別,偏向鎖指的是某對象鎖第一次被線程獲取,該對象鎖進入偏向模式,其實這時是單線程訪問數據,十分安全,因此若沒有其它線程訪問對象數據,就一直是單線程訪問,因此不需要同步,消除同步。當另外有別的線程嘗試獲取鎖,偏向線程就結束了,偏向撤銷之後就不可恢復到偏向鎖,而是轉爲未鎖定或者輕量級鎖。偏向鎖消除了同步所以沒有鎖指針,相當於沒有用到同步鎖,輕量級鎖首先得是在不可偏向模式,然後它是用CAS操作替代了互斥量,用Lock Record取代了Monitor,而不是消除了整個同步。偏向鎖適用於該對象一直只有一個線程使用的情況(無競爭),而輕量級鎖適用於線程交替執行的情況(有競爭但無同時劇烈競爭)

4、對象鎖在我的理解其實指的是存儲在Mark Word中的鎖指針,重量鎖中,它存儲的是Monitor初始地址的指針,輕量級鎖中,它存儲的是指向鎖記錄(Lock Record)的指針,而偏向鎖,則相當於沒有用到鎖,對象頭中只存儲了獲得對象的線程ID,因爲此時它不需要同步。

5、各種鎖優化其實都是應用於特定情況,當不滿足該情況,使用該鎖優化反而會帶來額外的性能損耗。偏向鎖適用於大多數對象鎖只被一個線程使用(無競爭);輕量級鎖適用於大多數對象鎖只會被線程交替使用(有競爭,但無同時競爭);自旋鎖適用於對象鎖不會被線程長時間持有。

知識拓展

1、可重入鎖:synchronized和ReentrantLock都是可重入的。即使用synchronized關鍵字和ReentrantLock類實現同步時,當線程擁有該對象鎖,則線程可以隨時利用該對象進入同步塊,無需再次加鎖進入,不會出現把自己鎖死的情況。例如類中有兩個被synchronized關鍵字修飾的方法,分別是methodA()和methodB(),方法methodA()中執行methodB方法,若爲不可重入鎖,則會把自己鎖死,而重入鎖則不會。

    //檢驗synchronized的可重入性
    public synchronized  void methodA(){
        methodB();
    }
    public synchronized  void methodB(){
        System.out.println("methodB執行成功");
    }
    @Override
    public void run(){
        methodA();
    }

//執行結果
methodB執行成功

Process finished with exit code 0

2、CAS操作是一種原子操作,稱作比較並交換(Compare-and-Swap),CAS指令有三個操作數,分別是內存地址V、舊的預期值A、新值B。CAS執行指令時,僅當V存儲的值符合舊的預期值A時,CPU纔會用新值B更新V的值,否則不執行更新。但無論是否更新了V的值,都會返回V的舊值。CAS具有原子性,因此我們可以通過循環做CAS操作來保證線程安全。

3、相比synchronized關鍵字,ReentrantLock增加的高級性能:
1、公平鎖–所謂的公平鎖就是指當多個線程等待同一個鎖時,按時間順序依次獲得鎖,先等待的線程先獲得鎖。ReenTrantLock可以指定是公平鎖還是非公平鎖(默認非公平)。而synchronized只能是非公平鎖。
2、 等待可中斷–當持有鎖的線程長時間不釋放鎖,正在等待的線程可以選擇放棄等待,去處理其他事情,通過lock.lockInterruptibly()來實現這個機制。
3、 鎖綁定多個條件–ReenTrantLock提供了一個Condition(條件)類,用來實現分組有選擇的喚醒需要喚醒的線程們,而不是像synchronized要麼隨機喚醒一個線程要麼喚醒全部線程。

本篇論文參考《深入理解Java虛擬機》這本書,若內容若有錯誤,請幫忙指出,萬分感謝!若有問題歡迎在評論區討論。推薦讀者閱讀以下兩篇博文:
《JVM源碼分析之synchronized實現》
《CAS你以爲你真的懂?》

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