Java自旋鎖及自旋的好處詳解
什么是自旋
我們了解什么叫自旋?“自旋”可以理解為“自我旋轉(zhuǎn)”,這里的“旋轉(zhuǎn)”指“循環(huán)”,比如 while 循環(huán)或者 for 循環(huán)。“自旋”就是自己在這里不停地循環(huán),直到目標(biāo)達(dá)成。而不像普通的鎖那樣,如果獲取不到鎖就進(jìn)入阻塞。
對(duì)比自旋和非自旋的獲取鎖的流程
我們用這樣一張流程圖來對(duì)比一下自旋鎖和非自旋鎖的獲取鎖的過程。
我們來看自旋鎖,它并不會(huì)放棄 CPU 時(shí)間片,而是通過自旋等待鎖的釋放,也就是說,它會(huì)不停地再次地嘗試獲取鎖,如果失敗就再次嘗試,直到成功為止。
我們?cè)賮砜聪路亲孕i,非自旋鎖和自旋鎖是完全不一樣的,如果它發(fā)現(xiàn)此時(shí)獲取不到鎖,它就把自己的線程切換狀態(tài),讓線程休眠,然后 CPU 就可以在這段時(shí)間去做很多其他的事情,直到之前持有這把鎖的線程釋放了鎖,于是 CPU 再把之前的線程恢復(fù)回來,讓這個(gè)線程再去嘗試獲取這把鎖。如果再次失敗,就再次讓線程休眠,如果成功,一樣可以成功獲取到同步資源的鎖。
可以看出,非自旋鎖和自旋鎖最大的區(qū)別,就是如果它遇到拿不到鎖的情況,它會(huì)把線程阻塞,直到被喚醒。而自旋鎖會(huì)不停地嘗試。那么,自旋鎖這樣不停嘗試的好處是什么呢?
自旋鎖的好處
阻塞和喚醒線程都是需要高昂的開銷的,如果同步代碼塊中的內(nèi)容不復(fù)雜,那么可能轉(zhuǎn)換線程帶來的開銷比實(shí)際業(yè)務(wù)代碼執(zhí)行的開銷還要大。
在很多場(chǎng)景下,可能我們的同步代碼塊的內(nèi)容并不多,所以需要的執(zhí)行時(shí)間也很短,如果我們僅僅為了這點(diǎn)時(shí)間就去切換線程狀態(tài),那么其實(shí)不如讓線程不切換狀態(tài),而是讓它自旋地嘗試獲取鎖,等待其他線程釋放鎖,有時(shí)我只需要稍等一下,就可以避免上下文切換等開銷,提高了效率。
用一句話總結(jié)自旋鎖的好處,那就是自旋鎖用循環(huán)去不停地嘗試獲取鎖,讓線程始終處于 Runnable 狀態(tài),節(jié)省了線程狀態(tài)切換帶來的開銷。
AtomicLong 的實(shí)現(xiàn)
在 Java 1.5 版本及以上的并發(fā)包中,也就是 java.util.concurrent 的包中,里面的原子類基本都是自旋鎖的實(shí)現(xiàn)。
比如我們看一個(gè) AtomicLong 的實(shí)現(xiàn),里面有一個(gè) getAndIncrement 方法,源碼如下:
public final long getAndIncrement() { return unsafe.getAndAddLong(this, valueOffset, 1L); }
可以看到它調(diào)用了一個(gè) unsafe.getAndAddLong,所以我們?cè)賮砜催@個(gè)方法:
public final long getAndAddLong (Object var1,long var2, long var4){ long var6; do { var6 = this.getLongVolatile(var1, var2); } while (!this.compareAndSwapLong(var1, var2, var6, var6 + var4)); return var6; }
在這個(gè)方法中,它用了一個(gè) do while 循環(huán)。這里就很明顯了:
do { var6 = this.getLongVolatile(var1, var2); } while (!this.compareAndSwapLong(var1, var2, var6, var6 + var4));
這里的 do-while 循環(huán)就是一個(gè)自旋操作,如果在修改過程中遇到了其他線程競(jìng)爭(zhēng)導(dǎo)致沒修改成功的情況,就會(huì) while 循環(huán)里進(jìn)行死循環(huán),直到修改成功為止。
自己實(shí)現(xiàn)一個(gè)可重入的自旋鎖
下面我們來看一個(gè)自己實(shí)現(xiàn)可重入的自旋鎖。
代碼如下所示:
package lesson27; import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.locks.Lock; /** * 描述: 實(shí)現(xiàn)一個(gè)可重入的自旋鎖 */ public class ReentrantSpinLock { private AtomicReference<Thread> owner = new AtomicReference<>(); //重入次數(shù) private int count = 0; public void lock() { Thread t = Thread.currentThread(); if (t == owner.get()) { ++count; return; } //自旋獲取鎖 while (!owner.compareAndSet(null, t)) { System.out.println("自旋了"); } } public void unlock() { Thread t = Thread.currentThread(); //只有持有鎖的線程才能解鎖 if (t == owner.get()) { if (count > 0) { --count; } else { //此處無需CAS操作,因?yàn)闆]有競(jìng)爭(zhēng),因?yàn)橹挥芯€程持有者才能解鎖 owner.set(null); } } } public static void main(String[] args) { ReentrantSpinLock spinLock = new ReentrantSpinLock(); Runnable runnable = new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName() + "開始嘗試獲取自旋鎖"); spinLock.lock(); try { System.out.println(Thread.currentThread().getName() + "獲取到了自旋鎖"); Thread.sleep(4000); } catch (InterruptedException e) { e.printStackTrace(); } finally { spinLock.unlock(); System.out.println(Thread.currentThread().getName() + "釋放了了自旋鎖"); } } }; Thread thread1 = new Thread(runnable); Thread thread2 = new Thread(runnable); thread1.start(); thread2.start(); } }
這段代碼的運(yùn)行結(jié)果是:
...
自旋了
自旋了
自旋了
自旋了
自旋了
自旋了
自旋了
自旋了
Thread-0釋放了了自旋鎖
Thread-1獲取到了自旋鎖
前面會(huì)打印出很多“自旋了”,說明自旋期間,CPU依然在不停運(yùn)轉(zhuǎn)。
缺點(diǎn)
那么自旋鎖有沒有缺點(diǎn)呢?其實(shí)自旋鎖是有缺點(diǎn)的。它最大的缺點(diǎn)就在于雖然避免了線程切換的開銷,但是它在避免線程切換開銷的同時(shí)也帶來了新的開銷,因?yàn)樗枰煌5萌L試獲取鎖。如果這把鎖一直不能被釋放,那么這種嘗試只是無用的嘗試,會(huì)白白浪費(fèi)處理器資源。也就是說,雖然一開始自旋鎖的開銷低于線程切換,但是隨著時(shí)間的增加,這種開銷也是水漲船高,后期甚至?xí)^線程切換的開銷,得不償失。
適用場(chǎng)景
所以我們就要看一下自旋鎖的適用場(chǎng)景。首先,自旋鎖適用于并發(fā)度不是特別高的場(chǎng)景,以及臨界區(qū)比較短小的情況,這樣我們可以利用避免線程切換來提高效率。
可是如果臨界區(qū)很大,線程一旦拿到鎖,很久才會(huì)釋放的話,那就不合適用自旋鎖,因?yàn)樽孕龝?huì)一直占用 CPU 卻無法拿到鎖,白白消耗資源。
到此這篇關(guān)于Java自旋鎖及自旋的好處詳解的文章就介紹到這了,更多相關(guān)Java自旋鎖內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
使Java的JButton文字隱藏功能的實(shí)現(xiàn)(不隱藏按鈕的前提)
這篇文章主要介紹了使Java的JButton文字隱藏功能的實(shí)現(xiàn)(不隱藏按鈕的前提),本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-01-01SpringBoot配置文件方式,在線yml文件轉(zhuǎn)properties
這篇文章主要介紹了SpringBoot配置文件方式,在線yml文件轉(zhuǎn)properties,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-07-07mybatis-plus生成mapper擴(kuò)展文件的方法
這篇文章主要介紹了mybatis-plus生成mapper擴(kuò)展文件的方法,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-09-09RabbitMQ排他性隊(duì)列Exclusive Queue詳解
這篇文章主要介紹了RabbitMQ排他性隊(duì)列Exclusive Queue詳解,如果你想創(chuàng)建一個(gè)只有自己可見的隊(duì)列,即不允許其它用戶訪問,RabbitMQ允許你將一個(gè)Queue聲明成為排他性的Exclusive Queue,需要的朋友可以參考下2023-08-08實(shí)例詳解java Struts2的配置與簡(jiǎn)單案例
這篇文章主要介紹了java Struts2的配置與簡(jiǎn)單案例,需要的朋友可以參考下2017-04-04教你怎么用Java數(shù)組和鏈表實(shí)現(xiàn)棧
本篇文章為大家詳細(xì)介紹了怎么用Java數(shù)組和鏈表實(shí)現(xiàn)棧,文中有非常詳細(xì)的代碼示例及注釋,對(duì)正在學(xué)習(xí)java的小伙伴們很有幫助,需要的朋友可以參考下2021-05-05Java使用ant.jar執(zhí)行SQL腳本文件的示例代碼
這篇文章主要介紹了Java使用ant.jar執(zhí)行SQL腳本文件,文中通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2024-02-02