synchronized底層原理詳解


synchronized底層原理詳解#

一、特性##

  1. 原子性:操作整體要么全部完成,要么全部未完成。就是為了保證數據一致,線程安全。

  2. 有序性:程序的執行順序按照代碼的順序執行。一般情況下,虛擬機為了提高執行效率,會對代碼進行指令重排序,運行的順序可能和代碼的順序不一致,結果不變。單線程不會出現問題,多線程有可能出現問題。

    深入理解Java虛擬機中有這么一句話:

    Java程序中天然的有序性可以總結為一句話:如果在本線程內觀察,所有的操作都是有序的;如果在一個線程中觀察另一個線程,所有的操作都是無序的。前半句是指“線程內表現為串行的語義”( Within-Thread As-If-Serial Semantics),后半句是指“指令重排序”現象和“工作內存與主內存同步延遲”現象

    理解:

    怎么在一個線程中?

    用戶的指令都是在線程中執行的。指令執行在哪個線程里,就是“在”哪一個線程。

    什么叫觀察?

    線程的運行通常沒有辦法直接觀察到,一般只能觀察到線程執行的后果,比如內存的改變。於是觀察,多數情況下是指去讀取被修改了內存的值。

    在一個線程中觀察另一個

    在一個線程中執行的指令,去讀取另一個線程修改的變量的值。
    所謂無序,就是說讀取線程中讀取到的變量值發生改變的順序,和修改線程中修改變量的順序,不一定一致。

  3. 可見性:變量的修改對所有的線程都是可見的。

    理解:

    synchronized對一個對象加鎖,這時其他線程都無法操作。當前線程釋放鎖之后,其他線程才能獲取到synchronized里的內容。

二、synchronized和ReentrantLock的區別##

  1. 使用來說,synchronized是Java的關鍵字,對象只有在同步塊或者同步方法中才能調用wait/notify方法。ReentrantLock是JDK1.5之后提供的API層面的鎖,需要主動創建,配合condition的await/signal使用
  2. ReentrantLock比較靈活,可以嘗試獲取鎖、可以鎖多個條件、可以中斷等
  3. ReentrantLock必須手動使用調用獲取鎖和釋放鎖的方法,synchronized由系統調用
  4. ReentrantLock只能用於鎖代碼塊,而synchronized可以修飾靜態方法、實例方法和代碼塊
  5. 性能上說,ReentrantLock略高於synchronized.JDK6及之后,synchronized被優化為無鎖、偏向鎖、輕量級鎖、重量級鎖和GC標記等狀態,在升級為重量級鎖之前,性能還是很好地。
  6. synchronized是悲觀鎖、可重入鎖、非公平鎖,ReentrantLock是樂觀鎖、可重入鎖,可設置為公平鎖或非公平鎖。
  7. 鎖的對象來說,synchronized鎖的是對象,ReentrantLock鎖的是線程,根據進入的線程和int類型的state標識鎖的獲得/爭搶。
  8. 鎖的實現來說,synchronized是在軟件層面依賴JVM實現,而j.u.c.Lock是在硬件層面依賴特殊的CPU指令實現。

三、底層原理

  1. synchronized不論是修飾靜態方法、實例方法或者是代碼塊,最后鎖住的要么是實例化后的對象,要么是一個類。對於修飾一個(靜態/實例)方法時,JVM會在字節碼層面給該方法打上一個ACC_SYNCHRONIZE標識,當有線程訪問這個方法時,都會嘗試去獲取對象的objectMonitor對象鎖,得到鎖的線程才能繼續訪問該方法。修飾代碼塊時,JVM會在字節碼層面給方法塊入口處加monitorenter,出口處添加monitorexit標識,一般出口有兩個,正常出口和異常出口,所以一般1個monitorenter對應2個monitorexit。線程執行到monitorenter處就需要嘗試獲取objectMonitor對象鎖,獲取不到就會一直阻塞,獲取到了才能繼續運行。

     ObjectMonitor() {
     	_header       = NULL;
         _count        = 0; // 記錄個數
         _waiters      = 0,
         _recursions   = 0;
         _object       = NULL;
         _owner        = NULL;
         _WaitSet      = NULL; // 處於wait狀態的線程,會被加入到_WaitSet
         _WaitSetLock  = 0 ;
         _Responsible  = NULL ;
         _succ         = NULL ;
         _cxq          = NULL ;
         FreeNext      = NULL ;
         _EntryList    = NULL ; // 處於等待鎖block狀態的線程,會被加入到該列表
         _SpinFreq     = 0 ;
         _SpinClock    = 0 ;
         OwnerIsThread = 0 ;
     }
    
  2. 在JDK6之后,鎖被優化為無鎖、偏向鎖、輕量級鎖和重量級鎖。在編譯過程中有鎖粗化,鎖消除,在運行時有鎖升級。

    1. 鎖粗化:如果虛擬機探測到有一系列的連續操作都對同一個對象加鎖,甚至加鎖操作出現在循環中,那么將會把加鎖同步范圍擴展到整個操作的外部,這就是鎖粗化。
    2. 鎖消除:經過逃逸分析后,發現同步代碼塊不可能存在共享數據競爭的情況,那么就會將鎖消除。逃逸分析,主要是分析對象的動態作用范圍,比如在一個方法里一個對象創建后,在調用外部方法時,該對象作為參數傳遞到其他方法中,成為方法逃逸;當被其他線程訪問,如賦值給其他線程中的實例變量,則成為線程逃逸。
    3. 鎖升級:JD6之后分為無鎖,偏向鎖,輕量級鎖,重量級鎖。其中偏向鎖->輕量級鎖->重量級鎖的升級過程不可逆。

    一句話概括偏向鎖、輕量級鎖、重量級鎖

    偏向鎖:當一個線程第一次獲取到鎖之后,再次申請就可以直接取到鎖

    輕量級鎖:沒有多線程競爭,但有多個線程交替執行

    重量級鎖:有多線程競爭,線程獲取不到鎖進入阻塞狀態

    Java對象的內存結構在64位操作系統中,占16個字節:分為對象頭、實例數據、對齊填充

    對象頭占12個字節,實例數據+對齊填充占4個字節,實例數據如果不足4個字節,才會有對齊填充

    對象頭分markword和classAddressMethod,其中markword占8個字節

    avatar

    鎖升級過程:

    無鎖升級為偏向鎖

    1. 線程訪問同步代碼塊,判斷鎖標識位(01)
    2. 判斷是否偏向鎖
    3. 否,CAS操作替換線程ID
    4. 成功,獲得偏向鎖

    偏向鎖升級為輕量級鎖

    1. 線程訪問同步代碼塊,判斷鎖標識位(01)
    2. 判斷是否偏向鎖
    3. 是,檢查對象頭的markword中記錄的是否是當前線程ID
    4. 是,獲得偏向鎖
    5. 不是,CAS操作替換線程ID
    6. 成功,獲取偏向鎖
    7. 失敗,線程進入阻塞狀態,等待原持有線程到達安全點
    8. 原持有線程到達安全點,檢查線程狀態
    9. 已退出同步代碼塊,釋放偏向鎖
    10. 未退出代碼塊,升級為偏向鎖,在原持有線程的棧中分配lock record(鎖記錄),拷貝對象頭中的markword到lock record中,對象頭中的markword修改為指向線程中鎖記錄的指針,升級成功
    11. 喚醒線程繼續執行

    輕量級鎖升級為重量級鎖

    1. 線程訪問同步代碼塊,判斷鎖標識位(00)
    2. 判斷是否輕量級鎖
    3. 是,當前線程的棧中分配lock record
    4. 拷貝對象頭中的markword到lock record中
    5. CAS操作嘗試獲取將對象頭中的鎖記錄指針指向當前線程的鎖記錄
    6. 成功,當前線程得到輕量級鎖
    7. 執行代碼塊
    8. 開始輕量級鎖解鎖
    9. CAS操作,判斷對象頭的鎖記錄指針是否仍指向當前線程鎖記錄,拷貝在當前線程鎖記錄的mark word信息與當前線程的鎖記錄指針是否一致
    10. 兩個條件都一致,釋放鎖
    11. 不一致,釋放鎖(鎖已經升級為重量級鎖了),喚醒其他線程
    12. 5失敗,自旋嘗試5、
    13. 自旋過程中成功了,執行6,7,8,9,10,11
    14. 自旋一定次數仍然失敗,升級為重量級鎖


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM