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è)自旋操作,如果在修改過程中遇到了其他線程競爭導(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)闆]有競爭,因?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-01
SpringBoot配置文件方式,在線yml文件轉(zhuǎn)properties
這篇文章主要介紹了SpringBoot配置文件方式,在線yml文件轉(zhuǎn)properties,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-07-07
mybatis-plus生成mapper擴(kuò)展文件的方法
這篇文章主要介紹了mybatis-plus生成mapper擴(kuò)展文件的方法,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-09-09
RabbitMQ排他性隊(duì)列Exclusive Queue詳解
這篇文章主要介紹了RabbitMQ排他性隊(duì)列Exclusive Queue詳解,如果你想創(chuàng)建一個(gè)只有自己可見的隊(duì)列,即不允許其它用戶訪問,RabbitMQ允許你將一個(gè)Queue聲明成為排他性的Exclusive Queue,需要的朋友可以參考下2023-08-08
教你怎么用Java數(shù)組和鏈表實(shí)現(xiàn)棧
本篇文章為大家詳細(xì)介紹了怎么用Java數(shù)組和鏈表實(shí)現(xiàn)棧,文中有非常詳細(xì)的代碼示例及注釋,對(duì)正在學(xué)習(xí)java的小伙伴們很有幫助,需要的朋友可以參考下2021-05-05
Java使用ant.jar執(zhí)行SQL腳本文件的示例代碼
這篇文章主要介紹了Java使用ant.jar執(zhí)行SQL腳本文件,文中通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2024-02-02

