二、详解 synchronize 锁的升级
synchronized 鎖定的是一個(gè)對(duì)象,執(zhí)行某段代碼的時(shí)候必須鎖定一個(gè)對(duì)象,不鎖定就無(wú)法執(zhí)行
一、概念介紹
1.1 用戶態(tài)與內(nèi)核態(tài)
內(nèi)核態(tài)(kener):內(nèi)核/操作系統(tǒng)可以做的一些操作。
用戶態(tài)(APP):用戶的程序可以做的一些操作。
用戶態(tài)的程序要訪問(wèn)一些比較危險(xiǎn)的操作的時(shí)候,比如格式化硬盤或直接訪問(wèn)內(nèi)存網(wǎng)卡等,必須經(jīng)過(guò)操作系統(tǒng)即內(nèi)核的允許,這樣可以保證安全性。
從指令來(lái)講,用戶態(tài)只能執(zhí)行某些指令,而內(nèi)核態(tài)可以執(zhí)行所有指令。
對(duì)于 JVM 虛擬機(jī)來(lái)說(shuō)就是一個(gè)普通程序,即屬于用戶態(tài)。
早期的 synchronized 叫重量級(jí)鎖,因?yàn)樵缙谑褂?synchronized 加鎖的時(shí)候要結(jié)果內(nèi)核態(tài)的允許,即要經(jīng)過(guò)操作系統(tǒng)線程的調(diào)度才能拿到鎖,所以稱為重量級(jí)鎖。
后期經(jīng)過(guò)了優(yōu)化在某些特定情況下不需要結(jié)果操作系統(tǒng),在用戶態(tài)就可以解決,即使輕量級(jí)鎖,比如 CAS 只是一個(gè)對(duì)比和交換,不需要經(jīng)過(guò)操作系統(tǒng)是輕量級(jí)鎖(鎖的升級(jí))。
1.2 CAS
CAS :compare and swap/compare and exchange
舉個(gè)例子:
A 線程獲取變量 a 的值此時(shí) a = 1,然后 A 線程對(duì)變量 a 進(jìn)行 a++ 操作,操作完成要寫回內(nèi)存。
此時(shí)會(huì)再次獲取當(dāng)前時(shí)間下變量 a 的值,如果此時(shí) a 依舊為0,就認(rèn)為沒(méi)有線程操作過(guò) a,就正常將 a=1 寫入。
如果發(fā)現(xiàn) a 的值已近變了比如 a = 3了,說(shuō)明有線程對(duì) a 做了操作,那就不寫入。
此時(shí)重新獲取 a 的值,在進(jìn)行 ++ 操作,操作完在判斷當(dāng)前 a 的值和 ++ 前的值是否一致。這樣一致循環(huán)下去。
上面說(shuō)的這種情況不用上鎖, CAS 也稱為自旋鎖/無(wú)鎖。無(wú)鎖不是沒(méi)有鎖,是沒(méi)有內(nèi)核狀態(tài)的鎖。
對(duì)圖中的 ABA 問(wèn)題做一下解釋:
還是上面的例子,A 線程執(zhí)行完 a++ 操作后,要將新的 a 值寫入內(nèi)存,此時(shí)會(huì)再次獲取當(dāng)前時(shí)間下變量 a 的值,如果此時(shí) a 依舊為0,就認(rèn)為沒(méi)有線程操作過(guò) a,就正常將 a=1 寫入。但是可能存在這種情況,就是 B 線程將變量 a 改為3,然后 C 線程又將變量 a 改為了0,實(shí)際上此時(shí)變量 a 已經(jīng)發(fā)生了變化。這就是 ABA 問(wèn)題。
解決方法:可以給變量 a 增加一個(gè)版本號(hào)
再舉個(gè)例子:
public static void main(String[] args) throws InterruptedException {
AtomicInteger integer = new AtomicInteger(0);
Thread[] threads = new Thread[10];
// 等待線程結(jié)束
CountDownLatch downLatch = new CountDownLatch(threads.length);
for (int i = 0; i < threads.length; i++) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
for (int j = 0; j < 5; j++) {
// 如果是 integer++ 的話就要加鎖
integer.incrementAndGet();
}
downLatch.countDown();
}
});
threads[i] = thread;
}
Arrays.stream(threads).forEach(f -> f.start());
downLatch.await();
System.out.println(Thread.currentThread().getName() + " " + integer);
}
上面的代碼中如果采用 integer++ 這種方式就要進(jìn)行加鎖,采用 integer.incrementAndGet() 就不需要加鎖,因?yàn)?incrementAndGet 方法底層就是采用的 CAS 實(shí)現(xiàn)的,是匯編的一條指令lock cmpxchg 指令。cmpxchg 指令不是原子的,所以需要 lock 指令給 CPU 加鎖,只讓一個(gè)線程操作。
1.3 對(duì)象在內(nèi)存中的分布
二、鎖的升級(jí)
偏向鎖、自旋鎖都是在用戶空間完成
重量級(jí)鎖都需要向內(nèi)核空間申請(qǐng)
偏向鎖:
向 markword 上記錄自己的線程指針,實(shí)際上沒(méi)有上鎖,只是標(biāo)記,此時(shí)只有一個(gè)線程執(zhí)行,沒(méi)有競(jìng)爭(zhēng)的概念。
為何會(huì)有偏向鎖:因?yàn)榻?jīng)過(guò)統(tǒng)計(jì)大多數(shù)情況下 synchronized 方法只有一個(gè)線程在執(zhí)行(如:stringbuffer的一些sync方法,vector的一些sync方法),此時(shí)沒(méi)必要申請(qǐng)鎖,節(jié)約資源
JVM 中偏向鎖是默認(rèn)打開(kāi)的,但是有延遲 4S,可以設(shè)置參數(shù)修改 1.-XX:BiasedLockingStartupDelay=0。對(duì)應(yīng)的就是鎖升級(jí)圖中 new 一個(gè)對(duì)象后會(huì)有兩種情況。
偏向鎖默認(rèn)打開(kāi)原因是:JVM 虛擬機(jī)自己有一些默認(rèn)啟動(dòng)的線程,里面有好多 sync 代碼,這些 sync 代碼啟動(dòng)時(shí)就知道肯定會(huì)有競(jìng)爭(zhēng),如果使用偏向鎖,就會(huì)造成偏向鎖不斷的進(jìn)行鎖撤銷和鎖升級(jí)的操作,效率較低。
偏向鎖是否一定比自旋鎖效率高:不一定,在明確知道會(huì)有多線程競(jìng)爭(zhēng)的情況下,偏向鎖肯定會(huì)涉及鎖撤銷,這時(shí)候直接使用自旋鎖不涉及鎖撤銷,效果高。
自旋鎖/輕量級(jí)鎖:
有偏向鎖升級(jí)而來(lái),當(dāng)有多個(gè)線程執(zhí)行(>= 2)的時(shí)候,此時(shí)就會(huì)有競(jìng)爭(zhēng)不能在采用偏向鎖了。
多個(gè)線程通過(guò)競(jìng)爭(zhēng),某一個(gè)線程會(huì)將自己的線程指針寫入 markword,標(biāo)記自己占有,其他線程只能等待。
怎么等待呢,就是采用 CAS 的方式,不停的去獲取 markword 上記錄的指針信息,看是不是被占有,如果沒(méi)有被占有就把自己的指針寫進(jìn)去。這種方式下等待的線程會(huì)占用 CPU 資源
所以自旋鎖也沒(méi)有經(jīng)過(guò)內(nèi)核態(tài)的操作,是輕量級(jí)鎖。
每個(gè)線程有自己的 LockRecord 在自己的線程棧上,用 CAS 去爭(zhēng)用 markword 的 LR 的指針,指針指向哪個(gè)線程的 LR,哪個(gè)線程就擁有鎖。
重量級(jí)鎖:
可以是自旋鎖升級(jí)而來(lái),自旋是消耗 CPU 資源的,如果鎖的時(shí)間長(zhǎng),或者自旋線程多,CPU 會(huì)被大量消耗。
重量級(jí)鎖有等待隊(duì)列,競(jìng)爭(zhēng)隊(duì)列,所有拿不到鎖的進(jìn)入等待隊(duì)列,不需要消耗 CPU 資源。
JDK6之前,一個(gè)線程自旋超過(guò)10次,或者等待的線程數(shù)超過(guò) CPU 核數(shù)的1/2,升級(jí)為重量級(jí)鎖,如果太多線程自旋 CPU 消耗過(guò)大,不如升級(jí)為重量級(jí)鎖,進(jìn)入等待隊(duì)列(不消耗CPU)。自旋次數(shù)和等待的線程數(shù)都可以通過(guò)參數(shù)控制。-XX:PreBlockSpin。
自旋鎖在 JDK1.4.2 中引入,使用 -XX:+UseSpinning 來(lái)開(kāi)啟。JDK 6 中變?yōu)槟J(rèn)開(kāi)啟,并且引入了自適應(yīng)的自旋鎖(適應(yīng)性自旋鎖)。
自適應(yīng)自旋鎖意味著自旋的時(shí)間(次數(shù))不再固定,根據(jù)歷史情況由 JVM 來(lái)管理。
偏向鎖耗時(shí)過(guò)長(zhǎng),或有 wait 時(shí)也會(huì)進(jìn)入重量級(jí)鎖。
總結(jié)
以上是生活随笔為你收集整理的二、详解 synchronize 锁的升级的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: flutte的第一个hello worl
- 下一篇: 《最终幻想7》蒂法、爱丽丝可动手办 单个