Java synchronized輕量級(jí)鎖的核心原理詳解
問(wèn)題:
什么是自旋鎖?
說(shuō)一下 synchronized 底層實(shí)現(xiàn)原理?
多線(xiàn)程中 synchronized 鎖升級(jí)的原理是什么?
1. 輕量級(jí)鎖的原理
引入輕量級(jí)鎖的主要目的是在多線(xiàn)程競(jìng)爭(zhēng)不激烈的情況下,通過(guò)CAS機(jī)制競(jìng)爭(zhēng)鎖減少重量級(jí)鎖產(chǎn)生的性能損耗。重量級(jí)鎖使用了操作系統(tǒng)底層的互斥鎖(Mutex Lock),會(huì)導(dǎo)致線(xiàn)程在用戶(hù)態(tài)和核心態(tài)之間頻繁切換,從而帶來(lái)較大的性能損耗。
輕量級(jí)鎖的使用場(chǎng)景:如果一個(gè)對(duì)象雖然有多線(xiàn)程要加鎖,但加鎖的時(shí)間是錯(cuò)開(kāi)的(也就是沒(méi)有競(jìng)爭(zhēng)),那么可以 使用輕量級(jí)鎖來(lái)優(yōu)化。
輕量鎖存在的目的是盡可能不動(dòng)用操作系統(tǒng)層面的互斥鎖,因?yàn)槠湫阅鼙容^差。線(xiàn)程的阻塞和喚醒需要CPU從用戶(hù)態(tài)轉(zhuǎn)為核心態(tài),頻繁地阻塞和喚醒對(duì)CPU來(lái)說(shuō)是一件負(fù)擔(dān)很重的工作。同時(shí)我們可以發(fā)現(xiàn),很多對(duì)象鎖的鎖定狀態(tài)只會(huì)持續(xù)很短的一段時(shí)間,例如整數(shù)的自加操作,在很短的時(shí)間內(nèi)阻塞并喚醒線(xiàn)程顯然不值得,為此引入了輕量級(jí)鎖。輕量級(jí)鎖是一種自旋鎖,因?yàn)镴VM本身就是一個(gè)應(yīng)用,所以希望在應(yīng)用層面上通過(guò)自旋解決線(xiàn)程同步問(wèn)題。
public class Main { static final Object obj = new Object(); public static void main(String[] args) { Thread thread = new Thread(()->{ method1(); }); thread.start(); } public static void method1() { synchronized( obj ) { // 同步塊 A method2(); } } public static void method2() { synchronized( obj ) { // 同步塊 B } } }
輕量級(jí)鎖的執(zhí)行過(guò)程:
在搶鎖線(xiàn)程進(jìn)入臨界區(qū)之前,如果內(nèi)置鎖沒(méi)有被鎖定,JVM首先將在搶鎖線(xiàn)程的棧幀中建立一個(gè)鎖記錄(Lock Record
),用于存儲(chǔ)對(duì)象Mark Word的拷貝,
然后搶鎖線(xiàn)程將使用CAS自旋操作,嘗試將內(nèi)置鎖對(duì)象頭的Mark Word
的ptr_to_lock_record
(鎖記錄指針)更新為搶鎖線(xiàn)程棧幀中鎖記錄的地址,如果這個(gè)更新執(zhí)行成功了,這個(gè)線(xiàn)程就擁有了這個(gè)對(duì)象鎖。然后JVM將Mark Word中的lock標(biāo)記位改為00(輕量級(jí)鎖標(biāo)志),即表示該對(duì)象處于輕量級(jí)鎖狀態(tài)。搶鎖成功之后,JVM會(huì)將Mark Word中原來(lái)的鎖對(duì)象信息(如哈希碼等)保存在搶鎖線(xiàn)程鎖記錄的Displaced Mark Word
(可以理解為放錯(cuò)地方的Mark Word)字段中,再將搶鎖線(xiàn)程中鎖記錄的owner指針指向鎖對(duì)象。
鎖記錄是線(xiàn)程私有的,每個(gè)線(xiàn)程都有自己的一份鎖記錄,在創(chuàng)建完鎖記錄后,會(huì)將內(nèi)置鎖對(duì)象的Mark Word復(fù)制到鎖記錄的Displaced Mark Word
字段。這是為什么呢?因?yàn)閮?nèi)置鎖對(duì)象的MarkWord的結(jié)構(gòu)會(huì)有所變化,Mark Word將會(huì)出現(xiàn)一個(gè)指向鎖記錄的指針,而不再存著無(wú)鎖狀態(tài)下的鎖對(duì)象哈希碼等信息,所以必須將這些信息暫存起來(lái),供后面在鎖釋放時(shí)使用。
(1) 在搶鎖線(xiàn)程進(jìn)入臨界區(qū)之前,如果內(nèi)置鎖沒(méi)有被鎖定,JVM首先將在搶鎖線(xiàn)程的棧幀中建立一個(gè)鎖記錄(Lock Record
),每個(gè)線(xiàn)程都的棧幀都會(huì)包含一個(gè)鎖記錄的結(jié)構(gòu),內(nèi)部可以存儲(chǔ)鎖定對(duì)象的mark word
(2) 搶鎖線(xiàn)程將使用CAS自旋操作,嘗試將內(nèi)置鎖對(duì)象頭的mark word的ptr_to_lock_record(
鎖記錄指針)更新為搶鎖線(xiàn)程棧幀中鎖記錄的地址,如果這個(gè)更新執(zhí)行成功了,這個(gè)線(xiàn)程就擁有了這個(gè)對(duì)象鎖。然后jvm將mark word中的lock標(biāo)記位改為00,即表示該對(duì)象處于輕量級(jí)鎖狀態(tài)。
搶鎖成功之后,jvm會(huì)將mark word中原來(lái)的鎖對(duì)象信息(如哈希碼等)保存在搶鎖線(xiàn)程鎖記錄的Displaced Mark Word字段中,再將搶鎖線(xiàn)程中鎖記錄的owner指針指向鎖對(duì)象。
64位的mark word結(jié)構(gòu)如表所示:
在輕量級(jí)鎖搶占成功之后,鎖記錄和對(duì)象頭的狀態(tài)如圖所示:
鎖記錄是線(xiàn)程私有的,每個(gè)線(xiàn)程都有自己的一份鎖記錄,在創(chuàng)建完鎖記錄后,會(huì)將內(nèi)置鎖對(duì)象的Mark Word復(fù)制到鎖記錄的Displaced Mark Word字段。這是為什么呢?因?yàn)閮?nèi)置鎖對(duì)象的mark word的結(jié)構(gòu)會(huì)有所變化,mark word將會(huì)出現(xiàn)一個(gè)指向鎖記錄的指針,而不再存著無(wú)鎖狀態(tài)下的鎖對(duì)象哈希碼等信息,所以必須將這些信息暫存起來(lái),供后面在鎖釋放時(shí)使用。
(3) 如果 cas 失敗,有兩種情況:
- 如果是其它線(xiàn)程已經(jīng)持有了該 Object 的輕量級(jí)鎖,這時(shí)表明有競(jìng)爭(zhēng),進(jìn)入鎖膨脹過(guò)程 ;
- 如果是自己執(zhí)行了 synchronized 鎖重入,那么再添加一條 Lock Record 作為重入的計(jì)數(shù);
(4) 當(dāng)退出 synchronized 代碼塊(解鎖時(shí))如果有取值為 null 的鎖記錄,表示有重入,這時(shí)重置鎖記錄,表示重入計(jì)數(shù)減一
(5) 當(dāng)退出 synchronized 代碼塊(解鎖時(shí))鎖記錄的值不為 null,這時(shí)使用 cas 將 mark word的值恢復(fù)給對(duì)象頭
成功,則解鎖成功失敗,說(shuō)明輕量級(jí)鎖進(jìn)行了鎖膨脹或已經(jīng)升級(jí)為重量級(jí)鎖,進(jìn)入重量級(jí)鎖解鎖流程
2. 輕量級(jí)鎖的分類(lèi)
輕量級(jí)鎖主要有兩種:普通自旋鎖和自適應(yīng)自旋鎖。
1、普通自旋鎖
所謂普通自旋鎖,就是指當(dāng)有線(xiàn)程來(lái)競(jìng)爭(zhēng)鎖時(shí),搶鎖線(xiàn)程會(huì)在原地循環(huán)等待,而不是被阻塞,直到那個(gè)占有鎖的線(xiàn)程釋放鎖之后,這個(gè)搶鎖線(xiàn)程才可以獲得鎖。
說(shuō)明:
鎖在原地循環(huán)等待的時(shí)候是會(huì)消耗CPU的,就相當(dāng)于在執(zhí)行一個(gè)什么也不干的空循環(huán)。所以輕量級(jí)鎖適用于臨界區(qū)代碼耗時(shí)很短的場(chǎng)景,這樣線(xiàn)程在原地等待很短的時(shí)間就能夠獲得鎖了。默認(rèn)情況下,自旋的次數(shù)為10次,用戶(hù)可以通過(guò)-XX:PreBlockSpin
選項(xiàng)來(lái)進(jìn)行更改。
2、自適應(yīng)自旋鎖
所謂自適應(yīng)自旋鎖,就是等待線(xiàn)程空循環(huán)的自旋次數(shù)并非是固定的,而是會(huì)動(dòng)態(tài)地根據(jù)實(shí)際情況來(lái)改變自旋等待的次數(shù),自旋次數(shù)由前一次在同一個(gè)鎖上的自旋時(shí)間及鎖的擁有者的狀態(tài)來(lái)決定。自適應(yīng)自旋鎖的大概原理是:
- 如果搶鎖線(xiàn)程在同一個(gè)鎖對(duì)象上之前成功獲得過(guò)鎖,jvm就會(huì)認(rèn)為這次自旋很有可能再次成功,因此允許自旋等待持續(xù)相對(duì)更長(zhǎng)的時(shí)間。
- 如果對(duì)于某個(gè)鎖,搶鎖線(xiàn)程很少成功獲得過(guò),那么jvm將可能減少自旋時(shí)間甚至省略自旋過(guò)程,以避免浪費(fèi)處理器資源。
自適應(yīng)自旋解決的是“鎖競(jìng)爭(zhēng)時(shí)間不確定”的問(wèn)題。自適應(yīng)自旋假定不同線(xiàn)程持有同一個(gè)鎖對(duì)象的時(shí)間基本相當(dāng),競(jìng)爭(zhēng)程度趨于穩(wěn)定??偟乃枷胧牵焊鶕?jù)上一次自旋的時(shí)間與結(jié)果調(diào)整下一次自旋的時(shí)間。
JDK 1.6的輕量級(jí)鎖使用的是普通自旋鎖,且需要使用-XX:+UseSpinning
選項(xiàng)手工開(kāi)啟。
JDK 1.7后,輕量級(jí)鎖使用自適應(yīng)自旋鎖,JVM
啟動(dòng)時(shí)自動(dòng)開(kāi)啟,且自旋時(shí)間由JVM
自動(dòng)控制。
輕量級(jí)鎖也被稱(chēng)為非阻塞同步、樂(lè)觀鎖,因?yàn)檫@個(gè)過(guò)程并沒(méi)有把線(xiàn)程阻塞掛起,而是讓線(xiàn)程空循環(huán)等待。
3. 輕量級(jí)鎖的膨脹
輕量級(jí)鎖的問(wèn)題在哪里呢?
雖然大部分臨界區(qū)代碼的執(zhí)行時(shí)間都是很短的,但是也會(huì)存在執(zhí)行得很慢的臨界區(qū)代碼。臨界區(qū)代碼執(zhí)行耗時(shí)較長(zhǎng),在其執(zhí)行期間,其他線(xiàn)程都在原地自旋等待,會(huì)空消耗CPU。因此,如果競(jìng)爭(zhēng)這個(gè)同步鎖的線(xiàn)程很多,就會(huì)有多個(gè)線(xiàn)程在原地等待繼續(xù)空循環(huán)消耗CPU(空自旋),這會(huì)帶來(lái)很大的性能損耗。
輕量級(jí)鎖的本意是為了減少多線(xiàn)程進(jìn)入操作系統(tǒng)底層的互斥鎖的概率,并不是要替代操作系統(tǒng)互斥鎖。所以,在爭(zhēng)用激烈的場(chǎng)景下,輕量級(jí)鎖會(huì)膨脹為基于操作系統(tǒng)內(nèi)核互斥鎖實(shí)現(xiàn)的重量級(jí)鎖。
如果在嘗試加輕量級(jí)鎖的過(guò)程中,CAS 操作無(wú)法成功,這時(shí)一種情況就是有其它線(xiàn)程為此對(duì)象加上了輕量級(jí)鎖(有 競(jìng)爭(zhēng)),這時(shí)需要進(jìn)行鎖膨脹,將輕量級(jí)鎖變?yōu)橹亓考?jí)鎖。
(1) 當(dāng) Thread-1 進(jìn)行輕量級(jí)加鎖時(shí),Thread-0 已經(jīng)對(duì)該對(duì)象加了輕量級(jí)鎖
這時(shí) Thread-1 加輕量級(jí)鎖失敗,進(jìn)入鎖膨脹流程,即為鎖對(duì)象申請(qǐng) Monitor 鎖,讓鎖對(duì)象指向重量級(jí)鎖地址,然后自己進(jìn)入 Monitor 的 EntryList BLOCKED
當(dāng) Thread-0 退出同步塊解鎖時(shí),使用 cas 將mark word的值恢復(fù)給對(duì)象頭,失敗。這時(shí)會(huì)進(jìn)入重量級(jí)解鎖 流程,即按照 Monitor 地址找到 Monitor 對(duì)象,設(shè)置 Owner 為 null,喚醒 EntryList 中 BLOCKED 線(xiàn)程。
總結(jié)
本篇文章就到這里了,希望能夠給你帶來(lái)幫助,也希望您能夠多多關(guān)注腳本之家的更多內(nèi)容!
- Java面試synchronized偏向鎖后hashcode存址
- Java同步鎖Synchronized底層源碼和原理剖析(推薦)
- Java中synchronized鎖升級(jí)的過(guò)程
- Java中的synchronized?優(yōu)化方法之鎖膨脹機(jī)制
- Java常用鎖synchronized和ReentrantLock的區(qū)別
- Java對(duì)象級(jí)別與類(lèi)級(jí)別的同步鎖synchronized語(yǔ)法示例
- 深入了解Java?Synchronized鎖升級(jí)過(guò)程
- Java synchronized偏向鎖的核心原理詳解
- Java關(guān)鍵字synchronized原理與鎖的狀態(tài)詳解
相關(guān)文章
SpringBoot項(xiàng)目加入沖突動(dòng)態(tài)監(jiān)測(cè)算法的實(shí)現(xiàn)
沖突動(dòng)態(tài)監(jiān)測(cè)算法是一種網(wǎng)絡(luò)通信中的沖突檢測(cè)方法,適用于無(wú)線(xiàn)網(wǎng)絡(luò)或其他共享傳輸介質(zhì)的環(huán)境,本文主要介紹了SpringBoot項(xiàng)目加入沖突動(dòng)態(tài)監(jiān)測(cè)算法的實(shí)現(xiàn),感興趣的可以了解一下2023-09-09Springboot使用POI實(shí)現(xiàn)導(dǎo)出Excel文件示例
本篇文章主要介紹了Springboot使用POI實(shí)現(xiàn)導(dǎo)出Excel文件示例,非常具有實(shí)用價(jià)值,需要的朋友可以參考下。2017-02-02使用IDEA搭建Hadoop開(kāi)發(fā)環(huán)境的操作步驟(Window10為例)
經(jīng)過(guò)三次重裝,查閱無(wú)數(shù)資料后成功完成hadoop在win10上實(shí)現(xiàn)偽分布式集群,以及IDEA開(kāi)發(fā)環(huán)境的搭建。一步一步跟著本文操作可以避免無(wú)數(shù)天坑2021-07-07Spring Security 中如何讓上級(jí)擁有下級(jí)的所有權(quán)限(案例分析)
這篇文章主要介紹了Spring Security 中如何讓上級(jí)擁有下級(jí)的所有權(quán)限,本文通過(guò)案例分析給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-09-09關(guān)于SpringMVC中數(shù)據(jù)綁定@ModelAttribute注解的使用
這篇文章主要介紹了關(guān)于SpringMVC中數(shù)據(jù)綁定@ModelAttribute注解的使用,SpringMVC是一個(gè)基于Spring框架的Web框架,它提供了一種簡(jiǎn)單、靈活的方式來(lái)開(kāi)發(fā)Web應(yīng)用程序,在開(kāi)發(fā)Web應(yīng)用程序時(shí),我們需要將用戶(hù)提交的數(shù)據(jù)綁定到我們的Java對(duì)象上,需要的朋友可以參考下2023-07-07新的Java訪問(wèn)mysql數(shù)據(jù)庫(kù)工具類(lèi)的操作代碼
本文通過(guò)實(shí)例代碼給大家介紹新的Java訪問(wèn)mysql數(shù)據(jù)庫(kù)工具類(lèi)的方法,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2021-12-12SpringBoot定時(shí)任務(wù)參數(shù)運(yùn)行代碼實(shí)例解析
這篇文章主要介紹了SpringBoot定時(shí)任務(wù)運(yùn)行代碼實(shí)例解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-06-06