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