Java中的自旋鎖與適應(yīng)性自旋鎖的區(qū)別
自旋鎖
1、概念:
當(dāng)一個線程嘗試去獲取某一把鎖的時候,如果這個鎖此時已經(jīng)被別人獲取(占用),那么此線程就無法獲取到這把鎖,該線程將會等待,間隔一段時間后會再次嘗試獲取。這種采用循環(huán)加鎖 -> 等待的機制被稱為自旋鎖(spinlock)
2、提出背景
由于在多處理器環(huán)境中某些資源的有限性,有時需要互斥訪問(mutual exclusion),這時候就需要引入鎖的概念,只有獲取了鎖的線程才能夠?qū)Y源進行訪問,由于多線程的核心是CPU的時間分片,所以同一時刻只能有一個線程獲取到鎖。那么就面臨一個問題,那么沒有獲取到鎖的線程應(yīng)該怎么辦?
通常有兩種處理方式:一種是沒有獲取到鎖的線程就一直循環(huán)等待判斷該資源是否已經(jīng)釋放鎖,這種鎖叫做自旋鎖,它不用將線程阻塞起來(NON-BLOCKING);還有一種處理方式就是把自己阻塞起來,等待重新調(diào)度請求,這種叫做互斥鎖。
3、自旋鎖的原理
自旋鎖的原理比較簡單,如果持有鎖的線程能在短時間內(nèi)釋放鎖資源,那么那些等待競爭鎖的線程就不需要做內(nèi)核態(tài)和用戶態(tài)之間的切換進入阻塞狀態(tài),它們只需要等一等(自旋),等到持有鎖的線程釋放鎖之后即可獲取,這樣就避免了用戶進程和內(nèi)核切換的消耗。
因為自旋鎖避免了操作系統(tǒng)進程調(diào)度和線程切換,所以自旋鎖通常適用在時間比較短的情況下。由于這個原因,操作系統(tǒng)的內(nèi)核經(jīng)常使用自旋鎖。但是,如果長時間上鎖的話,自旋鎖會非常耗費性能,它阻止了其他線程的運行和調(diào)度。線程持有鎖的時間越長,則持有該鎖的線程將被 OS(Operating System) 調(diào)度程序中斷的風(fēng)險越大。如果發(fā)生中斷情況,那么其他線程將保持旋轉(zhuǎn)狀態(tài)(反復(fù)嘗試獲取鎖),而持有該鎖的線程并不打算釋放鎖,這樣導(dǎo)致的是結(jié)果是無限期推遲,直到持有鎖的線程可以完成并釋放它為止。
解決上面這種情況一個很好的方式是給自旋鎖設(shè)定一個自旋時間,等時間一到立即釋放自旋鎖。自旋鎖的目的是占著CPU資源不進行釋放,等到獲取鎖立即進行處理。但是如何去選擇自旋時間呢?如果自旋執(zhí)行時間太長,會有大量的線程處于自旋狀態(tài)占用 CPU 資源,進而會影響整體系統(tǒng)的性能。因此自旋的周期選的額外重要!JDK在1.6 引入了適應(yīng)性自旋鎖,適應(yīng)性自旋鎖意味著自旋時間不是固定的了,而是由前一次在同一個鎖上的自旋時間以及鎖擁有的狀態(tài)來決定,基本認為一個線程上下文切換的時間是最佳的一個時間。
4、 自旋鎖的優(yōu)缺點
優(yōu)點:自旋鎖盡可能的減少線程的阻塞,這對于鎖的競爭不激烈,且占用鎖時間非常短的代碼塊來說性能能大幅度的提升,因為自旋的消耗會小于線程阻塞掛起再喚醒的操作的消耗,這些操作會導(dǎo)致線程發(fā)生兩次上下文切換!
缺點:但是如果鎖的競爭激烈,或者持有鎖的線程需要長時間占用鎖執(zhí)行同步塊,這時候就不適合使用自旋鎖了,因為自旋鎖在獲取鎖前一直都是占用 cpu 做無用功,占著 XX 不 XX,同時有大量線程在競爭一個鎖,會導(dǎo)致獲取鎖的時間很長,線程自旋的消耗大于線程阻塞掛起操作的消耗,其它需要 cpu 的線程又不能獲取到 cpu,造成 cpu 的浪費。所以這種情況下我們要關(guān)閉自旋鎖。
5、自旋鎖開啟
雖然在JDK1.4.2的時候就引入了自旋鎖,但是需要使用“-XX:+UseSpinning”參數(shù)來開啟。在到了JDK1.6以后,就已經(jīng)是默認開啟了。
舉個栗子:
public class SpinLockTest { /** * 持有鎖的線程,null表示鎖未被線程持有 */ private AtomicReference<Thread> ref = new AtomicReference<>(); public void lock(){ Thread currentThread = Thread.currentThread(); while(!ref.compareAndSet(null, currentThread)){ //當(dāng)ref為null的時候compareAndSet返回true,反之為false //通過循環(huán)不斷的自旋判斷鎖是否被其他線程持有 } } public void unLock() { Thread cur = Thread.currentThread(); if(ref.get() != cur){ //exception ... } ref.set(null); } } //自旋鎖測試 public class SpinLockTestTest { static int count = 0; @Test public void spinLockTest() throws InterruptedException { ExecutorService executorService = Executors.newFixedThreadPool(100); CountDownLatch countDownLatch = new CountDownLatch(100); SpinLockTest2 spinLockTest2 = new SpinLockTest2(); for (int i = 0; i < 100; i++) { executorService.execute(new Runnable() { @Override public void run() { spinLockTest2.lock(); ++count; spinLockTest2.unLock(); countDownLatch.countDown(); } }); } countDownLatch.await(); System.out.println (count); } }
通過上面的代碼可以看出,自旋就是在循環(huán)判斷條件是否滿足,那么會有什么問題嗎?如果鎖被占用很長時間的話,自旋的線程等待的時間也會變長,白白浪費掉處理器資源。因此在JDK中,自旋操作默認10次,我們可以通過參數(shù)“-XX:PreBlockSpin”來設(shè)置,當(dāng)超過來此參數(shù)的值,則會使用傳統(tǒng)的線程掛起方式來等待鎖釋放。
自適應(yīng)自旋鎖
隨著JDK的更新,在1.6的時候,又出現(xiàn)了一個叫做“自適應(yīng)自旋鎖”的玩意。它的出現(xiàn)使得自旋操作變得聰明起來,不再跟之前一樣死板。所謂的“自適應(yīng)”意味著對于同一個鎖對象,線程的自旋時間是根據(jù)上一個持有該鎖的線程的自旋時間以及狀態(tài)來確定的。例如對于A鎖對象來說,如果一個線程剛剛通過自旋獲得到了鎖,并且該線程也在運行中,那么JVM會認為此次自旋操作也是有很大的機會可以拿到鎖,因此它會讓自旋的時間相對延長。但是如果對于B鎖對象自旋操作很少成功的話,JVM甚至可能直接忽略自旋操作。因此,自適應(yīng)自旋鎖是一個更加智能,對我們的業(yè)務(wù)性能更加友好的一個鎖。
總結(jié)
自旋鎖是為了提高資源的使用頻率而出現(xiàn)的一種鎖,自旋鎖說的是線程獲取鎖的時候,如果鎖被其他線程持有,則當(dāng)前線程將循環(huán)等待,直到獲取到鎖。
自旋鎖在等待期間不會睡眠或者釋放自己的線程。自旋鎖不適用于長時間持有CPU的情況,這會加劇系統(tǒng)的負擔(dān),為了解決這種情況,需要設(shè)定自旋周期,那么自旋周期的設(shè)定也是一門學(xué)問。
在自旋鎖中 另有三種常見的鎖形式:TicketLock、CLHlock和MCSlock
這里直接舉栗子:
- TicketLock 是一種同步機制或鎖定算法,它是一種自旋鎖,它使用ticket 來控制線程執(zhí)行順序。
/** * @Description TicketLock 是一種同步機制或鎖定算法,它是一種自旋鎖,它使用ticket 來控制線程執(zhí)行順序。 * * TicketLock 是基于先進先出(FIFO) 隊列的機制。 * 它增加了鎖的公平性,其設(shè)計原則如下:TicketLock 中有兩個 int 類型的數(shù)值, * 開始都是0,第一個值是隊列ticket(隊列票據(jù)), 第二個值是 出隊(票據(jù))。 * 隊列票據(jù)是線程在隊列中的位置,而出隊票據(jù)是現(xiàn)在持有鎖的票證的隊列位置。 * 可能有點模糊不清,簡單來說,就是隊列票據(jù)是你取票號的位置,出隊票據(jù)是你距離叫號的位置。 * * * @Author zhoumm * @Version V1.0.0 * @Since 1.0 * @Date 2020-05-29 */ //這個設(shè)計是有問題的,因為獲得自己的號碼之后,是可以對號碼進行更改的,這就造成系統(tǒng)紊亂,鎖不能及時釋放。 //需要有一個能確保每個人按會著自己號碼排隊辦業(yè)務(wù)的角色 public class TicketLock { // 隊列票據(jù)(當(dāng)前排隊號碼) private AtomicInteger queueNum = new AtomicInteger(); // 出隊票據(jù)(當(dāng)前需等待號碼) private AtomicInteger dueueNum = new AtomicInteger(); // 獲取鎖:如果獲取成功,返回當(dāng)前線程的排隊號 public int lock(){ int currentTicketNum = dueueNum.incrementAndGet(); while (currentTicketNum != queueNum.get()){ // doSomething... } return currentTicketNum; } // 釋放鎖:傳入當(dāng)前排隊的號碼 public void unLock(int ticketNum){ queueNum.compareAndSet(ticketNum,ticketNum + 1); } //改進后,這就不再需要返回值,辦業(yè)務(wù)的時候,要將當(dāng)前的這一個號碼緩存起來,在辦完業(yè)務(wù)后,需要釋放緩存的這條票據(jù)。 //缺點:雖然解決了公平性的問題,但是多處理器系統(tǒng)上,每個進程/線程占用的處理器都在讀寫同一個變量queueNum , // 每次讀寫操作都必須在多個處理器緩存之間進行緩存同步,這會導(dǎo)致繁重的系統(tǒng)總線和內(nèi)存的流量,大大降低系統(tǒng)整體的性能。 //為了解決這個問題,MCSLock 和 CLHLock 應(yīng)運而生 public class TicketLock2 { // 隊列票據(jù)(當(dāng)前排隊號碼) private AtomicInteger queueNum = new AtomicInteger(); // 出隊票據(jù)(當(dāng)前需等待號碼) private AtomicInteger dueueNum = new AtomicInteger(); //線程內(nèi)部的存儲類,可以在指定線程內(nèi)存儲數(shù)據(jù),數(shù)據(jù)存儲以后,只有指定線程可以得到存儲數(shù)據(jù) private ThreadLocal<Integer> ticketLocal = new ThreadLocal<>(); public void lock(){ int currentTicketNum = dueueNum.incrementAndGet(); // 獲取鎖的時候,將當(dāng)前線程的排隊號保存起來 ticketLocal.set(currentTicketNum); while (currentTicketNum != queueNum.get()){ // doSomething... } } // 釋放鎖:從排隊緩沖池中取 public void unLock(){ Integer currentTicket = ticketLocal.get(); queueNum.compareAndSet(currentTicket,currentTicket + 1); } } }
- TicketLock 是基于隊列的,那么 CLHLock 就是基于鏈表設(shè)計的
/** * @Description * TicketLock 是基于隊列的,那么 CLHLock 就是基于鏈表設(shè)計的 * * CLH 是一種基于鏈表的可擴展,高性能,公平的自旋鎖,申請線程只能在本地變量上自旋, * 它會不斷輪詢前驅(qū)的狀態(tài),如果發(fā)現(xiàn)前驅(qū)釋放了鎖就結(jié)束自旋。 * * * @Author zhoumm * @Version V1.0.0 * @Since 1.0 * @Date 2020-05-29 */ public class CLHLock { public static class CLHNode{ private volatile boolean isLocked = true; } // 尾部節(jié)點 private volatile CLHNode tail; private static final ThreadLocal<CLHNode> LOCAL = new ThreadLocal<>(); private static final AtomicReferenceFieldUpdater<CLHLock,CLHNode> UPDATER = AtomicReferenceFieldUpdater.newUpdater(CLHLock.class,CLHNode.class,"tail"); public void lock(){ // 新建節(jié)點并將節(jié)點與當(dāng)前線程保存起來 CLHNode node = new CLHNode(); LOCAL.set(node); // 將新建的節(jié)點設(shè)置為尾部節(jié)點,并返回舊的節(jié)點(原子操作),這里舊的節(jié)點實際上就是當(dāng)前節(jié)點的前驅(qū)節(jié)點 CLHNode preNode = UPDATER.getAndSet(this,node); if(preNode != null){ // 前驅(qū)節(jié)點不為null表示當(dāng)鎖被其他線程占用,通過不斷輪詢判斷前驅(qū)節(jié)點的鎖標(biāo)志位等待前驅(qū)節(jié)點釋放鎖 while (preNode.isLocked){ } preNode = null; LOCAL.set(node); } // 如果不存在前驅(qū)節(jié)點,表示該鎖沒有被其他線程占用,則當(dāng)前線程獲得鎖 } public void unlock() { // 獲取當(dāng)前線程對應(yīng)的節(jié)點 CLHNode node = LOCAL.get(); // 如果tail節(jié)點等于node,則將tail節(jié)點更新為null,同時將node的lock狀態(tài)職位false,表示當(dāng)前線程釋放了鎖 if (!UPDATER.compareAndSet(this, node, null)) { node.isLocked = false; } node = null; } }
- MCS Spinlock 是一種基于鏈表的可擴展、高性能、公平的自旋鎖,申請線程只在本地變量上自旋,直接前驅(qū)負責(zé)通知其結(jié)束自旋,從而極大地減少了不必要的處理器緩存同步的次數(shù),降低了總線和內(nèi)存的開銷
/** * @Description * MCS Spinlock 是一種基于鏈表的可擴展、高性能、公平的自旋鎖,申請線程只在本地變量上自旋, * 直接前驅(qū)負責(zé)通知其結(jié)束自旋,從而極大地減少了不必要的處理器緩存同步的次數(shù),降低了總線和內(nèi)存的開銷。 * * 總結(jié): * 1.都是基于鏈表,不同的是CLHLock是基于隱式鏈表,沒有真正的后續(xù)節(jié)點屬性,MCSLock是顯示鏈表,有一個指向后續(xù)節(jié)點的屬性。 * 2.將獲取鎖的線程狀態(tài)借助節(jié)點(node)保存,每個線程都有一份獨立的節(jié)點,這樣就解決了TicketLock多處理器緩存同步的問題。 * @Author zhoumm * @Version V1.0.0 * @Since 1.0 * @Date 2020-05-29 */ public class MCSLock { public static class MCSNode { volatile MCSNode next; volatile boolean isLocked = true; } private static final ThreadLocal<MCSNode> NODE = new ThreadLocal<>(); // 隊列 @SuppressWarnings("unused") private volatile MCSNode queue; private static final AtomicReferenceFieldUpdater<MCSLock,MCSNode> UPDATE = AtomicReferenceFieldUpdater.newUpdater(MCSLock.class,MCSNode.class,"queue"); public void lock(){ // 創(chuàng)建節(jié)點并保存到ThreadLocal中 MCSNode currentNode = new MCSNode(); NODE.set(currentNode); // 將queue設(shè)置為當(dāng)前節(jié)點,并且返回之前的節(jié)點 MCSNode preNode = UPDATE.getAndSet(this, currentNode); if (preNode != null) { // 如果之前節(jié)點不為null,表示鎖已經(jīng)被其他線程持有 preNode.next = currentNode; // 循環(huán)判斷,直到當(dāng)前節(jié)點的鎖標(biāo)志位為false while (currentNode.isLocked) { } } } public void unlock() { MCSNode currentNode = NODE.get(); // next為null表示沒有正在等待獲取鎖的線程 if (currentNode.next == null) { // 更新狀態(tài)并設(shè)置queue為null if (UPDATE.compareAndSet(this, currentNode, null)) { // 如果成功了,表示queue==currentNode,即當(dāng)前節(jié)點后面沒有節(jié)點了 return; } else { // 如果不成功,表示queue!=currentNode,即當(dāng)前節(jié)點后面多了一個節(jié)點,表示有線程在等待 // 如果當(dāng)前節(jié)點的后續(xù)節(jié)點為null,則需要等待其不為null(參考加鎖方法) while (currentNode.next == null) { } } } else { // 如果不為null,表示有線程在等待獲取鎖,此時將等待線程對應(yīng)的節(jié)點鎖狀態(tài)更新為false,同時將當(dāng)前線程的后繼節(jié)點設(shè)為null currentNode.next.isLocked = false; currentNode.next = null; } } }
到此這篇關(guān)于Java中的自旋鎖與適應(yīng)性自旋鎖的區(qū)別的文章就介紹到這了,更多相關(guān)自旋鎖與適應(yīng)性自旋鎖區(qū)別內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringBoot一個接口多個實現(xiàn)類的調(diào)用方式總結(jié)
這篇文章主要介紹了SpringBoot一個接口多個實現(xiàn)類的調(diào)用方式,本文給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2024-01-01Java DecimalFormat 保留小數(shù)位及四舍五入的陷阱介紹
這篇文章主要介紹了Java DecimalFormat 保留小數(shù)位及四舍五入的陷阱,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-10-10