Java?CAS與Atomic原子操作核心原理詳解
什么是原子操作
Mysql事務(wù)中的原子性就是一個(gè)事務(wù)中執(zhí)行的多條sql,要么同時(shí)成功,要么同時(shí)失敗,他們不可拆分。并發(fā)中的原子操作也一樣,多個(gè)線(xiàn)程中,站在線(xiàn)程A的角度看線(xiàn)程B的操作,線(xiàn)程B的操作就是一個(gè)原子的;站在線(xiàn)程B的角度看線(xiàn)程A,線(xiàn)程A的操作是原子的。一整個(gè)操作要么全部執(zhí)行完了,要么就沒(méi)有執(zhí)行,中間不能拆分。
那么要怎么實(shí)現(xiàn)原子性嘞?可以使用synchronized鎖來(lái)保證一段代碼的原子性,但是加鎖影響性能,甚至還有死鎖方面的問(wèn)題需要考慮。
所以鎖機(jī)制是比較重量級(jí)的,粒度較大的一種機(jī)制,比如對(duì)于計(jì)數(shù)器方面的操作來(lái)說(shuō),可能加鎖的耗時(shí)都比整個(gè)計(jì)算的耗時(shí)還要高。Java 就提供了 Atomic 系列的原子操作類(lèi),在java.util.concurrent.atomic
包下
這些原子操作類(lèi)是基于處理器的CAS指令來(lái)實(shí)現(xiàn)原子性的,Compare and swap。比較并且交換
CAS
每個(gè)CAS操作過(guò)程基本上都包含三個(gè)部分:內(nèi)存地址V、期望值A(chǔ)、新值B
期望值就是舊值,首先會(huì)去內(nèi)存地址中進(jìn)行比較,我期望當(dāng)前這個(gè)內(nèi)存地址中的值是我期望的舊值,如果是則把新值賦值到這個(gè)內(nèi)存地址中,如果不是則不做任何事。在一般的使用中我們會(huì)不斷嘗試去進(jìn)行CAS操作,直到成功為止。
Java 中的 Atomic 系列的原子操作類(lèi)的實(shí)現(xiàn)則是利用了循環(huán) CAS 來(lái)實(shí)現(xiàn)。
使用CAS實(shí)現(xiàn)原子操作的幾個(gè)問(wèn)題
ABA問(wèn)題
ABA問(wèn)題在大多數(shù)場(chǎng)景下,不解決其實(shí)也沒(méi)什么影響。
解決思路:添加版本戳,在變量前面追加上版本號(hào),每次變量更新的時(shí)候把版本號(hào)加 1,那么 A-->B-->A
就會(huì)變成 1A-->2B-->3A
循環(huán)時(shí)間長(zhǎng),對(duì)于cpu來(lái)說(shuō)開(kāi)銷(xiāo)較大
只能保證一個(gè)共享變量的原子操作
對(duì)于多個(gè)共享變量操作時(shí)就無(wú)法使用CAS來(lái)保證原子性了,這個(gè)時(shí)候還是需要用鎖。
還有一個(gè)取巧的辦法,就是把多個(gè)共享變量合并成一個(gè)共享變量來(lái)操作。比如,有兩個(gè)共享變量 i=2,j=a,合并一下 ij=2a,然后用 CAS 來(lái)操作 ij。
從 Java 1.5開(kāi)始,JDK 提供了AtomicReference
類(lèi)來(lái)保證引用對(duì)象之間的原子性,就可以把多個(gè)變量放在一個(gè)對(duì)象里來(lái)進(jìn)行 CAS 操作。
相關(guān)原子操作類(lèi)的使用
這些類(lèi)的用戶(hù)都大同小異,這里就拿幾個(gè)典型來(lái)舉例
AtomicInteger
// 以原子方式將給定值添加到當(dāng)前值,然后將相加后的結(jié)果返回 public final int addAndGet(int delta){} // 指定期望值與修改后的值,如果期望值和當(dāng)前值相同則進(jìn)行更新操作 public final boolean compareAndSet(int expect, int update) {} // 先返回當(dāng)前值,然后再進(jìn)行原子自增1 public final int getAndIncrement() {} // 先返回當(dāng)前值,然后進(jìn)行原子更新操作 public final int getAndSet(int newValue) {}
案例:
public class UseAtomicInt { static AtomicInteger ai = new AtomicInteger(10); public static void main(String[] args) { ai.getAndIncrement(); ai.incrementAndGet(); //ai.compareAndSet(); ai.addAndGet(24); } }
AtomicIntegerArray
提供原子的方式更新數(shù)據(jù)中的整形,常用方法如下:
// 以原子方式將給定值添加到索引 i 處的元素。然后返回更新后的值 public final int addAndGet(int i, int delta){} // 先比較,期望值和當(dāng)前值相同再執(zhí)行更新操作 public final boolean compareAndSet(int i, int expect, int update) {}
案例:
public class AtomicArray { static int[] value = new int[] { 1, 2 }; static AtomicIntegerArray ai = new AtomicIntegerArray(value); public static void main(String[] args) { ai.getAndSet(0, 3); System.out.println(ai.get(0)); //原數(shù)組不會(huì)變化 System.out.println(value[0]); } } Process finished with exit code 0
// 輸出結(jié)果
3
1
需要注意的是,數(shù)組 value 通過(guò)構(gòu)造方法傳遞進(jìn)去,然后 AtomicIntegerArray會(huì)將當(dāng)前數(shù)組復(fù)制一份,所以當(dāng) AtomicIntegerArray 對(duì)內(nèi)部的數(shù)組元素進(jìn)行修改 時(shí),不會(huì)影響傳入的數(shù)組。
更新引用類(lèi)型
如果要同時(shí)更新多個(gè)原子變量就需要使用更新引用類(lèi)型提供的類(lèi)了。Atomic提供了三個(gè)類(lèi):
AtomicReference
原子更新引用類(lèi)型
案例:
public class UseAtomicReference { public static AtomicReference<UserInfo> atomicUserRef; public static void main(String[] args) { //要修改的實(shí)體的實(shí)例 UserInfo user = new UserInfo("Mark", 15); atomicUserRef = new AtomicReference(user); // 再創(chuàng)建一個(gè)對(duì)象 UserInfo updateUser = new UserInfo("Bill",17); // 期望值和當(dāng)前值相同就進(jìn)行修改 atomicUserRef.compareAndSet(user,updateUser); System.out.println(atomicUserRef.get()); System.out.println(user); /* 輸出結(jié)果: UserInfo{name='Bill', age=17} UserInfo{name='Mark', age=15} */ } /** * 定義一個(gè)實(shí)體類(lèi) */ static class UserInfo { private volatile String name; private int age; public UserInfo(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public int getAge() { return age; } @Override public String toString() { return "UserInfo{" + "name='" + name + '\'' + ", age=" + age + '}'; } } }
AtomicStampedReference
利用版本戳的形式記錄了每次改變以后的版本號(hào),這樣的話(huà)就不會(huì)存在 ABA問(wèn)題了
AtomicMarkableReference
原子更新帶有標(biāo)記位的引用類(lèi)型??梢栽痈乱粋€(gè)布爾類(lèi)型的標(biāo)記位和引 用類(lèi)型。
構(gòu)造方法是 AtomicMarkableReference(V initialRef,booleaninitialMark)。
AtomicMarkableReference跟 AtomicStampedReference 差不多,
AtomicStampedReference 是使用 pair 的 int stamp 作為計(jì)數(shù)器使用
AtomicMarkableReference 的使用pair 的boolean mark。
AtomicStampedReference 可能關(guān)心的是動(dòng)過(guò)幾次,AtomicMarkableReference 關(guān)心的是有沒(méi)有被人動(dòng)過(guò)。
案例:
// 第二個(gè)線(xiàn)程,期望的時(shí)間戳和當(dāng)前時(shí)間戳不同,所以更新不成功 public class UseAtomicStampedReference { static AtomicStampedReference<String> asr = new AtomicStampedReference("mark", 0); public static void main(String[] args) throws InterruptedException { //拿到當(dāng)前的版本號(hào)(舊) final int oldStamp = asr.getStamp(); final String oldReference = asr.getReference(); System.out.println(oldReference + "============" + oldStamp); Thread rightStampThread = new Thread(new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName() + ":當(dāng)前變量值:" + oldReference + "-當(dāng)前版本戳:" + oldStamp + "\n" + asr.compareAndSet(oldReference, oldReference + "+Java", oldStamp, oldStamp + 1)); } }); Thread errorStampThread = new Thread(new Runnable() { @Override public void run() { String reference = asr.getReference(); System.out.println(Thread.currentThread().getName() + ":當(dāng)前變量值:" + reference + "-當(dāng)前版本戳:" + asr.getStamp() + "\n" + asr.compareAndSet(reference, reference + "+C", oldStamp, oldStamp + 1)); } }); rightStampThread.start(); rightStampThread.join(); errorStampThread.start(); errorStampThread.join(); System.out.println(asr.getReference() + "============" + asr.getStamp()); } }
輸出結(jié)果
mark============0
Thread-0:當(dāng)前變量值:mark-當(dāng)前版本戳:0
true
Thread-1:當(dāng)前變量值:mark+Java-當(dāng)前版本戳:1
false
mark+Java============1
原子更新字段類(lèi)
如果需原子地更新某個(gè)類(lèi)里的某個(gè)字段時(shí),就需要使用原子更新字段類(lèi)
Atomic 包提供了以下 3 個(gè)類(lèi)進(jìn)行原子字段更新。 要想原子地更新字段類(lèi)需要兩步。
因?yàn)樵痈伦侄晤?lèi)都是抽象類(lèi), 每次使用的時(shí)候必須使用靜態(tài)方法 newUpdater()創(chuàng)建一個(gè)更新器,并且需要設(shè)置想要更新的類(lèi)和屬性。
更新類(lèi)的字段(屬性)必須使用 public volatile修飾符。
- AtomicIntegerFieldUpdater:原子更新整型的字段的更新器。
- AtomicLongFieldUpdater:原子更新長(zhǎng)整型字段的更新器。
- AtomicReferenceFieldUpdater:原子更新引用類(lèi)型里的字段。
LongAdder
并發(fā)量較少,自旋的沖突也就較少。但如果并發(fā)很多的情況下,CAS機(jī)制就不如synchronized了,因?yàn)楹芏鄠€(gè)線(xiàn)程都集中判斷一個(gè)變量的值,不斷的自旋,對(duì)cpu的消耗也較大,同一時(shí)刻又只會(huì)一個(gè)線(xiàn)程更新成功。
在JDK1.8就引入了LongAdder
類(lèi),它在處理上面問(wèn)題的時(shí)候是采用的一種熱點(diǎn)數(shù)據(jù)的分散寫(xiě)
LongAdder中有兩個(gè)成員變量
// 當(dāng)為非空時(shí),大小為 2 的冪。 // 如果并發(fā)很高就使用cell數(shù)組做寫(xiě)熱點(diǎn)的分散,其中某些線(xiàn)程共同操作某一個(gè)數(shù)組中的元素 transient volatile Cell[] cells; // 當(dāng)爭(zhēng)搶較少時(shí)使用這個(gè)變量來(lái)進(jìn)行cas,就類(lèi)似于A(yíng)tomicInteger類(lèi)中的value變量 transient volatile long base;
然后調(diào)用sum()
方法將數(shù)組cells和base變量的中做一個(gè)匯總,返回當(dāng)前總和。在沒(méi)有并發(fā)更新的情況下調(diào)用將返回準(zhǔn)確的結(jié)果,但在計(jì)算總和時(shí)發(fā)生的并發(fā)更新可能不會(huì)合并,所以sum()方法并不能保證強(qiáng)一致性,它返回的只是一個(gè)近似值
// 可以看到 sum()方法沒(méi)有任何加鎖的邏輯 public long sum() { Cell[] as = cells; Cell a; long sum = base; if (as != null) { for (int i = 0; i < as.length; ++i) { if ((a = as[i]) != null) sum += a.value; } } return sum; }
到此這篇關(guān)于Java CAS與Atomic原子操作核心原理詳解的文章就介紹到這了,更多相關(guān)Java CAS與Atomic原子操作內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Spring框架基于A(yíng)OP實(shí)現(xiàn)簡(jiǎn)單日志管理步驟解析
這篇文章主要介紹了Spring框架基于A(yíng)OP實(shí)現(xiàn)簡(jiǎn)單日志管理步驟解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-06-06Spring Boot 配置文件(application.yml、application-dev.y
本文主要介紹了Spring Boot 配置文件,主要包含application.yml、application-dev.yml、application-test.yml,具有一定的參考價(jià)值,感興趣的可以了解一下2024-03-03Java Floyd算法求有權(quán)圖(非負(fù)權(quán))的最短路徑并打印
這篇文章主要介紹了Java Floyd算法求有權(quán)圖(非負(fù)權(quán))的最短路徑并打印,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-07-07JavaWeb Spring依賴(lài)注入深入學(xué)習(xí)
這篇文章主要為大家詳細(xì)介紹了JavaWeb Spring依賴(lài)注入,深入學(xué)習(xí)Spring依賴(lài)注入,感興趣的小伙伴們可以參考一下2016-09-09Java發(fā)送http請(qǐng)求的示例(get與post方法請(qǐng)求)
這篇文章主要介紹了Java發(fā)送http請(qǐng)求的示例(get與post方法請(qǐng)求),幫助大家更好的理解和使用Java,感興趣的朋友可以了解下2021-01-01Java實(shí)現(xiàn)Excel導(dǎo)入導(dǎo)出數(shù)據(jù)庫(kù)的方法示例
這篇文章主要介紹了Java實(shí)現(xiàn)Excel導(dǎo)入導(dǎo)出數(shù)據(jù)庫(kù)的方法,結(jié)合實(shí)例形式分析了java針對(duì)Excel的讀寫(xiě)及數(shù)據(jù)庫(kù)操作相關(guān)實(shí)現(xiàn)技巧,需要的朋友可以參考下2017-08-08elasticsearch集群cluster主要功能詳細(xì)分析
這篇文章主要為大家介紹了elasticsearch集群cluster主要功能詳細(xì)分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-04-04