目錄
4 ABA問題以及AtomicStampedReference的使用
1 Atomic類作用
前面文章我們知道volatile只能保證可見性,不能保證原子性,下面我們通過代碼證明一下
// 定義一個volatile變量
private volatile int value = 0;
@Test
public void volatileTest() throws InterruptedException {
// 循環10000次,每次自增1
for (int i = 0; i < 10000; i ++) {
new Thread(() -> value ++).start();
}
TimeUnit.SECONDS.sleep(1);
System.out.println(value);
}
通過測試我們可以看到程序輸出結果不確定的,大都達不到10000,可以說明transient不能保證原子性。
爲了滿足jvm內存模型原子性特性,juc併發包提供了Atomic類來保證原子性。
2 AtomicInteger使用
2.1 AtomicInteger用法
同樣的功能我們用AtomicInteger來測試
private AtomicInteger value = new AtomicInteger();
@Test
public void atomicIntegerTest() throws InterruptedException {
for (int i = 0; i < 10000; i ++) {
new Thread(() -> value.addAndGet(1)).start();
}
TimeUnit.SECONDS.sleep(1);
System.out.println(value);
}
每次輸出的結果都是10000,可以說明AtomicInteger保證原子性
2.2 AtomicInteger原理
AtomicInteger原理也比較簡單,就是前面文章講過的CAS自旋volatile變量。
2.2.1 重要成員變量
// 定義一個volatile變量
private volatile int value;
// value屬性的內存偏移量,在static塊中初始化
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
2.2.2 addAndGet方法
public final int addAndGet(int delta) {
return unsafe.getAndAddInt(this, valueOffset, delta) + delta;
}
/**
* var1:當前對象
* var2:某個屬性的內存地址偏移量
* var4: 增量
*/
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
// 通過當前對象和和某個屬性內存偏移量讀取屬性值
var5 = this.getIntVolatile(var1, var2);
// 循環給對象賦值,直到成功
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
2.3 AtomicInteger優缺點
優點:無鎖,效率高
缺點:cpu佔用較高,只能保證一個共享變量原子操作,ABA問題
解決方案
1 無解,高性能是建立在高CPU佔用的基礎上實現的
2 AtomicReference
3 AtomicStampedReference (多了一個stamp變量,可以理解爲版本號)
3 AtomicReference使用
3.1 AtomicReference用法
創建一個員工類,有一個id和age屬性,多線程對這個員工的id和age同時+1,循環10000次,觀察是否會有併發問題
@Data
public class Employee implements Serializable {
private static final long serialVersionUID = 7773021982257206440L;
private int id;
private int age;
測試代碼
@Test
public void atomicReferenceTest() throws InterruptedException {
for (int i = 0; i < 10000; i ++) {
new Thread(() -> employeeAtomicReference.updateAndGet(employee -> {
Employee newEmp = new Employee();
newEmp.setId(employee.getId() + 1);
newEmp.setAge(employee.getAge() + 1);
return newEmp;
})).start();
}
TimeUnit.SECONDS.sleep(1);
System.out.println(employeeAtomicReference.get());
}
輸出Employee{id=10000, age=10000}
3.2 AtomicReference原理
3.2.1 重要成員變量
// 定義一個泛型volatile變量
private volatile V value;
// value屬性的內存偏移量,在static塊中初始化
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
3.2.3 updateAndGet方法
public final V updateAndGet(UnaryOperator<V> updateFunction) {
V prev, next;
do {
// 獲取當前對象
prev = get();
// 獲取執行後的對象
next = updateFunction.apply(prev);
// CAS更新對象爲next
} while (!compareAndSet(prev, next));
return next;
}
/**
* expect 希望的當前值
* update 要修改的值
*/
public final boolean compareAndSet(V expect, V update) {
return unsafe.compareAndSwapObject(this, valueOffset, expect, update);
}
3.3.3 AtomicReference使用誤區
在使用AtomicReference的時候做對象操作的時候一定要用一個新建的對象,否則不能保證原子性
錯誤用例:
@Test
public void atomicReferenceTest() throws InterruptedException {
for (int i = 0; i < 10000; i ++) {
new Thread(() -> employeeAtomicReference.updateAndGet(employee -> {
// 這裏直接用原來的對象進行修改
employee.setId(employee.getId() + 1);
employee.setAge(employee.getAge() + 1);
return employee;
})).start();
}
TimeUnit.SECONDS.sleep(1);
System.out.println(employeeAtomicReference.get());
}
輸出:Employee{id=9995, age=9996}
問題原因:
看CAS參數compareAndSet(prev, next)方法,prev參數和next參數實際上都是AtomicReference內部引用的employee對象
所以該方法始終返回true,達不到併發控制效果
4 ABA問題以及AtomicStampedReference的使用
4.1 ABA問題
整個CAS過程分爲3步,查詢當前值,比較希望的值(第一步取到的當前值)和當前值,修改。這裏假設有3個線程操作數據
上個流程圖可知線程一先獲取數據,然後掛起。
線程二和線程三修改數據,但是值沒有變。
這個時候線程一不能感知數據已經被修改過,進行CAS修改成功。這就是ABA問題
4.2 AtomicStampedReference使用
AtomicStampedReference解決ABA問題的方法也很簡單,在原有的CAS方法在expectValue的基礎上多了一個expectStamp條件的判斷,
不僅expectedValue要和當前值相等,expectedStamp也要和當前值相等。
測試代碼
private AtomicStampedReference<Integer> value = new AtomicStampedReference<>(0, 0);
@Test
public void atomicReferenceTest() throws InterruptedException {
System.out.println(value.compareAndSet(0, 1, 1, 1));
System.out.println(value.getReference() + " " + value.getStamp());
System.out.println(value.compareAndSet(0, 1, 0, 1));
System.out.println(value.getReference() + " " + value.getStamp());
}
輸出
false
0 0
true
1 1
4.3 AtomicStampedReference原理
4.3.1 重要成員變量
// 定義一個靜態內部類,維護reference和stamp兩個屬性
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);
}
}
// 定義一個volatile的Pair對象
private volatile Pair<V> pair;
private static final long pairOffset = objectFieldOffset(UNSAFE, "pair", AtomicStampedReference.class);
4.3.2 compareAndSet方法
public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp) {
Pair<V> current = pair;
// CAS的時候要保證expectedReference和當前reference相等,expectedStamp和當前stamp相等,才能進行CAS處理
return
expectedReference == current.reference &&
expectedStamp == current.stamp &&
((newReference == current.reference &&
newStamp == current.stamp) ||
casPair(current, Pair.of(newReference, newStamp)));
}
// CAS修改成員變量pair
private boolean casPair(Pair<V> cmp, Pair<V> val) {
return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
}