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