Java Atomic總結
所謂 Atomic
,翻譯過來就是原子。原子被認為是操作中最小的單位,一段程式碼如果是原子的,則表示這段程式碼在執行過程中,要麼執行成功,要麼執行失敗。原子操作一般都是底層通過 CPU
的指令來實現。而 atomic
包下的這些類,則可以讓我們在多執行緒環境下,通過一種無鎖的原子操作來實現執行緒安全。
atomic
包下的類基本上都是藉助 Unsafe
類,通過 CAS
操作來封裝實現的。Unsafe
這個類不屬於 Java
標準,或者說這個類是 Java
預留的一個後門類,JDK
中,有關提升效能的 concurrent
或者 NIO
等操作,大部分都是藉助於這個類來封裝操作的。Java
是種編譯型語言,不像 C
JVM
進行記憶體的建立回收等操作,但這個類提供了一些直接操作記憶體相關的底層操作,使得我們也可以手動操作記憶體,但從類的名字就可以看出,這個類不是安全的,官方也是不建議我們使用的。
CAS原理
CAS
包含 3
個引數 CAS(V,E,N)
. V
表示要更新的變數,E
表示預期值,N
表示新值.
僅當V
值等於E
值時,才會將V
的值設為N
,如果V
值和E
值不同,則說明已經有其他執行緒做了更新,則當前執行緒什麼都不做. 最後,CAS
返回當前V
的真實值. CAS
操作是抱著樂觀的態度進行的,它總是認為自己可以成功完成操作.
當多個執行緒同時使用CAS
操作一個變數時,只有一個會勝出,併成功更新,其餘均會失敗.失敗的執行緒不會被掛起,僅是被告知失敗,並且允許再次嘗試,當然也允許失敗的執行緒放棄操作.基於這樣的原理,CAS
在 JDK8
的 atomic
包下,大概有 16
個類,按照原子的更新方式,大概可以分為 4
類:原子更新普通型別,原子更新陣列,原子更新引用,原子更新欄位。
原子更新普通型別
atomic
包下提供了三種基本型別的原子更新,分別是 AtomicBoolean
,AtomicInteger
,AtomicLong
,這幾個原子類對應於基礎型別的布林,整形,長整形,至於 Java
中其他的基本型別,如 float
等,如果需要,可以參考這幾個類的原始碼自行實現。
AtomicBoolean
主要介面
public final boolean get();
public final boolean compareAndSet(boolean expect,boolean update);
public boolean weakCompareAndSet(boolean expect,boolean update);
public final void set(boolean newValue);
public final void lazySet(boolean newValue);
public final boolean getAndSet(boolean newValue);複製程式碼
這裡面的操作都很正常,主要都是用到了 CAS
。這個類中的方法不多,基本上上面都介紹了,而內部的計算則是先將布林轉換為數字0/1
,然後再進行後續計算。
AtomicLong
主要介面
public final long get();
public final void set(long newValue);
public final void lazySet(long newValue);
public final long getAndSet(long newValue);
public final boolean compareAndSet(long expect,long update);
public final boolean weakCompareAndSet(long expect,long update);
public final long getAndIncrement();
public final long getAndDecrement();
public final long getAndAdd(long delta);
public final long incrementAndGet();
public final long decrementAndGet();
public final long addAndGet(long delta);
public final long getAndUpdate(LongUnaryOperator updateFunction);
public final long updateAndGet(LongUnaryOperator updateFunction);複製程式碼
這個和下面要講的 AtomicInteger
類似,下面具體說下。
AtomicInteger
主要介面
// 取得當前值
public final int get();
// 設定當前值
public final void set(int newValue);
// 設定新值,並返回舊值
public final int getAndSet(int newValue);
// 如果當前值為expect,則設定為u
public final boolean compareAndSet(int expect,int u);
// 當前值加1,返回舊值
public final int getAndIncrement();
// 當前值減1,返回舊值
public final int getAndDecrement();
// 當前值增加delta,返回舊值
public final int getAndAdd(int delta);
// 當前值加1,返回新值
public final int incrementAndGet();
// 當前值減1,返回新值
public final int decrementAndGet();
// 當前值增加delta,返回新值
public final int addAndGet(int delta);複製程式碼
實現
// 封裝了一個int對其加減
private volatile int value;
.......
public final boolean compareAndSet(int expect,int update) {
// 通過unsafe 基於CPU的CAS指令來實現,可以認為無阻塞.
return unsafe.compareAndSwapInt(this,valueOffset,expect,update);
}
.......
public final int getAndIncrement() {
for (;;) {
// 當前值
int current = get();
// 預期值
int next = current + 1;
if (compareAndSet(current,next)) {
// 如果加成功了,則返回當前值
return current;
}
// 如果加失敗了,說明其他執行緒已經修改了資料,與期望不相符,// 則繼續無限迴圈,直到成功. 這種樂觀鎖,理論上只要等兩三個時鐘週期就可以設值成功
// 相比於直接通過synchronized獨佔鎖的方式操作int,要大大節約等待時間.
}
}複製程式碼
用一個簡單的例子測試下:
AtomicInteger atomicInteger = new AtomicInteger(1);
System.out.println(atomicInteger.incrementAndGet()); // 2
System.out.println(atomicInteger.getAndIncrement()); // 2
System.out.println(atomicInteger.getAndAccumulate(2,(i,j) -> i + j)); // 3
System.out.println(atomicInteger.get()); // 5
System.out.println(atomicInteger.addAndGet(5)); 複製程式碼
原子更新陣列
atomic
包下提供了三種陣列相關型別的原子更新,分別是 AtomicIntegerArray
,AtomicLongArray
,AtomicReferenceArray
,對應於整型,長整形,引用型別,要說明的一點是,這裡說的更新是指更新陣列中的某一個元素的操作。
由於方法和更新基本型別方法相同,這裡只簡單看下 AtomicIntegerArray
這個類的幾個方法,其他的方法類似。
AtomicIntegerArray
主要介面
// 獲得陣列第i個下標的元素
public final int get(int i);
// 獲得陣列的長度
public final int length();
// 將陣列第i個下標設定為newValue,並返回舊的值
public final int getAndSet(int i,int newValue);
// 進行CAS操作,如果第i個下標的元素等於expect,則設定為update,設定成功返回true
public final boolean compareAndSet(int i,int expect,int update);
// 將第i個下標的元素加1
public final int getAndIncrement(int i);
// 將第i個下標的元素減1
public final int getAndDecrement(int i);
// 將第i個下標的元素增加delta(delta可以是負數)
public final int getAndAdd(int i,int delta);複製程式碼
實現
// 陣列本身基地址
private static final int base = unsafe.arrayBaseOffset(int[].class);
// 封裝了一個陣列
private final int[] array;
static {
// 陣列中物件的寬度,int型別,4個位元組,scale = 4;
int scale = unsafe.arrayIndexScale(int[].class);
if ((scale & (scale - 1)) != 0)
throw new Error("data type scale not a power of two");
// 前導0 : 一個數字轉為二進位制後,他前面0的個數
// 對於4來講,他就是00000000 00000000 00000000 00000100,他的前導0 就是29
// 所以shift = 2
shift = 31 - Integer.numberOfLeadingZeros(scale);
}
// 獲取第i個元素
public final int get(int i) {
return getRaw(checkedByteOffset(i));
}
// 第i個元素,在陣列中的偏移量是多少
private long checkedByteOffset(int i) {
if (i < 0 || i >= array.length)
throw new IndexOutOfBoundsException("index " + i);
return byteOffset(i);
}
// base : 陣列基地址,i << shift,其實就是i * 4,因為這邊是int array.
private static long byteOffset(int i) {
// i * 4 + base
return ((long) i << shift) + base;
}
// 根據偏移量從陣列中獲取資料
private int getRaw(long offset) {
return unsafe.getIntVolatile(array,offset);
}複製程式碼
用一個簡單的例子測試一下:
AtomicIntegerArray array = new AtomicIntegerArray(5);
array.set(0,1); // 設定陣列第一個值為1
System.out.println(array.getAndDecrement(0)); // 1
System.out.println(array.addAndGet(0,5)); // 5複製程式碼
原子更新引用
更新引用型別的原子類包含了AtomicReference
(更新引用型別),AtomicReferenceFieldUpdater
(抽象類,更新引用型別裡的欄位),AtomicMarkableReference
(更新帶有標記的引用型別)這三個類,這幾個類能同時更新多個變數。
AtomicReference
與 AtomicInteger
類似,只是裡面封裝了一個物件,而不是 int
,對引用進行修改。
主要介面
public final V get();
public final void set(V newValue);
public final boolean compareAndSet(V expect,V update);
public final V getAndSet(V newValue);複製程式碼
測試使用 10
個執行緒,同時嘗試修改 AtomicReference
中的 String
,最終只有一個執行緒可以成功。
import java.util.concurrent.atomic.AtomicReference;
public class AtomicReferenceTest {
public final static AtomicReference<String> attxnicStr = new AtomicReference<String>("abc");
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread() {
public void run() {
try {
Thread.sleep(Math.abs((int) (Math.random() * 100)));
} catch (InterruptedException e) {
e.printStackTrace();
}
if (attxnicStr.compareAndSet("abc","def")) {
System.out.println("Thread:" + Thread.currentThread().getId() + " change value to " + attxnicStr.get());
} else {
System.out.println("Thread:" + Thread.currentThread().getId() + " change failed!");
}
}
}.start();
}
}
}複製程式碼
原子更新欄位
如果更新的時候只更新物件中的某一個欄位,則可以使用 atomic
包提供的更新欄位型別:AtomicIntegerFieldUpdater
,AtomicLongFieldUpdater
和 AtomicStampedReference
,前兩個顧名思義,就是更新 int
和 long
型別,最後一個是更新引用型別,該類提供了版本號,用於解決通過 CAS
進行原子更新過程中,可能出現的 ABA
問題。前面這兩個類和上面介紹的 AtomicReferenceFieldUpdater
有些相似,都是抽象類,都需要通過 newUpdater
方法進行例項化,並且對欄位的要求也是一樣的。
AtomicStampedReference
ABA問題
執行緒一準備用 CAS
將變數的值由 A
替換為 B
,在此之前執行緒二將變數的值由 A
替換為 C
,執行緒三又將 C
替換為A
,然後執行緒一執行 CAS
時發現變數的值仍然為 A
,所以執行緒一 CAS
成功.
主要介面
// 比較設定 引數依次為:期望值 寫入新值 期望時間戳 新時間戳
public boolean compareAndSet(V expectedReference,V newReference,int expectedStamp,int newStamp)
// 獲得當前物件引用
public V getReference()
// 獲得當前時間戳
public int getStamp()
// 設定當前物件引用和時間戳
public void set(V newReference,int newStamp)複製程式碼
分析
// 內部封裝了一個Pair物件,每次對物件操作的時候,stamp + 1
private static class Pair<T> {
final T reference;
final int stamp;
private Pair(T reference,int stamp) {
this.reference = reference;
this.stamp = stamp;
}
static <T> Pair<T> of(T reference,int stamp) {
return new Pair<T>(reference,stamp);
}
}
private volatile Pair<V> pair;
// 進行cas操作的時候,會對比stamp的值
public boolean compareAndSet(V expectedReference,V newReference,int newStamp) {
Pair<V> current = pair;
return
expectedReference == current.reference &&
expectedStamp == current.stamp &&
((newReference == current.reference &&
newStamp == current.stamp) ||
casPair(current,Pair.of(newReference,newStamp)));
}複製程式碼
測試
要求:後臺使用多個執行緒對使用者充值,要求只能充值一次.
public class AtomicStampedReferenceDemo {
static AtomicStampedReference<Integer> money=new AtomicStampedReference<Integer>(19,0);
public staticvoid main(String[] args) {
//模擬多個執行緒同時更新後臺資料庫,為使用者充值
for(int i = 0 ; i < 3 ; i++) {
final int timestamp=money.getStamp();
newThread() {
public void run() {
while(true){
while(true){
Integerm=money.getReference();
if(m<20){
if(money.compareAndSet(m,m+20,timestamp,timestamp+1)){
System.out.println("餘額小於20元,充值成功,餘額:"+money.getReference()+"元");
break;
}
}else{
//System.out.println("餘額大於20元,無需充值");
break ;
}
}
}
}
}.start();
}
//使用者消費執行緒,模擬消費行為
new Thread() {
publicvoid run() {
for(int i=0;i<100;i++){
while(true){
int timestamp=money.getStamp();
Integer m=money.getReference();
if(m>10){
System.out.println("大於10元");
if(money.compareAndSet(m,m-10,timestamp+1)){
System.out.println("成功消費10元,餘額:"+money.getReference());
break;
}
}else{
System.out.println("沒有足夠的金額");
break;
}
}
try {Thread.sleep(100);} catch (InterruptedException e) {}
}
}
}.start();
}
}複製程式碼
AtomicIntegerFieldUpdater
能夠讓普通變數也能夠進行原子操作。
主要介面
public static <U> AtomicIntegerFieldUpdater<U> newUpdater(Class<U> tclass,String fieldName);
public int incrementAndGet(T obj);複製程式碼
-
Updater
只能修改它可見範圍內的變數。因為Updater
使用反射得到這個變數。如果變數不可見,就會出錯。比如如果score
申明為private
,就是不可行的。 - 為了確保變數被正確的讀取,它必須是
volatile
型別的。如果我們原有程式碼中未申明這個型別,那麼簡單得申明一下就行。 - 由於
CAS
操作會通過物件例項中的偏移量直接進行賦值,因此,它不支援static
欄位(Unsafe.objectFieldOffset()
不支援靜態變數)。
測試
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
public class AtomicIntegerFieldUpdaterDemo {
public static class Candidate {
int id;
// 如果直接把int改成atomicinteger,可能對程式碼破壞比較大
// 因此使用AtomicIntegerFieldUpdater對score進行封裝
volatile int score;
}
// 通過反射實現
public final static AtomicIntegerFieldUpdater<Candidate> scoreUpdater = AtomicIntegerFieldUpdater.newUpdater(Candidate.class,"score");
// 檢查Updater是否工作正確,allScore的結果應該跟score一致
public static AtomicInteger allScore = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
final Candidate stu = new Candidate();
Thread[] t = new Thread[10000];
for (int i = 0; i < 10000; i++) {
t[i] = new Thread() {
public void run() {
if (Math.random() > 0.4) {
scoreUpdater.incrementAndGet(stu);
allScore.incrementAndGet();
}
}
};
t[i].start();
}
for (int i = 0; i < 10000; i++) {
t[i].join();
}
System.out.println("score=" + stu.score);
System.out.println("allScore=" + allScore);
}
}複製程式碼
JDK8之後引入的型別
在JDK8
之前,針對原子操作,我們基本上可以通過上面提供的這些類來完成我們的多執行緒下的原子操作,不過在併發高的情況下,上面這些單一的 CAS
+ 自旋操作的效能將會是一個問題,所以上述這些類一般用於低併發操作。而針對這個問題,JDK8
又引入了下面幾個類:DoubleAdder
,LongAdder
,DoubleAccumulator
,LongAccumulator
,這些類是對AtomicLong
這些類的改進與增強,這些類都繼承自Striped64
這個類。
參考部落格:Java高併發之無鎖與Atomic原始碼分析
本文由部落格一文多發平臺 OpenWrite 釋出!
更多內容請點選我的部落格沐晨