JDK基于CAS實(shí)現(xiàn)原子類盤點(diǎn)解析
前言
JDK中提供了一系列的基于CAS實(shí)現(xiàn)的原子類,CAS 的全稱是Compare-And-Swap
,底層是lock cmpxchg
指令,可以在單核和多核 CPU 下都能夠保證比較交換的原子性。所以說,這些原子類都是線程安全的,而且是無鎖并發(fā),線程不會(huì)頻繁上下文切換,所以在某些場景下性能是優(yōu)于加鎖。
本文就盤點(diǎn)一下JDK中的原子類,方便我們后續(xù)拿來使用。
基礎(chǔ)原子類
AtomicInteger
:Integer整數(shù)類型的原子操作類AtomicBoolean
:Boolean類型的原子操作類AtomicLong
:Long類型的原子操作類
這邊以AtomicInteger
講解下它的API和用法。
構(gòu)造方法:
public AtomicInteger()
:初始化一個(gè)默認(rèn)值為 0 的原子型Integer
public AtomicInteger(int initialValue)
:初始化一個(gè)指定值的原子型Integer
常用API:
public final int get()
: 獲取 AtomicInteger 的值public final int getAndIncrement()
: 以原子方式將當(dāng)前值加 1,返回的是自增前的值public final int incrementAndGet()
:以原子方式將當(dāng)前值加 1,返回的是自增后的值public final int getAndSet(int value)
:以原子方式設(shè)置為 newValue 的值,返回舊值public final int addAndGet(int data)
:以原子方式將輸入的數(shù)值與實(shí)例中的值相加并返回
使用:
- 結(jié)果1000,大致說明并發(fā)情況下保證了線程安全
原理分析:
整體實(shí)現(xiàn)思路: 自旋(循環(huán)) + CAS算法
- 當(dāng)舊的預(yù)期值 A == 內(nèi)存值 V 此時(shí)可以修改,將 V 改為 B
- 當(dāng)舊的預(yù)期值 A != 內(nèi)存值 V 此時(shí)不能修改,并重新獲取現(xiàn)在的最新值,重新獲取的動(dòng)作就是自旋
public final int getAndIncrement() { return unsafe.getAndAddInt(this, valueOffset, 1); }
valueOffset
:偏移量表示該變量值相對于當(dāng)前對象地址的偏移,Unsafe 就是根據(jù)內(nèi)存偏移地址獲取數(shù)據(jù)
- 從主內(nèi)存中拷貝到工作內(nèi)存中的值(每次都要從主內(nèi)存拿到最新的值到本地內(nèi)存),然后執(zhí)行
compareAndSwapInt()
再和主內(nèi)存的值進(jìn)行比較,假設(shè)方法返回 false,那么就一直執(zhí)行 while 方法,直到期望的值和真實(shí)值一樣,修改數(shù)據(jù)。
- 原子類
AtomicInteger
的value
屬性是volatile
類型,保證了多線程之間的內(nèi)存可見性,避免線程從工作緩存中獲取失效的變量。
原子引用
原子引用主要是對對象的原子操作,原子引用類分為AtomicReference
、AtomicStampedReference
、AtomicMarkableReference
。它們之間有什么區(qū)別呢?
- AtomicReference類
普通的原子類對象
public class AtomicReferenceDemo { public static void main(String[] args) { User user1 = new User("旭陽"); // 創(chuàng)建原子引用包裝類 AtomicReference<User> atomicReference = new AtomicReference<>(user1); while (true) { User user2 = new User("alvin"); // 比較并交換 if (atomicReference.compareAndSet(user1, user2)) { break; } } System.out.println(atomicReference.get()); } } @Data @AllArgsConstructor @ToString class User { private String name; }
- 調(diào)用
compareAndSet()
方法進(jìn)行比較替換對象
ABA問題
但是如果使用AtomicReference
類,會(huì)有一個(gè)ABA問題。什么意思呢?就是一個(gè)線程將共享變量從A改成B, 后面又改回A, 這是,另外一個(gè)線程就無法感知這個(gè)變化過程,就傻傻的比較,就以為沒有變化,還是一開始的A,就替換了。 實(shí)際的確存在這樣只要共享變量發(fā)生過變化,就要CAS失敗,有什么辦法呢?
- AtomicStampedReference類
帶版本號(hào)的原子類對象
@Slf4j(topic = "a.AtomicStampedReferenceTest") public class AtomicStampedReferenceTest { // 構(gòu)造AtomicStampedReference static AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0); public static void main(String[] args) throws InterruptedException { log.debug("main start..."); // 獲取值 A String prev = ref.getReference(); // 獲取版本號(hào) int stamp = ref.getStamp(); log.debug("版本 {}", stamp); // 如果中間有其它線程干擾,發(fā)生了 ABA 現(xiàn)象 other(); Thread.sleep(1000); // 嘗試改為 C log.debug("change A->C {}", ref.compareAndSet(prev, "C", stamp, stamp + 1)); } private static void other() throws InterruptedException { new Thread(() -> { log.debug("change A->B {}", ref.compareAndSet(ref.getReference(), "B", ref.getStamp(), ref.getStamp() + 1)); log.debug("更新版本為 {}", ref.getStamp()); }, "t1").start(); Thread.sleep(500); new Thread(() -> { log.debug("change B->A {}", ref.compareAndSet(ref.getReference(), "A", ref.getStamp(), ref.getStamp() + 1)); log.debug("更新版本為 {}", ref.getStamp()); }, "t2").start(); } }
- 雖然對象的值變回了A,但是由于版本變了,所以主線程CAS失敗
- AtomicMarkableReference 類
其實(shí)有時(shí)候并不關(guān)心共享變量修改了幾次,而是只要標(biāo)記下是否發(fā)生過更改,是否加個(gè)標(biāo)記即可,所以就有了AtomicMarkableReference
類。
@Slf4j(topic = "c.AtomicMarkableReferenceTest") public class AtomicMarkableReferenceTest { // 構(gòu)造 AtomicMarkableReference, 初始標(biāo)記為false static AtomicMarkableReference<String> ref = new AtomicMarkableReference<>("A", false); public static void main(String[] args) throws InterruptedException { log.debug("main start..."); other(); Thread.sleep(1000); // 看看是否發(fā)生了變化 log.debug("change {}", ref.isMarked()); } private static void other() throws InterruptedException { new Thread(() -> { log.debug("change A->B {}", ref.compareAndSet(ref.getReference(), "B", false, true)); }, "t1").start(); Thread.sleep(500); new Thread(() -> { log.debug("change B->A {}", ref.compareAndSet(ref.getReference(), "A", true, true)); }, "t2").start(); } }
- 通過調(diào)用
isMarked()
方法查看是否發(fā)生變化。
原子數(shù)組
AtomicIntegerArray
: Integer類型的原子數(shù)組AtomicLongArray
:Long類型的原子數(shù)組AtomicReferenceArray
:引用類型的原子數(shù)組
直接上例子
public class AtomicIntegerArrayTest { public static void main(String[] args) throws Exception{ AtomicIntegerArray array = new AtomicIntegerArray(10); Thread t1 = new Thread(()->{ int index; for(int i=1; i<100000; i++) { index = i%10; //范圍0~9 array.incrementAndGet(index); } }); Thread t2 = new Thread(()->{ int index; for(int i=1; i<100000; i++) { index = i%10; //范圍0~9 array.decrementAndGet(index); } }); t1.start(); t2.start(); Thread.sleep(5 * 1000); System.out.println(array.toString()); } }
- 兩個(gè)線程同時(shí)對數(shù)組對象進(jìn)行加和減的操作,最終結(jié)果都是0,說明線程安全。
原子字段更新器
AtomicReferenceFieldUpdater
AtomicIntegerFieldUpdater
AtomicLongFieldUpdater
利用字段更新器,可以針對對象的某個(gè)域(Field
)進(jìn)行原子操作,只能配合 volatile
修飾的字段使用,否則會(huì)出現(xiàn)異常。
@Data public class AtomicReferenceFieldUpdaterTest { private volatile int age = 10; private int age2; public static void main(String[] args) { AtomicIntegerFieldUpdater integerFieldUpdater = AtomicIntegerFieldUpdater.newUpdater(AtomicReferenceFieldUpdaterTest.class, "age"); AtomicReferenceFieldUpdaterTest ref = new AtomicReferenceFieldUpdaterTest(); // 對volatile 的age字段+1 integerFieldUpdater.getAndIncrement(ref); System.out.println(ref.getAge()); // 修改 非volatile的age2 integerFieldUpdater = AtomicIntegerFieldUpdater.newUpdater(AtomicReferenceFieldUpdaterTest.class, "age2"); integerFieldUpdater.getAndIncrement(ref); } }
- 原子字段更新器只能更新
volatile
字段,它可以保證可見性,但是無法保證原子性。
原子累加器
原子累加器主要是用來做累加的,相關(guān)的類有LongAdder
、DoubleAdder
、LongAccumulator
、DoubleAccumulator
。
LongAdder
是jdk1.8中引入的,它的性能要比AtomicLong
方式好。
LongAddr
類是 LongAccumulator
類的一個(gè)特例,只是 LongAccumulator
提供了更強(qiáng)大的功能,可以自定義累加規(guī)則,當(dāng)accumulatorFunction
為 null 時(shí)就等價(jià)于 LongAddr
。
這邊做個(gè)性能的對比例子。
public class LongAdderTest { public static void main(String[] args) { System.out.println("LongAdder ..........."); for (int i = 0; i < 5; i++) { addFunc(() -> new LongAdder(), adder -> adder.increment()); } System.out.println("AtomicLong ..........."); for (int i = 0; i < 5; i++) { addFunc(() -> new AtomicLong(), adder -> adder.getAndIncrement()); } } private static <T> void addFunc(Supplier<T> adderSupplier, Consumer<T> action) { T adder = adderSupplier.get(); long start = System.nanoTime(); List<Thread> ts = new ArrayList<>(); // 40個(gè)線程,每人累加 50 萬 for (int i = 0; i < 40; i++) { ts.add(new Thread(() -> { for (int j = 0; j < 500000; j++) { action.accept(adder); } })); } ts.forEach(t -> t.start()); ts.forEach(t -> { try { t.join(); } catch (InterruptedException e) { e.printStackTrace(); } }); long end = System.nanoTime(); System.out.println(adder + " cost:" + (end - start)/1000_000); } }
主要是由于LongAdder
會(huì)設(shè)置多個(gè)累加單元,Therad-0 累加 Cell[0],而 Thread-1 累加Cell[1]... 最后將結(jié)果匯總。這樣它們在累加時(shí)操作的不同的 Cell 變量,因此減少了 CAS 重試失敗,從而提高性能。
總結(jié)
本文總結(jié)了JDK中提供的各種原子類,包括基礎(chǔ)原子類、原子引用類、原子數(shù)組類、原子字段更新器和原子累加器等。有時(shí)候,使用這些原子類的性能是比加鎖要高的,特別是在讀多寫少的場景下。但是,不知道大家發(fā)現(xiàn)沒有,所有的原子類操作對于一個(gè)共享變量執(zhí)行操作是原子的,如果對于多個(gè)共享變量操作時(shí),循環(huán) CAS 就無法保證操作的原子性,還是老老實(shí)實(shí)加鎖吧,更多關(guān)于JDK CAS原子類的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Spring?Boot項(xiàng)目啟動(dòng)報(bào)錯(cuò)Unable?to?start?web?server解決方法
這篇文章主要給大家介紹了關(guān)于Spring?Boot項(xiàng)目啟動(dòng)報(bào)錯(cuò)Unable?to?start?web?server的解決方法,錯(cuò)誤內(nèi)容大概的意思是未能加載嵌入的供web應(yīng)用加載的空間,是因?yàn)槿鄙賁ervletWebServerFactorybean,需要的朋友可以參考下2024-07-07Java實(shí)現(xiàn)實(shí)時(shí)視頻轉(zhuǎn)播的代碼示例
這篇文章主要給大家詳細(xì)介紹了Java如何實(shí)現(xiàn)實(shí)時(shí)視頻轉(zhuǎn)播,文中通過代碼實(shí)例介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴可以自己動(dòng)手試一試2023-09-09Java?離線中文語音文字識(shí)別功能的實(shí)現(xiàn)代碼
這篇文章主要介紹了Java?離線中文語音文字識(shí)別,本次使用springboot?+maven實(shí)現(xiàn),官方demo為springboot+gradle,結(jié)合實(shí)例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-07-07Spring中的接口重試機(jī)制spring-retry之listeners參數(shù)解析
這篇文章主要介紹了Spring中的接口重試機(jī)制spring-retry之listeners參數(shù)解析,注解@Retryable有一個(gè)參數(shù)listeners沒有說明,那么本篇文章我們詳細(xì)介紹一個(gè)這個(gè)參數(shù)的用,需要的朋友可以參考下2024-01-01SpringBoot集成JWT實(shí)現(xiàn)token驗(yàn)證的流程
Json web token (JWT), 是為了在網(wǎng)絡(luò)應(yīng)用環(huán)境間傳遞聲明而執(zhí)行的一種基于JSON的開放標(biāo)準(zhǔn)((RFC 7519).這篇文章主要介紹了SpringBoot集成JWT實(shí)現(xiàn)token驗(yàn)證,需要的朋友可以參考下2020-01-01Java-ElementUi中的row-class-name使用
這篇文章主要介紹了Java-ElementUi中的row-class-name使用方式,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-08-08