溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

Java CAS及其應(yīng)用之自旋鎖 Atomic類的示例分析

發(fā)布時(shí)間:2021-09-10 10:20:37 來源:億速云 閱讀:112 作者:柒染 欄目:大數(shù)據(jù)

今天就跟大家聊聊有關(guān)Java CAS及其應(yīng)用之自旋鎖 Atomic類的示例分析,可能很多人都不太了解,為了讓大家更加了解,小編給大家總結(jié)了以下內(nèi)容,希望大家根據(jù)這篇文章可以有所收獲。

一、CAS 操作

樂觀鎖用到的機(jī)制就是CAS,Compare and Swap。

CAS有3個(gè)操作數(shù),內(nèi)存值V,舊的預(yù)期值A(chǔ),要修改的新值B。當(dāng)且僅當(dāng)預(yù)期值A(chǔ)和內(nèi)存值V相同時(shí),將內(nèi)存值V修改為B,否則什么都不做。

1、非阻塞算法 (nonblocking algorithms)

一個(gè)線程的失敗或者掛起不應(yīng)該影響其他線程的失敗或掛起的算法。

現(xiàn)代的CPU提供了特殊的指令,可以自動(dòng)更新共享數(shù)據(jù),而且能夠檢測到其他線程的干擾,而 compareAndSet() 就用這些代替了鎖定。

2、Atomic示例

Atomic包是java.util.concurrent下的另一個(gè)專門為線程安全設(shè)計(jì)的Java包,包含多個(gè)原子操作類。這個(gè)包里面提供了一組原子變量類。其基本的特性就是在多線程環(huán)境下,當(dāng)有多個(gè)線程同時(shí)執(zhí)行這些類的實(shí)例包含的方法時(shí),具有排他性,即當(dāng)某個(gè)線程進(jìn)入方法,執(zhí)行其中的指令時(shí),不會(huì)被其他線程打斷,而別的線程就像自旋鎖一樣,一直等到該方法執(zhí)行完成,才由JVM從等待隊(duì)列中選擇一個(gè)另一個(gè)線程進(jìn)入,這只是一種邏輯上的理解。實(shí)際上是借助硬件的相關(guān)指令來實(shí)現(xiàn)的,不會(huì)阻塞線程(或者說只是在硬件級(jí)別上阻塞了)??梢詫?duì)基本數(shù)據(jù)、數(shù)組中的基本數(shù)據(jù)、對(duì)類中的基本數(shù)據(jù)進(jìn)行操作。原子變量類相當(dāng)于一種泛化的volatile變量,能夠支持原子的和有條件的讀-改-寫操作。

AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference這四種基本類型用來處理布爾,整數(shù),長整數(shù),對(duì)象四種數(shù)據(jù),其內(nèi)部實(shí)現(xiàn)不是簡單的使用synchronized,而是一個(gè)更為高效的方式CAS (compare and swap) + volatile和native方法,從而避免了synchronized的高開銷,執(zhí)行效率大為提升。我們來看個(gè)例子,與我們平時(shí)i++所對(duì)應(yīng)的原子操作為:getAndIncrement()

public static void main(String[] args) {
    AtomicInteger ai = new AtomicInteger();
    System.out.println(ai);
    ai.getAndIncrement();
    System.out.println(ai);
}

拿出AtomicInteger來研究在沒有鎖的情況下是如何做到數(shù)據(jù)正確性的。

private volatile int value;

在沒有鎖的機(jī)制下需要借助volatile原語,保證線程間的數(shù)據(jù)是可見的(共享的)。

這樣才獲取變量的值的時(shí)候才能直接讀取。

public final int get() {
    return value;
}

然后來看看 ++i 是怎么做到的。

public final int incrementAndGet() {
    for (;;) {
        int current = get();
        int next = current + 1;
        if (compareAndSet(current, next))
            return next;
    }
}

在這里采用了CAS操作,每次從內(nèi)存中讀取數(shù)據(jù)然后將此數(shù)據(jù)和+1后的結(jié)果進(jìn)行CAS操作,如果成功就返回結(jié)果,否則重試直到成功為止。

而compareAndSet利用JNI來完成CPU指令的操作。

public final boolean compareAndSet(int expect, int update) {   
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

整體的過程就是這樣子的,利用CPU的CAS指令,同時(shí)借助JNI來完成Java的非阻塞算法。其它原子操作都是利用類似的特性完成的。

而整個(gè)J.U.C都是建立在CAS之上的,因此對(duì)于synchronized阻塞算法,J.U.C在性能上有了很大的提升。參考資料的文章中介紹了如果利用CAS構(gòu)建非阻塞計(jì)數(shù)器、隊(duì)列等數(shù)據(jù)結(jié)構(gòu)。

二、ABA問題

CAS看起來很爽,但是會(huì)導(dǎo)致“ABA問題”。

CAS算法實(shí)現(xiàn)一個(gè)重要前提需要取出內(nèi)存中某時(shí)刻的數(shù)據(jù),而在下時(shí)刻比較并替換,那么在這個(gè)時(shí)間差類會(huì)導(dǎo)致數(shù)據(jù)的變化。

比如說一個(gè)線程one從內(nèi)存位置V中取出A,這時(shí)候另一個(gè)線程two也從內(nèi)存中取出A,并且two進(jìn)行了一些操作變成了B,然后two又將V位置的數(shù)據(jù)變成A,這時(shí)候線程one進(jìn)行CAS操作發(fā)現(xiàn)內(nèi)存中仍然是A,然后one操作成功。盡管線程one的CAS操作成功,但是不代表這個(gè)過程就是沒有問題的。如果鏈表的頭在變化了兩次后恢復(fù)了原值,但是不代表鏈表就沒有變化。因此前面提到的原子操作AtomicStampedReference/AtomicMarkableReference就很有用了。這允許一對(duì)變化的元素進(jìn)行原子操作。

在運(yùn)用CAS做Lock-Free操作中有一個(gè)經(jīng)典的ABA問題:

線程1準(zhǔn)備用CAS將變量的值由A替換為B,在此之前,線程2將變量的值由A替換為C,又由C替換為A,然后線程1執(zhí)行CAS時(shí)發(fā)現(xiàn)變量的值仍然為A,所以CAS成功。但實(shí)際上這時(shí)的現(xiàn)場已經(jīng)和最初不同了,盡管CAS成功,但可能存在潛藏的問題,例如下面的例子:現(xiàn)有一個(gè)用單向鏈表實(shí)現(xiàn)的堆棧,棧頂為A,這時(shí)線程T1已經(jīng)知道A.next為B,然后希望用CAS將棧頂替換為B:

head.compareAndSet(A,B);

在T1執(zhí)行上面這條指令之前,線程T2介入,將A、B出棧,再pushD、C、A,此時(shí)堆棧結(jié)構(gòu)如下圖,而對(duì)象B此時(shí)處于游離狀態(tài):

此時(shí)輪到線程T1執(zhí)行CAS操作,檢測發(fā)現(xiàn)棧頂仍為A,所以CAS成功,棧頂變?yōu)锽,但實(shí)際上B.next為null,所以此時(shí)的情況變?yōu)椋?/p>

其中堆棧中只有B一個(gè)元素,C和D組成的鏈表不再存在于堆棧中,平白無故就把C、D丟掉了。

以上就是由于ABA問題帶來的隱患,各種樂觀鎖的實(shí)現(xiàn)中通常都會(huì)用版本戳version來對(duì)記錄或?qū)ο髽?biāo)記,避免并發(fā)操作帶來的問題,在Java中,AtomicStampedReference<E>也實(shí)現(xiàn)了這個(gè)作用,它通過包裝[E,Integer]的元組來對(duì)對(duì)象標(biāo)記版本戳stamp,從而避免ABA問題,例如下面的代碼分別用AtomicInteger和AtomicStampedReference來對(duì)初始值為100的原子整型變量進(jìn)行更新,AtomicInteger會(huì)成功執(zhí)行CAS操作,而加上版本戳的AtomicStampedReference對(duì)于ABA問題會(huì)執(zhí)行CAS失?。?/p>

package concur.lock;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicStampedReference;

public class ABA {
    
    private static AtomicInteger atomicInt = new AtomicInteger(100);
    private static AtomicStampedReference<Integer> atomicStampedRef = 
            new AtomicStampedReference<Integer>(100, 0);
    
    public static void main(String[] args) throws InterruptedException {
        Thread intT1 = new Thread(new Runnable() {
            @Override
            public void run() {
                atomicInt.compareAndSet(100, 101);
                atomicInt.compareAndSet(101, 100);
            }
        });
        
        Thread intT2 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                boolean c3 = atomicInt.compareAndSet(100, 101);
                System.out.println(c3);        //true
            }
        });
        
        intT1.start();
        intT2.start();
        intT1.join();
        intT2.join();
        
        Thread refT1 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                atomicStampedRef.compareAndSet(100, 101, 
                        atomicStampedRef.getStamp(), atomicStampedRef.getStamp()+1);
                atomicStampedRef.compareAndSet(101, 100, 
                        atomicStampedRef.getStamp(), atomicStampedRef.getStamp()+1);
            }
        });
        
        Thread refT2 = new Thread(new Runnable() {
            @Override
            public void run() {
                int stamp = atomicStampedRef.getStamp();
                System.out.println("before sleep : stamp = " + stamp);    // stamp = 0
                try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("after sleep : stamp = " + atomicStampedRef.getStamp());//stamp = 1
                boolean c3 = atomicStampedRef.compareAndSet(100, 101, stamp, stamp+1);
                System.out.println(c3);        //false
            }
        });
        
        refT1.start();
        refT2.start();
    }

}

三.自旋鎖詳解

自旋鎖的概念
首先是一種鎖,與互斥鎖相似,基本作用是用于線程(進(jìn)程)之間的同步。與普通鎖不同的是,一個(gè)線程A在獲得普通鎖后,如果再有線程B試圖獲取鎖,那么這個(gè)線程B將會(huì)掛起(阻塞);試想下,如果兩個(gè)線程資源競爭不是特別激烈,而處理器阻塞一個(gè)線程引起的線程上下文的切換的代價(jià)高于等待資源的代價(jià)的時(shí)候(鎖的已保持者保持鎖時(shí)間比較短),那么線程B可以不放棄CPU時(shí)間片,而是在“原地”忙等,直到鎖的持有者釋放了該鎖,這就是自旋鎖的原理,可見自旋鎖是一種非阻塞鎖。
自旋鎖可能引起的問題:
1.過多占據(jù)CPU時(shí)間:如果鎖的當(dāng)前持有者長時(shí)間不釋放該鎖,那么等待者將長時(shí)間的占據(jù)cpu時(shí)間片,導(dǎo)致CPU資源的浪費(fèi),因此可以設(shè)定一個(gè)時(shí)間,當(dāng)鎖持有者超過這個(gè)時(shí)間不釋放鎖時(shí),等待者會(huì)放棄CPU時(shí)間片阻塞;
2.死鎖問題:試想一下,有一個(gè)線程連續(xù)兩次試圖獲得自旋鎖(比如在遞歸程序中),第一次這個(gè)線程獲得了該鎖,當(dāng)?shù)诙卧噲D加鎖的時(shí)候,檢測到鎖已被占用(其實(shí)是被自己占用),那么這時(shí),線程會(huì)一直等待自己釋放該鎖,而不能繼續(xù)執(zhí)行,這樣就引起了死鎖。因此遞歸程序使用自旋鎖應(yīng)該遵循以下原則:遞歸程序決不能在持有自旋鎖時(shí)調(diào)用它自己,也決不能在遞歸調(diào)用時(shí)試圖獲得相同的自旋鎖。

class SpinLock {
        //java中原子(CAS)操作
	AtomicReference<Thread> owner = new AtomicReference<Thread>();//持有自旋鎖的線程對(duì)象
	private int count;
	public void lock() {
		Thread cur = Thread.currentThread();
		//lock函數(shù)將owner設(shè)置為當(dāng)前線程,并且預(yù)測原來的值為空。unlock函數(shù)將owner設(shè)置為null,并且預(yù)測值為當(dāng)前線程。 
        //當(dāng)有第二個(gè)線程調(diào)用lock操作時(shí)由于owner值不為空,導(dǎo)致循環(huán)	
	    //一直被執(zhí)行,直至第一個(gè)線程調(diào)用unlock函數(shù)將owner設(shè)置為null,第二個(gè)線程才能進(jìn)入臨界區(qū)。
		while (!owner.compareAndSet(null, cur)){
		}
	}
	public void unLock() {
		Thread cur = Thread.currentThread();
			owner.compareAndSet(cur, null);
		}
	}
}

看完上述內(nèi)容,你們對(duì)Java CAS及其應(yīng)用之自旋鎖 Atomic類的示例分析有進(jìn)一步的了解嗎?如果還想了解更多知識(shí)或者相關(guān)內(nèi)容,請(qǐng)關(guān)注億速云行業(yè)資訊頻道,感謝大家的支持。

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請(qǐng)聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI