1、各種鎖概括
![]()
【多線程】Java的各種鎖機制、鎖的優化/更新(無鎖、偏向鎖、自旋鎖、重量級鎖)
2、樂觀鎖與悲觀鎖
其實樂觀鎖和悲觀鎖隻是一種思想,是對于線程同步的不同看法。
悲觀鎖
擷取同步資料的時候會加鎖,以防止被其他線程修改。
對于同一個資料的并發操作,悲觀鎖認為自己在使用資料的時候一定有别的線程來修改資料,是以在擷取資料的時候會先加鎖,確定資料不會被别的線程修改。Java中,synchronized關鍵字和Lock的實作類都是悲觀鎖。
樂觀鎖
不會添加鎖,但是會在更新資料的時候判斷資料是否和之前一樣(Compare and Swap)。
如果這個資料沒有被更新,目前線程将自己修改的資料成功寫入。如果資料已經被其他線程更新,則根據不同的實作方式執行不同的操作(例如報錯或者自動重試)。最常采用的是CAS算法,Java原子類中的遞增操作就是通過CAS自旋實作的。
簡單說一下CAS的缺點:
1、ABA問題;
JDK從1.5開始提供了AtomicStampedReference類來解決ABA問題,具體操作封裝在compareAndSet()中。compareAndSet()首先檢查目前引用和目前标志與預期引用和預期标志是否相等,如果都相等,則以原子方式将引用值和标志的值設定為給定的更新值。
2、不斷自旋循環時間太長開銷大;
3、隻能保證一個共享變量的原子操作。
Java從1.5開始JDK提供了AtomicReference類來保證引用對象之間的原子性,可以把多個變量放在一個對象裡來進行CAS操作。
對比
- 悲觀鎖适合寫操作較多的場景,先加鎖可以保證寫操作時資料正确。
- 樂觀鎖适合讀操作多的場景,不加鎖的特點能夠使其讀操作的性能大幅提升。
——————————————————————————————————————————————————————————![]()
【多線程】Java的各種鎖機制、鎖的優化/更新(無鎖、偏向鎖、自旋鎖、重量級鎖)
3、自旋鎖和适應性自旋鎖
自旋鎖
由于阻塞和喚醒一個Java線程需要作業系統切換CPU狀态來完成,這種狀态轉換需要耗費處理器時間,而在許多場景中,同步資源鎖定的時間比較短,是以可以讓請求鎖的線程稍等一段時間(自旋),假如自旋完成後鎖定同步資源的線程已經釋放了鎖,那麼目前線程就可以不必阻塞而是直接擷取同步資源。
但是雖然自旋避免了線程切換的開銷,但是要占用處理器時間,自旋時間太久會浪費處理器資源,是以自旋等待有限制次數,預設是10次(可以使用-XX:PreBlockSpin來更改),假如沒有成功獲得鎖,那麼就應該挂起線程。
阻塞或喚醒一個Java線程需要作業系統切換CPU狀态來完成,這種狀态轉換需要耗費處理器時間。如果同步代碼塊中的内容過于簡單,狀态轉換消耗的時間有可能比使用者代碼執行的時間還要長。
在許多場景中,同步資源的鎖定時間很短,為了這一小段時間去切換線程,線程挂起和恢複現場的花費可能會讓系統得不償失。如果實體機器有多個處理器,能夠讓兩個或以上的線程同時并行執行,我們就可以讓後面那個請求鎖的線程不放棄CPU的執行時間,看看持有鎖的線程是否很快就會釋放鎖。
而為了讓目前線程“稍等一下”,我們需讓目前線程進行自旋,如果在自旋完成後前面鎖定同步資源的線程已經釋放了鎖,那麼目前線程就可以不必阻塞而是直接擷取同步資源,進而避免切換線程的開銷。這就是自旋鎖。
适應性自旋鎖
自旋鎖在JDK1.4.2中引入,使用-XX:+UseSpinning來開啟。JDK 6中變為預設開啟,并且引入了自适應的自旋鎖(适應性自旋鎖)。
自适應意味着自旋的時間(次數)不再固定,而是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀态來決定。
如果在同一個鎖對象上,自旋等待剛剛成功獲得過鎖,并且持有鎖的線程正在運作中,那麼虛拟機就會認為這次自旋也是很有可能再次成功,進而它将允許自旋等待持續相對更長的時間。如果對于某個鎖,自旋很少成功獲得過,那在以後嘗試擷取這個鎖時将可能省略掉自旋過程,直接阻塞線程,避免浪費處理器資源。
在自旋鎖中 另有三種常見的鎖形式:TicketLock、CLHlock和MCSlock。
4、鎖的更新
這四種鎖指的是鎖的狀态,專門針對synchronized的。
Synchronized實作線程同步的原理
synchronized是悲觀鎖,在操作同步資源之前需要給同步資源先加鎖,這把鎖就是存在Java對象頭裡的
Java對象頭: Mark Word(标記字段)、Klass Pointer(類型指針)
MarkWord:存儲了預設存儲對象的HashCode,分代年齡和鎖标志位資訊。在運作期間MarkWord裡存儲的資料會随着鎖标志位的變化而變化。
Klass Pointer:對象指向它的類中繼資料的指針,虛拟機通過這個指針來确定這個對象是哪個類的執行個體。
Monitor: 它是依賴于底層作業系統的Mutex Lock互斥鎖(Monitor enter、Monitor exit)來實作的線程同步。
synchronized最初實作同步就是依賴的Mutex Lock,效率低,是重量級鎖。
Monitor是線程私有的資料結構,每一個線程都有一個可用monitor record清單,同時還有一個全局的可用清單。每一個被鎖住的對象都會和一個monitor關聯,同時monitor中有一個Owner字段存放擁有該鎖的線程的唯一辨別,表示該鎖被這個線程占用。
為了減少獲得鎖和釋放鎖帶來的性能消耗,引入了 偏向鎖 和 輕量級鎖。
優化Synchronzied(更新):
更新過程:無鎖—>偏向鎖—>輕量級鎖—>重量級鎖
![]()
【多線程】Java的各種鎖機制、鎖的優化/更新(無鎖、偏向鎖、自旋鎖、重量級鎖)
- 無鎖 無鎖狀态,不需要加鎖解鎖(标志位01);
沒有對資源進行鎖定,所有的線程都能通路并修改同一個資源,但同時隻有一個線程能修改成功。
while{有沖突就不修改,不沖突就修改}
- 偏向鎖 然後當有一個線程開始通路同步塊時,該線程會自動擷取鎖(降低擷取鎖的代價),更新為偏向鎖。(更新主要就是改變對象頭的MARKWORD)
優點: 加鎖解鎖不需要額外的消耗,适用于隻有一個線程通路同步塊場景;
缺點: 如果線程間存在鎖競争,會帶來額外鎖撤銷的開銷。(隻要有線程競争資源,就撤銷鎖)
第一次線程通路同步塊并擷取鎖時,會在MarkWord裡存儲鎖偏向的線程ID,通過CAS修改對象頭内容,讓鎖對象變成偏向鎖;
第二次到達該同步塊,會檢測Mark Word裡是否存儲着指向目前線程的偏向鎖,判斷此時持有鎖的線程是否是自己,是的話不需要加鎖,正常往下執行。(是以,當隻有一個線程操作的時候,幾乎不需要啥加鎖解鎖的開銷)
偏向鎖隻有遇到其他線程嘗試競争偏向鎖時,持有偏向鎖的線程才會釋放鎖,線程不會主動釋放偏向鎖。偏向鎖的撤銷,需要等待全局安全點(在這個時間點上沒有位元組碼正在執行),它會首先暫停擁有偏向鎖的線程,判斷鎖對象是否處于被鎖定狀态。撤銷偏向鎖後恢複到無鎖(标志位為“01”)或輕量級鎖(标志位為“00”)的狀态。
偏向鎖在JDK 6及以後的JVM裡是預設啟用的。可以通過JVM參數關閉偏向鎖:-XX:-UseBiasedLocking=false,關閉之後程式預設會進入輕量級鎖狀态。
- 自旋鎖 然後遇到其他線程嘗試競争偏向鎖時,更新為自旋鎖(輕量級鎖,标志位00)
優點: 競争的線程不會阻塞,提高了程式的響應速度,适用于追求響應速度,同步塊執行速度快的場景;
缺點: 如果始終得不到競争的資源,使用自旋會消耗CPU
在輕量級鎖狀态下繼續鎖競争,沒有搶到鎖的線程将自旋,即不停地循環判斷鎖是否能夠被成功擷取。
能等就不要出動作業系統,畢竟開銷太大了
在代碼進入同步塊的時候,如果同步對象鎖狀态為無鎖狀态(鎖标志位為“01”狀态,是否為偏向鎖為“0”),虛拟機首先将在目前線程的棧幀中建立一個名為鎖記錄(Lock Record)的空間,用于存儲鎖對象目前的Mark Word的拷貝,然後拷貝對象頭中的Mark Word複制到鎖記錄中。
拷貝成功後,虛拟機将使用CAS操作嘗試将對象的Mark Word更新為指向Lock Record的指針,并将Lock Record裡的owner指針指向對象的Mark Word。
如果這個更新動作成功了,那麼這個線程就擁有了該對象的鎖,并且對象Mark Word的鎖标志位設定為“00”,表示此對象處于輕量級鎖定狀态。
如果輕量級鎖的更新操作失敗了,虛拟機首先會檢查對象的Mark Word是否指向目前線程的棧幀,如果是就說明目前線程已經擁有了這個對象的鎖,那就可以直接進入同步塊繼續執行,否則說明多個線程競争鎖。
若目前隻有一個等待線程,則該線程通過自旋進行等待。但是當自旋超過一定的次數,或者一個線程在持有鎖,一個在自旋,又有第三個來訪時,輕量級鎖更新為重量級鎖。
- 重量級鎖 自旋十次失敗後,更新為重量級鎖(标志位11)
優點: 不使用自旋,不會消耗CPU,适用于追求吞吐量,同步塊執行速度較慢的場景。
缺點: 線程阻塞,響應時間緩慢
鎖标志的狀态值變為“10”,此時Mark Word中存儲的是指向重量級鎖的指針,此時等待鎖的線程都會進入阻塞狀态。
之是以重量級,是因為它的實作依賴于低層作業系統的Mutex Lock(互斥鎖)實作的,而作業系統實作線程的切換需要從使用者态轉換為核心态,成本非常高。
5、公平鎖與非公平鎖
參考部落格:
深度分析:鎖更新過程和鎖狀态,看完這篇你就懂了!
https://www.cnblogs.com/zhengzhiwei/p/13139326.html