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-01
Java DecimalFormat 保留小數(shù)位及四舍五入的陷阱介紹
這篇文章主要介紹了Java DecimalFormat 保留小數(shù)位及四舍五入的陷阱,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-10-10

