Java?CAS與Atomic原子操作核心原理詳解
什么是原子操作
Mysql事務中的原子性就是一個事務中執(zhí)行的多條sql,要么同時成功,要么同時失敗,他們不可拆分。并發(fā)中的原子操作也一樣,多個線程中,站在線程A的角度看線程B的操作,線程B的操作就是一個原子的;站在線程B的角度看線程A,線程A的操作是原子的。一整個操作要么全部執(zhí)行完了,要么就沒有執(zhí)行,中間不能拆分。
那么要怎么實現(xiàn)原子性嘞?可以使用synchronized鎖來保證一段代碼的原子性,但是加鎖影響性能,甚至還有死鎖方面的問題需要考慮。
所以鎖機制是比較重量級的,粒度較大的一種機制,比如對于計數(shù)器方面的操作來說,可能加鎖的耗時都比整個計算的耗時還要高。Java 就提供了 Atomic 系列的原子操作類,在java.util.concurrent.atomic包下
這些原子操作類是基于處理器的CAS指令來實現(xiàn)原子性的,Compare and swap。比較并且交換
CAS
每個CAS操作過程基本上都包含三個部分:內(nèi)存地址V、期望值A(chǔ)、新值B
期望值就是舊值,首先會去內(nèi)存地址中進行比較,我期望當前這個內(nèi)存地址中的值是我期望的舊值,如果是則把新值賦值到這個內(nèi)存地址中,如果不是則不做任何事。在一般的使用中我們會不斷嘗試去進行CAS操作,直到成功為止。
Java 中的 Atomic 系列的原子操作類的實現(xiàn)則是利用了循環(huán) CAS 來實現(xiàn)。
使用CAS實現(xiàn)原子操作的幾個問題
ABA問題
ABA問題在大多數(shù)場景下,不解決其實也沒什么影響。
解決思路:添加版本戳,在變量前面追加上版本號,每次變量更新的時候把版本號加 1,那么 A-->B-->A 就會變成 1A-->2B-->3A
循環(huán)時間長,對于cpu來說開銷較大
只能保證一個共享變量的原子操作
對于多個共享變量操作時就無法使用CAS來保證原子性了,這個時候還是需要用鎖。
還有一個取巧的辦法,就是把多個共享變量合并成一個共享變量來操作。比如,有兩個共享變量 i=2,j=a,合并一下 ij=2a,然后用 CAS 來操作 ij。
從 Java 1.5開始,JDK 提供了AtomicReference類來保證引用對象之間的原子性,就可以把多個變量放在一個對象里來進行 CAS 操作。
相關(guān)原子操作類的使用
這些類的用戶都大同小異,這里就拿幾個典型來舉例

AtomicInteger
// 以原子方式將給定值添加到當前值,然后將相加后的結(jié)果返回
public final int addAndGet(int delta){}
// 指定期望值與修改后的值,如果期望值和當前值相同則進行更新操作
public final boolean compareAndSet(int expect, int update) {}
// 先返回當前值,然后再進行原子自增1
public final int getAndIncrement() {}
// 先返回當前值,然后進行原子更新操作
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){}
// 先比較,期望值和當前值相同再執(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ù)組不會變化
System.out.println(value[0]);
}
}
Process finished with exit code 0
// 輸出結(jié)果
3
1
需要注意的是,數(shù)組 value 通過構(gòu)造方法傳遞進去,然后 AtomicIntegerArray會將當前數(shù)組復制一份,所以當 AtomicIntegerArray 對內(nèi)部的數(shù)組元素進行修改 時,不會影響傳入的數(shù)組。
更新引用類型
如果要同時更新多個原子變量就需要使用更新引用類型提供的類了。Atomic提供了三個類:
AtomicReference
原子更新引用類型
案例:
public class UseAtomicReference {
public static AtomicReference<UserInfo> atomicUserRef;
public static void main(String[] args) {
//要修改的實體的實例
UserInfo user = new UserInfo("Mark", 15);
atomicUserRef = new AtomicReference(user);
// 再創(chuàng)建一個對象
UserInfo updateUser = new UserInfo("Bill",17);
// 期望值和當前值相同就進行修改
atomicUserRef.compareAndSet(user,updateUser);
System.out.println(atomicUserRef.get());
System.out.println(user);
/*
輸出結(jié)果:
UserInfo{name='Bill', age=17}
UserInfo{name='Mark', age=15}
*/
}
/**
* 定義一個實體類
*/
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
利用版本戳的形式記錄了每次改變以后的版本號,這樣的話就不會存在 ABA問題了
AtomicMarkableReference
原子更新帶有標記位的引用類型。可以原子更新一個布爾類型的標記位和引 用類型。
構(gòu)造方法是 AtomicMarkableReference(V initialRef,booleaninitialMark)。
AtomicMarkableReference跟 AtomicStampedReference 差不多,
AtomicStampedReference 是使用 pair 的 int stamp 作為計數(shù)器使用
AtomicMarkableReference 的使用pair 的boolean mark。
AtomicStampedReference 可能關(guān)心的是動過幾次,AtomicMarkableReference 關(guān)心的是有沒有被人動過。
案例:
// 第二個線程,期望的時間戳和當前時間戳不同,所以更新不成功
public class UseAtomicStampedReference {
static AtomicStampedReference<String> asr = new AtomicStampedReference("mark", 0);
public static void main(String[] args) throws InterruptedException {
//拿到當前的版本號(舊)
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() + ":當前變量值:"
+ oldReference + "-當前版本戳:" + 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() + ":當前變量值:"
+ reference + "-當前版本戳:" + 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:當前變量值:mark-當前版本戳:0
true
Thread-1:當前變量值:mark+Java-當前版本戳:1
false
mark+Java============1
原子更新字段類
如果需原子地更新某個類里的某個字段時,就需要使用原子更新字段類
Atomic 包提供了以下 3 個類進行原子字段更新。 要想原子地更新字段類需要兩步。
因為原子更新字段類都是抽象類, 每次使用的時候必須使用靜態(tài)方法 newUpdater()創(chuàng)建一個更新器,并且需要設(shè)置想要更新的類和屬性。
更新類的字段(屬性)必須使用 public volatile修飾符。
- AtomicIntegerFieldUpdater:原子更新整型的字段的更新器。
- AtomicLongFieldUpdater:原子更新長整型字段的更新器。
- AtomicReferenceFieldUpdater:原子更新引用類型里的字段。
LongAdder
并發(fā)量較少,自旋的沖突也就較少。但如果并發(fā)很多的情況下,CAS機制就不如synchronized了,因為很多個線程都集中判斷一個變量的值,不斷的自旋,對cpu的消耗也較大,同一時刻又只會一個線程更新成功。
在JDK1.8就引入了LongAdder類,它在處理上面問題的時候是采用的一種熱點數(shù)據(jù)的分散寫
LongAdder中有兩個成員變量
// 當為非空時,大小為 2 的冪。 // 如果并發(fā)很高就使用cell數(shù)組做寫熱點的分散,其中某些線程共同操作某一個數(shù)組中的元素 transient volatile Cell[] cells; // 當爭搶較少時使用這個變量來進行cas,就類似于AtomicInteger類中的value變量 transient volatile long base;
然后調(diào)用sum()方法將數(shù)組cells和base變量的中做一個匯總,返回當前總和。在沒有并發(fā)更新的情況下調(diào)用將返回準確的結(jié)果,但在計算總和時發(fā)生的并發(fā)更新可能不會合并,所以sum()方法并不能保證強一致性,它返回的只是一個近似值
// 可以看到 sum()方法沒有任何加鎖的邏輯
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)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java數(shù)據(jù)結(jié)構(gòu)之鏈表相關(guān)知識總結(jié)
今天給大家?guī)黻P(guān)于Java數(shù)據(jù)結(jié)構(gòu)的相關(guān)知識,文章圍繞Java鏈表展開,文中有非常詳細的介紹及代碼示例,需要的朋友可以參考下2021-06-06
Java 中使用數(shù)組存儲和操作數(shù)據(jù)
本文將介紹Java中常用的數(shù)組操作方法,通過詳細的示例和解釋,幫助讀者全面理解和掌握這些方法,具有一定的參考價值,感興趣的可以了解一下2023-09-09
Java基礎(chǔ)之引用相關(guān)知識總結(jié)
今天聊聊Java的引用,大多數(shù)時候我們說引用都是強引用,只有在對象不使用的情況下才會釋放內(nèi)存,其實Java 內(nèi)存有四種不同的引用.一起看看吧,,需要的朋友可以參考下2021-05-05

