關於java併發的三大特性,原子性、可見性、有序性關乎線程安全問題的基本原理,JDK提供java.util.concurrent.atomic包來對數據類型進行包裝,以實現各數據類型的原子性操作。
關於三大特性與volatile關鍵字可參考文章深入理解volatile關鍵字。
接下來就通過一道簡單的題目層層解讀AtomicInteger的底層原理。
demo1:i++的原子性問題
public class AtomicIntegerDemo1 {
private int value = 0;
public void add() {
value++;
}
public static void main(String[] args) throws InterruptedException {
AtomicIntegerDemo1 atomicIntegerDemo1 = new AtomicIntegerDemo1();
for (int i = 0; i < 20; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
atomicIntegerDemo1.add();
}
}).start();
}
Thread.sleep(1000);//等待所有線程執行完畢再輸出
System.out.println(atomicIntegerDemo1.value);
}
}
這是一道簡單的題目,創建20個線程,每個線程對共享變量value進行1000次自加操作。不論運行多少次,結果都不是我們期望的20000:
究其原因,假設現在value值爲1,線程1將變量value的值從堆空間(線程共享)讀取到虛擬機棧(線程私有)中進行自加操作value值變爲2,再將新值2刷新到堆內存。這個過程中可能線程1還沒來得及刷新到堆內存,線程2也讀取value值到自己的棧內存,然後將新值2刷新到堆內存。這樣兩個線程對變量value一共自加兩次,value值應當是3,而實際value的值仍然是2。這就是線程安全問題。本質原因還是因爲value++操作是非原子操作。
要實現線程安全方法有兩種:
- 同步阻塞:通過sychronized關鍵字或者Lock對象加鎖,
public void sychronized add() { value++; }
,保證add()方法的原子性。同步阻塞方式優點是安全,實現簡單,缺點是併發效率低。 - 非阻塞併發:通過CAS(比較並交換)機制實現樂觀鎖,只有噹噹前值與期望值一樣時,纔將舊值替換爲當前值,否則重新讀取當前值,這種循環重試機制也叫自旋鎖。異步併發的優點是性能高,CPU利用率高,線程持續運行,省去了CPU線程調度的性能消耗,缺點是如果當前值一直不與期望值一樣,將會一直循環重試。樂觀鎖還可能出現ABA問題。
以上代碼的運行結果並不會是我們期望的20000。將add()方法設置爲同步方法可以解決問題,那再來看看AtomicInteger如何解決原子性問題的。
注意:以上代碼中給value屬性添加volatile關鍵字並不能解決問題,因爲volatile關鍵字只有保證可見性和有序性的語義,而不能保證原子性。
demo2:使用AtomicInteger類
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicIntegerDemo2 {
private AtomicInteger value = new AtomicInteger();
public void add() {
value.getAndAdd(1);
}
public static void main(String[] args) throws InterruptedException {
AtomicIntegerDemo2 atomicIntegerDemo2 = new AtomicIntegerDemo2();
for (int i = 0; i < 20; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
atomicIntegerDemo2.add();
}
}).start();
}
Thread.sleep(1000);//等待所有線程執行完畢再輸出
System.out.println(atomicIntegerDemo2.value);
}
}
用AtomicInteger類對象的getAndAdd()方法代替int類型的i++操作,最後輸出結果是我們期望的20000.
那麼,AtomicInteger是如何實現原子性運算的呢。點開jdk源碼
查看AtomicInteger的getAndAdd()方法:
public final int getAndAdd(int delta) {
return unsafe.getAndAddInt(this, valueOffset, delta);
}
有一個unsafe對象,
private static final Unsafe unsafe = Unsafe.getUnsafe();
這個unsafe對象所屬類Unsafe提供了許多native方法,比如
public native int getIntVolatile(Object var1, long var2);
用getIntVolatile()在底層通過對象(var1)和偏移量(var2)獲取變量內存地址,直接通過內存地址而不是符號引用得到變量的當前值。有了對象(var1),屬性地址偏移量(var2),舊值(var5 ),加上期望值(var5 + var4),就可以調用CAS操作(compareAndSwapInt方法)。unsafe對象的getAndAddInt方法如下:
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);//第4行
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));//第5行
return var5;
}
噹噹前線程執行第4行後,變量值被其他線程刷新,則當前線程執行第5行方法返回false,重新執行第4行獲取值,直到當前值與期望值一樣,才修改變量的值並返回新值。
看完了源碼想仿造源碼自己動手寫一下AtomicInteger以加深印象。
demo3:仿造源碼實現的AtomicInteger類
import sun.misc.Unsafe;
public class AtomicIntegerDemo3 {
private volatile int value;
//提供底層CAS操作的對象
private static final Unsafe unsafe=Unsafe.getUnsafe();
//屬性在內存中相對於對象的地址偏移量
private static final long valueOffset;
static{
try {
//通過unsafe對象提供的方法獲取value屬性偏移量
valueOffset = unsafe.objectFieldOffset(AtomicIntegerDemo3.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
/**
* CAS操作方法
* @param current 舊值
* @param update 期望值
* @return 修改是否成功
*/
public boolean compareAndSet(int current,int update){
return unsafe.compareAndSwapInt(this,valueOffset,current,update);
}
//相當於AtomicInteger中getAndAdd()方法:將給定的值原子地添加到當前值
public void add() {
int current;
do {
//線程獲取當前值
current = unsafe.getIntVolatile(this,valueOffset);
}while (!compareAndSet(current,current+1));//修改失敗則重試
}
public static void main(String[] args) throws InterruptedException {
AtomicIntegerDemo3 atomicIntegerDemo3 = new AtomicIntegerDemo3();
for (int i = 0; i < 20; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
atomicIntegerDemo3.add();
}
}).start();
}
Thread.sleep(1000);//等待所有線程執行完畢再輸出
System.out.println(atomicIntegerDemo3.value);
}
}
想法很簡單,拿到底層內存地址操作相關的底層Unsafe類的實例,根據本類的value屬性用unsafe對象的objectFieldOffset()方法獲取屬性相對本類實例的地址偏移量,然後通過循環CAS操作改變屬性值。
但是運行時報了異常:
第8行private static final Unsafe unsafe=Unsafe.getUnsafe();
報錯,點開getUnsafe()方法,
@CallerSensitive
public static Unsafe getUnsafe() {
Class var0 = Reflection.getCallerClass();
if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
throw new SecurityException("Unsafe");
} else {
return theUnsafe;
}
}
再點開VM.isSystemDomainLoader()方法:
public static boolean isSystemDomainLoader(ClassLoader var0) {
return var0 == null;
}
可以看出爲直接調用getUnsafe()方法是不安全的,sunjdk設定只要調用類類加載器不爲空,則拋出異常並提示Unsafe,也就是該方法只能虛擬機自己調用。
既然我們不能直接調用getUnsafe()方法來獲取Unsafe的實例,那就用反射來獲取吧。
demo4:手寫AtomicInteger
import sun.misc.Unsafe;
import java.lang.reflect.Field;
public class AtomicIntegerDemo4 {
private volatile int value;
//提供底層CAS操作的對象
private static final Unsafe unsafe;
//屬性在內存中相對於對象的地址偏移量
private static final long valueOffset;
static{
try {
//getUnsafe()方法不好使,通過反射獲取unsafe對象
Field field = Unsafe.class.getDeclaredField("theUnsafe");
//Unsafe類中的theUnsafe屬性爲private,設置可訪問權限
field.setAccessible(true);
unsafe = (Unsafe) field.get(null);
//通過unsafe對象提供的方法獲取value屬性偏移量
valueOffset = unsafe.objectFieldOffset
(AtomicIntegerDemo4.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
/**
* CAS操作方法
* @param current 舊值
* @param update 期望值
* @return 修改是否成功
*/
public boolean compareAndSet(int current,int update){
return unsafe.compareAndSwapInt(this,valueOffset,current,update);
}
//相當於AtomicInteger中getAndAdd()方法
public void add() {
int current;
do {
//線程獲取當前值
current = unsafe.getIntVolatile(this,valueOffset);
}while (!compareAndSet(current,current+1));//修改失敗則重試
}
public static void main(String[] args) throws InterruptedException {
AtomicIntegerDemo4 atomicIntegerDemo4 = new AtomicIntegerDemo4();
for (int i = 0; i < 20; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
atomicIntegerDemo4.add();
}
}).start();
}
Thread.sleep(1000);//等待所有線程執行完畢再輸出
System.out.println(atomicIntegerDemo4.value);
}
}
修改過後通過反射獲取unsafe對象,運行成功,能夠正確輸出20000:
雖然只是完成了getAndAdd()一個方法,但是通過閱讀源碼和仿造手寫對Atomic原子類的原理有了一定的瞭解。
另外,測試發現,把value屬性的volatile關鍵字去掉也不影響結果,jdk源碼添加的關鍵字應該是不會多餘的,猜測是與unsafe.getIntVolatile()這個方法有關,但是這是個native方法不能查看。
望有大神不吝賜教0.0