詳解JUC并發(fā)編程之鎖
當(dāng)多個(gè)線程訪問一個(gè)對(duì)象時(shí),如果不用考慮這些線程在運(yùn)行環(huán)境下的調(diào)度和交替執(zhí)行,也不需要進(jìn)行額外的同步,或者在調(diào)用方進(jìn)行任何其他的協(xié)調(diào)操作,調(diào)用這個(gè)對(duì)象的行為都可以獲得正確的結(jié)果,那么這個(gè)對(duì)象就是線程安全的。但是現(xiàn)實(shí)并不是這樣子的,所以JVM實(shí)現(xiàn)了鎖機(jī)制,今天就叭叭叭JAVA中各種各樣的鎖。
1、自旋鎖和自適應(yīng)鎖
自旋鎖:在多線程競(jìng)爭(zhēng)的狀態(tài)下共享數(shù)據(jù)的鎖定狀態(tài)只會(huì)持續(xù)很短的一段時(shí)間,為了這段時(shí)間去掛起和恢復(fù)阻塞線程并不值得,而是讓沒有獲取到鎖的線程自旋(自旋并不會(huì)放棄CPU的分片時(shí)間)等待當(dāng)前線程釋放鎖,如果自旋超過了限定的次數(shù)仍然沒有成功獲取到鎖,就應(yīng)該使用傳統(tǒng)的方式去掛起線程了,在JDK定義中,自旋鎖默認(rèn)的自旋次數(shù)為10次,用戶可以使用參數(shù)-XX:PreBlockSpin來(lái)更改(jdk1.6之后默認(rèn)開啟自旋鎖)。
自適應(yīng)鎖:為了解決某些特殊情況,如果自旋剛結(jié)束,線程就釋放了鎖,那么是不是有點(diǎn)不劃算。自適應(yīng)自旋鎖是jdk1.6引入,規(guī)定自旋的時(shí)間不再固定了,而是由前一次在同一個(gè)鎖上的自旋 時(shí)間及鎖的擁有者的狀態(tài)來(lái)決定的。如果在同一個(gè)鎖對(duì)象上,自旋等待剛剛成功獲取過鎖,并且持有鎖的線程正在運(yùn)行中,那么JVM會(huì)認(rèn)為該線程自旋獲取到鎖的可能性很大,會(huì)自動(dòng)增加等待時(shí)間。反之就認(rèn)為不容易獲取到鎖,而放棄自旋這種方式。
鎖消除:鎖消除時(shí)指虛擬機(jī)即時(shí)編譯器在運(yùn)行時(shí),對(duì)一些代碼上要求同步,但是被檢測(cè)到不可能存在共享數(shù)據(jù)競(jìng)爭(zhēng)的鎖進(jìn)行消除。意思就是:在一段代碼中,堆上的所有數(shù)據(jù)都不會(huì)逃逸出去而被其他線程訪問到那就可以把他們當(dāng)作棧上的數(shù)據(jù)對(duì)待,認(rèn)為他們是線程私有的,不用再加鎖。
鎖粗化:
public static void main(String[] args) { StringBuffer buffer = new StringBuffer(); buffer.append("a"); buffer.append("b"); buffer.append("c"); System.out.println("拼接之后的結(jié)果是:>>>>>>>>>>>"+buffer); }
@Override @IntrinsicCandidate public synchronized StringBuffer append(String str) { toStringCache = null; super.append(str); return this; }
StringBuffer 在拼接字符串時(shí)是同步的。但是在一系列的操作中都對(duì)同一個(gè)對(duì)象(StringBuffer )反復(fù)加鎖和解鎖,頻繁的進(jìn)行加鎖解鎖操作會(huì)導(dǎo)致不必要的性能損耗,JVM會(huì)將加鎖同步的范圍擴(kuò)展到整個(gè)操作的外部,只加一次鎖。
2、輕量級(jí)鎖和重量級(jí)鎖
這種鎖實(shí)現(xiàn)的背后基于這樣一種假設(shè),即在真實(shí)的情況下我們程序中的大部分同步代碼一般都處于無(wú)鎖競(jìng)爭(zhēng)狀態(tài)(即單線程執(zhí)行環(huán)境),在無(wú)鎖競(jìng)爭(zhēng)的情況下完全可以避免調(diào)用操作系統(tǒng)層面的重量級(jí)互斥鎖, 取而代之的是在monitorenter和monitorexit中只需要依靠一條CAS原子指令就可以完成鎖的獲取及釋放。輕量級(jí)鎖是相對(duì)于重量級(jí)鎖而言的。
輕量級(jí)鎖加鎖過程
在HotSpot虛擬機(jī)的對(duì)象頭分為兩部分,一部分用于存儲(chǔ)對(duì)象自身的運(yùn)行時(shí)數(shù)據(jù),如Hashcode、GC分代年齡、標(biāo)志位等,這部分長(zhǎng)度在32位和64位的虛擬機(jī)中分別是32bit和64bit,稱為Mark Word。另一部分用于存儲(chǔ)指向方法區(qū)對(duì)象類型數(shù)據(jù)的指針,如果是數(shù)組對(duì)象的話,還會(huì)有一個(gè)額外的部分用于存儲(chǔ)數(shù)組長(zhǎng)度。
對(duì)象頭信息是與對(duì)象自身定義的數(shù)據(jù)無(wú)關(guān)的額外存儲(chǔ)成本,Mark Word 被設(shè)計(jì)成一個(gè)非固定的數(shù)據(jù)結(jié)構(gòu)以便在極小的空間內(nèi)存儲(chǔ)盡量多的信息。mark word中有兩個(gè)bit存儲(chǔ)鎖標(biāo)記位。
HotSpot虛擬機(jī)對(duì)象頭Mark Word
存儲(chǔ)內(nèi)容 | 標(biāo)志位 | 狀態(tài) |
對(duì)象哈希碼,分代年齡 | 01 | 無(wú)鎖 |
指向鎖記錄的指針 | 00 | 輕量級(jí)鎖 |
指向重量級(jí)鎖的指針 | 10 | 膨脹重量級(jí)鎖 |
空,不需要記錄信息 | 11 | GC標(biāo)記 |
偏向線程id,偏向時(shí)間戳,對(duì)象分代年齡 | 01 | 可偏向 |
在代碼進(jìn)入同步代碼塊時(shí),如果此對(duì)象沒有被鎖定(標(biāo)記位為01狀態(tài)),虛擬機(jī)首先在當(dāng)前線程的棧幀建立一個(gè)名為鎖記錄(Lock Record)的空間,用于存儲(chǔ)鎖對(duì)象目前Mark Word的拷貝,然后虛擬機(jī)使用CAS操作嘗試將對(duì)象的Mark Word 更新為指向Lock Record的指針,如果操作成功了,那么這個(gè)線程就有了這個(gè)對(duì)象的鎖,并且將Mark Word 的標(biāo)記位更改為00,表示這個(gè)對(duì)象處于輕量級(jí)鎖定狀態(tài)。如果更新失敗了虛擬機(jī)會(huì)首先檢查是否是當(dāng)前線程擁有了這個(gè)對(duì)象的鎖,如果是就進(jìn)入同步代碼,如果不是,那就說明鎖被其他線程占用了。如果有兩個(gè)以上的線程爭(zhēng)奪同一個(gè)鎖,那輕量級(jí)鎖就不再有效,要膨脹為重量級(jí)鎖,鎖標(biāo)記位變?yōu)?0,后面等待的線程就要進(jìn)入阻塞狀態(tài)。
輕量級(jí)鎖解鎖過程
解鎖過程同樣使用CAS操作來(lái)進(jìn)行,使用CAS操作將Mark Word 指向Lock Record 指針釋放,如果操作成功,那么整個(gè)同步過程就完成了,如果釋放失敗,說明有其他線程嘗試獲取該鎖,那就在釋放鎖的同時(shí),喚醒被掛起的線程。
3、偏向鎖
JVM 參數(shù) -XX:-UseBiasedLocking 禁用偏向鎖;-XX:+UseBiasedLocking 啟用偏向鎖。
啟用了偏向鎖才會(huì)執(zhí)行偏向鎖的操作。當(dāng)鎖對(duì)象第一次被線程獲取時(shí),虛擬機(jī)會(huì)把對(duì)象頭中的標(biāo)記位設(shè)置為01,偏向模式。同時(shí)使用CAS操作獲取到當(dāng)前線程的線程ID存儲(chǔ)到Mark Word 中,如果操作成功,那么持有偏向鎖的線程以后每次進(jìn)入這個(gè)鎖相關(guān)的同步塊時(shí),都不需要任何操作,直接進(jìn)入。如果有多個(gè)線程去嘗試獲取這個(gè)鎖時(shí),偏向鎖就宣告無(wú)效,然后會(huì)撤銷偏向或者恢復(fù)到未鎖定。然后再膨脹為重量級(jí)鎖,標(biāo)記位狀態(tài)變?yōu)?0。
4、可重入鎖和不可重入鎖
可重入鎖就是一個(gè)線程獲取到鎖之后,在另一個(gè)代碼塊還需要該鎖,那么不需要重新獲取而可以直接使用該鎖。大多數(shù)的鎖都是可重入鎖。但是CAS自旋鎖不可重入。
package com.xiaojie.juc.thread.lock; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** * @author xiaojie * @version 1.0 * @description: 測(cè)試鎖的重入性 * @date 2021/12/30 22:09 */ public class Test01 { public synchronized void a() { System.out.println(Thread.currentThread().getName() + "運(yùn)行a方法"); b(); } private synchronized void b() { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "運(yùn)行b方法"); } public static void main(String[] args) { Test01 test01 = new Test01(); ExecutorService executorService = Executors.newFixedThreadPool(3); for (int i=0;i<10;i++){ executorService.execute(() -> test01.a()); } } }
5、悲觀鎖和樂觀鎖
悲觀鎖總是悲觀的,總是認(rèn)為會(huì)發(fā)生安全問題,所以每次操作都會(huì)加鎖。比如獨(dú)占鎖、傳統(tǒng)數(shù)據(jù)庫(kù)中的行鎖、表鎖、讀鎖、寫鎖等。悲觀鎖存在以下幾個(gè)缺點(diǎn):
- 在多線程競(jìng)爭(zhēng)下,加鎖、釋放鎖會(huì)導(dǎo)致比較多的上下文切換和調(diào)度延遲,引起性能問題。
- 一個(gè)線程占有鎖后,其他線程就得阻塞等待。
- 如果優(yōu)先級(jí)高的線程等待一個(gè)優(yōu)先級(jí)低的線程,會(huì)導(dǎo)致線程優(yōu)先級(jí)導(dǎo)致,可能引發(fā)性能風(fēng)險(xiǎn)。
樂觀鎖總是樂觀的,總是認(rèn)為不會(huì)發(fā)生安全問題。在數(shù)據(jù)庫(kù)中可以使用版本號(hào)實(shí)現(xiàn)樂觀鎖,JAVA中的CAS和一些原子類都是樂觀鎖的思想。
6、公平鎖和非公平鎖
公平鎖:是指多個(gè)線程在等待同一個(gè)鎖時(shí),必須按照申請(qǐng)鎖的時(shí)間順序來(lái)依次獲得鎖。
非公平鎖:非公平鎖不需要按照申請(qǐng)鎖的時(shí)間順序來(lái)獲取鎖,而是誰(shuí)能獲取到CPU的時(shí)間片誰(shuí)就先執(zhí)行。非公平鎖的優(yōu)點(diǎn)是吞吐量比公平鎖大,缺點(diǎn)是有可能導(dǎo)致線程優(yōu)先級(jí)反轉(zhuǎn)或者造成過線程饑餓現(xiàn)象(就是有的線程玩命的一直在執(zhí)行任務(wù),有的線程至死沒有執(zhí)行一個(gè)任務(wù))。
synchronized中的鎖是非公平鎖,ReentrantLock默認(rèn)也是非公平鎖,但是可以通過構(gòu)造函數(shù)設(shè)置為公平鎖。
7、共享鎖和獨(dú)占鎖
共享鎖就是同一時(shí)刻允許多個(gè)線程持有的鎖。例如Semaphore(信號(hào)量)、ReentrantReadWriteLock的讀鎖、CountDownLatch倒數(shù)閂等。
獨(dú)占鎖也叫排它鎖、互斥鎖、獨(dú)占鎖是指鎖在同一時(shí)刻只能被一個(gè)線程所持有。例如synchronized內(nèi)置鎖和ReentrantLock顯示鎖,ReentrantReadWriteLock的寫鎖都是獨(dú)占鎖。
package com.xiaojie.juc.thread.lock; import java.util.concurrent.locks.ReentrantReadWriteLock; /** * @description: 讀寫鎖驗(yàn)證共享鎖和獨(dú)占鎖 * @author xiaojie * @date 2021/12/30 23:28 * @version 1.0 */ public class ReadAndWrite { static class ReadThred extends Thread { private ReentrantReadWriteLock lock; private String name; public ReadThred(String name, ReentrantReadWriteLock lock) { super(name); this.lock = lock; } @Override public void run() { try { lock.readLock().lock(); System.out.println(Thread.currentThread().getName() + "這是共享鎖。。。。。。"); } catch (Exception e) { e.printStackTrace(); } finally { lock.readLock().unlock(); System.out.println(Thread.currentThread().getName() + "釋放鎖成功。。。。。。"); } } } static class WriteThred extends Thread { private ReentrantReadWriteLock lock; private String name; public WriteThred(String name, ReentrantReadWriteLock lock) { super(name); this.lock = lock; } @Override public void run() { try { lock.writeLock().lock(); Thread.sleep(3000); System.out.println(Thread.currentThread().getName() + "這是獨(dú)占鎖。。。。。。。。"); } catch (Exception e) { e.printStackTrace(); } finally { lock.writeLock().unlock(); System.out.println(Thread.currentThread().getName() + "釋放鎖。。。。。。。"); } } } public static void main(String[] args) { ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(); ReadThred readThred1 = new ReadThred("read-thread-1", reentrantReadWriteLock); ReadThred readThred2 = new ReadThred("read-thread-1", reentrantReadWriteLock); WriteThred writeThred1 = new WriteThred("write-thread-1", reentrantReadWriteLock); WriteThred writeThred2 = new WriteThred("write-thread-2", reentrantReadWriteLock); readThred1.start(); readThred2.start(); writeThred1.start(); writeThred2.start(); } }
8、可中斷鎖和不可中斷鎖
可中斷鎖只在搶占鎖的過程中可以被中斷的鎖如ReentrantLock。
不可中斷鎖是不可中斷的鎖如java內(nèi)置鎖synchronized。
總結(jié):
名稱 | 優(yōu)點(diǎn) | 缺點(diǎn) | 使用場(chǎng)景 |
偏向鎖 | 加鎖和解鎖不需要CAS操作,沒有額外的性能消耗,和執(zhí)行非同步方法相比僅存在納秒級(jí)的差距 | 如果線程間存在鎖競(jìng)爭(zhēng),會(huì)帶來(lái)額外的鎖撤銷的消耗 | 適用于只有一個(gè)線程訪問同步快的場(chǎng)景 |
輕量級(jí)鎖 | 競(jìng)爭(zhēng)的線程不會(huì)阻塞,提高了響應(yīng)速度 | 如線程成始終得不到鎖競(jìng)爭(zhēng)的線程,使用自旋會(huì)消耗CPU性能 | 追求響應(yīng)時(shí)間,同步快執(zhí)行速度非???/p> |
重量級(jí)鎖 | 線程競(jìng)爭(zhēng)不適用自旋,不會(huì)消耗CPU | 線程阻塞,響應(yīng)時(shí)間緩慢,在多線程下,頻繁的獲取釋放鎖,會(huì)帶來(lái)巨大的性能消耗 | 追求吞吐量,同步快執(zhí)行速度較長(zhǎng) |
本篇文章就到這里了,希望能夠給你帶來(lái)幫助,也希望您能夠多多關(guān)注腳本之家的更多內(nèi)容!
相關(guān)文章
淺析SpringBoot中使用thymeleaf找不到.HTML文件的原因
這篇文章主要介紹了SpringBoot中使用thymeleaf找不到.HTML文件的原因分析,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-07-07SpringBoot+slf4j線程池全鏈路調(diào)用日志跟蹤問題及解決思路(二)
本文主要給大家介紹如何實(shí)現(xiàn)子線程中的traceId日志跟蹤,本文通過封裝Callable為例給大家介紹的非常詳細(xì),需要的朋友一起看看吧2021-05-05Java編程實(shí)現(xiàn)NBA賽事接口調(diào)用實(shí)例代碼
這篇文章主要介紹了Java編程實(shí)現(xiàn)NBA賽事接口調(diào)用實(shí)例代碼,具有一定參考價(jià)值,需要的朋友可以了解下。2017-11-11SpringSecurity oAuth2.0的四種模式(小結(jié))
本文主要介紹了SpringSecurity oAuth2.0的四種模式,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-02-02MyBatis-Plus條件構(gòu)造器Wrapper應(yīng)用實(shí)例
QueryWrapper是用于查詢的Wrapper條件構(gòu)造器,可以通過它來(lái)構(gòu)建SELECT語(yǔ)句中的WHERE條件,這篇文章主要介紹了MyBatis-Plus數(shù)據(jù)表操作條件構(gòu)造器Wrapper,需要的朋友可以參考下2023-09-09gradle項(xiàng)目中資源文件的相對(duì)路徑打包技巧必看
這篇文章主要介紹了gradle項(xiàng)目中資源文件的相對(duì)路徑打包技巧必看篇,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來(lái)看看吧2020-11-11SpringBoot結(jié)合Quartz實(shí)現(xiàn)數(shù)據(jù)庫(kù)存儲(chǔ)
本文主要介紹了SpringBoot+Quartz+數(shù)據(jù)庫(kù)存儲(chǔ),文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-01-01