*還是要使用才能暴露問題呀!
併發與鎖的知識整理
目錄
什麼是重量級鎖?
重量級鎖是指線程上鎖需要向內核態申請鎖資源,這中間會有一個申請的過程,所以很重。jdk早期是這種鎖,要上鎖得通過kernel申請鎖資源。
什麼是輕量級鎖
輕量級鎖也叫自旋鎖、無鎖或者一般來說CAS也是指輕量級鎖
輕量級鎖在程序中實現鎖,避免了向內核態申請鎖的操作。
正如它的名字所說,其核心在於“先寫入再交換”,當寫入的時候會校驗輸入的參數與獲取時是否一致,如果不一致,那麼重新來再跑一遍程序,如此往復循環就想在原地轉圈,所以又叫自旋鎖。
輕量級鎖的問題
1. 輕量級鎖什麼時候升級爲重量級鎖
1.6之前,當線程自旋超過10次,或者正在自旋的線程超過CPU核數的一半,就會升級。不過,1.6之後,java已經加入了自適應自旋,由JVM自己控制了。
2. 著名的ABA問題
輕量級鎖是通過比較輸入參數來確定自己獲取的鎖沒有被其他線程搶走的,但是如果其他線程做的事情是這樣的——線程1將值從A改爲B,線程2將值從B改爲A——那麼CAS要怎樣分辨獲取的鎖是沒有問題的呢?
·解決方法
如果是基礎類型的變量,那麼應該保證線程拿到這個變量做的事情只和其值有關係,是否變過、變過幾次多結果並不影響。
如果不是基礎類型的變量,則需要使用版本號來標識(這不就和git一樣嗎!)。
其實我們熟悉的atomic包下就有這樣的鎖:
/**
* Atomically adds the given value to the current value.
*
* @param delta the value to add
* @return the updated value
*/
public final int addAndGet(int delta) {
return unsafe.getAndAddInt(this, valueOffset, delta) + delta;
}
這裏我們可以看到AtomicInteger類調用了unsafe這個成員變量的getAndAddInt方法。那麼這個方法做了什麼呢?
// 我也不知道var3哪裏去了
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
這裏unsafe就是用了native的compareAndSwapInt方法來進行更新。
這個unsafe提供了硬件級別的原子操作,cas只是其中一個。
這個方法就已經是在Hotspot裏面的代碼了,如果要深究的話,Hotspot在底層實現是這個方法
inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {
int mp = os::is_MP();
__asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)"
: "=a" (exchange_value)
: "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
: "cc", "memory");
return exchange_value;
}
開始看不懂了吧,這塊你只要知道is_MP()是用來確定是不是多個cpu在處理(is_mutiple_processor),LOCK_IF_MP(%4)是說如果是多個處理器在處理,則鎖資源。
這邊其實可以看到最終使用了cmpxchgl命令(應該是彙編語言),硬件層面不會鎖總線。
lock cmpxchg指令
偏向鎖與匿名偏向鎖
什麼是偏向鎖?爲什麼會出現偏向鎖?
HotSpot的作者發現大多數情況下,鎖不存在多線程競爭,並且總是由同一個線程多次獲得,所以爲了讓線程獲得鎖的代價更低,引入了所謂“偏向鎖”——記錄了上鎖線程信息並且當上鎖線程在進入和退出同步塊的時候不需要進行cas操作加鎖解鎖。
啥是匿名偏向鎖?
如果jvm的偏向鎖設置是打開狀態的話,那麼一個實例被new出來的時候默認是開啓偏向鎖的(markword的倒數三位爲101),只不過這時候還沒有填上線程Id,所以叫匿名偏向鎖。
——如果沒有打開偏向鎖設置的話那麼實例被new出來的時候是無鎖狀態(001)。
偏向鎖什麼時候升級?
爲什麼這麼設計呢?我們可以想象一下,如果線程之間不存在競爭,那麼線程每次進入執行狀態的時候其實沒有必要做多次校驗,也省下了加鎖解鎖的開銷。
那麼如果有競爭了怎麼辦呢?
這裏就引入了偏向鎖的撤銷機制——如果有競爭,則撤銷偏向鎖,轉爲自旋鎖。
偏向鎖使用了一種等到競爭出現才釋放鎖的機制,所以當其他線程嘗試競爭偏向鎖時,
持有偏向鎖的線程纔會釋放鎖。偏向鎖的撤銷,需要等待全局安全點(在這個時間點上沒有正
在執行的字節碼)。它會首先暫停擁有偏向鎖的線程,然後檢查持有偏向鎖的線程是否活着,
如果線程不處於活動狀態,則將對象頭設置成無鎖狀態;如果線程仍然活着,擁有偏向鎖的棧
會被執行,遍歷偏向對象的鎖記錄,棧中的鎖記錄和對象頭的Mark Word要麼重新偏向於其他
線程,要麼恢復到無鎖或者標記對象不適合作爲偏向鎖,最後喚醒暫停的線程。圖2-1中的線
程1演示了偏向鎖初始化的流程,線程2演示了偏向鎖撤銷的流程。
——摘自《Java併發變成的藝術》
偏向鎖的撤銷機制可以再另一篇筆記中詳細研究。
大概的描述就是:
線程在自己的線程棧生成LockRecord ,用CAS操作將markword設置爲指向自己這個線程的LR的指針,設置成功者得到鎖。
關於偏向鎖的應用場景
1.偏向鎖一定比自旋鎖性能好嗎?
不一定,如果明知道肯定會有線程競爭,那可以一開始就是用自旋鎖(反正競爭了也要變成自旋鎖的)。
java6、java7和java11中都是默認啓用偏向鎖的,線程在獲取鎖時優先獲取偏向鎖,但是前4秒並不是這樣的,這是因爲jvm在啓動的時候明確知道會有線程競爭資源,所以延遲開啓了偏向鎖。
jdk8默認對象頭是無鎖的。←尚待研究
*附測試鏈接:https://blog.csdn.net/weixin_41263382/article/details/106677235
2.待續。