一、從a++說起為什麼使用AtomicInteger
我們知道java併發機制中主要有三個特性需要我們去考慮,原子性、可見性和有序性。volatile關鍵字可以保證可見性和有序性卻無法保證原子性。而這個AtomicInteger的作用就是為了保證原子性。我們先看一個例子。
public class Test { //一個變量a private static volatile int a = 0; public static void main(String[] args) { Test test = new Test(); Thread[] threads = new Thread[5]; //定義5個線程,每個線程加10 for (int i = 0; i < 5; i++) { threads[i] = new Thread(() -> { try { for (int j = 0; j < 10; j++) { System.out.println(a++); Thread.sleep(500); } } catch (Exception e) { e.printStackTrace(); } }); threads[i].start(); } } }
在上面的這個例子中,我們定義了一個變量a。並且使用了5個線程分別去增加。為了保證可見性和有序性我們使用了volatile關鍵字對a進行修飾。在這裡我們只測試原子性。如果我們第一次接觸的話肯定會覺得5個線程,每個線程加10,最後結果一定是50呀。我們可以運行一邊測試一波。
很明顯,可能跟你想象的不一樣。為什麼會出現這個問題呢?這是因為變量a雖然保證了可見性和有序性,但是缺沒有保證原子性。其原因我們可以來分析一下。
對於a++的操作,其實可以分解為3個步驟。
(1)從主存中讀取a的值
(2)對a進行加1操作
(3)把a重新刷新到主存
這三個步驟在單線程中一點問題都沒有,但是到了多線程就出現了問題了。比如說有的線程已經把a進行了加1操作,但是還沒來得及重新刷入到主存,其他的線程就重新讀取了舊值。因為才造成了錯誤。如何去解決呢?方法當然很多,但是為了和我們今天的主題對應上,很自然的聯想到使用AtomicInteger。下面我們使用AtomicInteger重新來測試一遍:
public class Test3 { //使用AtomicInteger定義a static AtomicInteger a = new AtomicInteger(); public static void main(String[] args) { Test3 test = new Test3(); Thread[] threads = new Thread[5]; for (int i = 0; i < 5; i++) { threads[i] = new Thread(() -> { try { for (int j = 0; j < 10; j++) { //使用getAndIncrement函數進行自增操作 System.out.println(a.incrementAndGet()); Thread.sleep(500); } } catch (Exception e) { e.printStackTrace(); } }); threads[i].start(); } } }
在上面的代碼中我們使用了AtomicInteger來定義a,而且使用了AtomicInteger的函數incrementAndGet來對a進行自增操作。現在我們再來測試一遍。
現在使用了AtomicInteger,不管你測試多少次,最後結果一定是50。為什麼會出現這樣的結果呢?AtomicInteger又是如何保證了這樣的特性呢?下面我們就正式的開始揭開其面紗。
二、原理分析
上面的例子中我們只是調用了incrementAndGet函數來進行自增操作。其實AtomicInteger類為我們提供了很多函數。可以先使用一下。
1、基本使用
public class Test4 { private static AtomicInteger atomicInteger = new AtomicInteger(); //1、獲取當前值 public static void getCurrentValue(){} //2、設置value值 public static void setValue(){} //3、先獲取舊值,然後設置新值 public static void getAndSet(){} //4、先取得舊值,然後再進行自增 public static void getAndIncrement(){} //5、先獲取舊值,然後再減少 public static void getAndDecrement(){} //6、先獲取舊值,然後再加10 public static void getAndAdd(){} //7、先加1.然後獲取新值 public static void incrementAndGet(){} //8、先減1,然後獲取新值 public static void decrementAndGet(){} 、 //9、先增加,然後再獲取新值 public static void addAndGet(){} }
最常用的方法就是這麼幾個。當然了還有很多其他的方法。對於上面幾個函數,每一個函數的意思都已經列了出來。意思都很簡單。下面我們就通過源碼的角度分析一下AtomicInteger的真正原理。
2、源碼分析
既然AtomicInteger使用了incrementAndGet函數,那我們就直接來看這個方法,對於其他的方法也是同樣的道理。我們直接看源碼,這裡使用的是jdk1.8的版本,不同的版本會有出入。
/** * Atomically increments by one the current value. * @return the updated value */ public final int incrementAndGet() { return unsafe.getAndAddInt(this, valueOffset, 1) + 1; }
在這裡我們會看到,底層使用的是unsafe的getAndAddInt方法。這裡你可能有一個疑問了,這個unsafe是個什麼鬼,而且還有一個valueOffset參數又是什麼,想要看明白,我們從源碼的開頭開始看起。
public class AtomicInteger extends Number implements java.io.Serializable { private static final long serialVersionUID = 6214790243416807050L; // setup to use Unsafe.compareAndSwapInt for updates private static final Unsafe unsafe = Unsafe.getUnsafe(); private static final long valueOffset; static { try { valueOffset = unsafe.objectFieldOffset (AtomicInteger.class.getDeclaredField("value")); } catch (Exception ex) { throw new Error(ex); } } private volatile int value; //這裡還有更多的代碼沒有列出 }
開頭在Unsafe的上面會發現,有一行註釋叫做Unsafe.compareAndSwapInt。這又是什麼?帶著這些疑問我們開始一點一點揭開其面紗。
(1)compareAndSwapInt的含義
compareAndSwapInt又叫做CAS,如果你將來找工作,這個不清楚的話,基本上可以告別java這個方向了。
CAS 即比較並替換,實現併發算法時常用到的一種技術。CAS操作包含三個操作數——內存位置、預期原值及新值。執行CAS操作的時候,將內存位置的值與預期原值比較,如果相匹配,那麼處理器會自動將該位置值更新為新值,否則,處理器不做任何操作。
我看過無數篇文章,對這個概念都是這樣解釋的,但是一開始看會一臉懵逼。我們使用一個例子來解釋相信你會更加的清楚。
比如說給你兒子訂婚。你兒子就是內存位置,你原本以為你兒子是和楊貴妃在一起了,結果在訂婚的時候發現兒子身邊是西施。這時候該怎麼辦呢?你一氣之下不做任何操作。如果兒子身邊是你預想的楊貴妃,你一看很開心就給他們訂婚了,也叫作執行操作。現在你應該明白了吧。
對於CAS的解釋我不準備長篇大論講解。因為裡面涉及到的知識點還是挺多的。在這裡你理解了其含義就好。
(2)Unsafe的含義
在上面我們主要是講解了CAS的含義,CAS修飾在Unsafe上面。那這個Unsafe是什麼意思呢?
Unsafe是位於sun.misc包下的一個類,Unsafe類使Java語言擁有了類似C語言指針一樣操作內存空間的能力,這無疑也增加了程序發生相關指針問題的風險。在程序中過度、不正確使用Unsafe類會使得程序出錯的概率變大,使得Java這種安全的語言變得不再“安全”,因此對Unsafe的使用一定要慎重。
這裡說一句題外話,在jdk1.9中,對Usafe進行了刪除,所以因為這,那些基於Usafe開發的框架慢慢的都死掉了。
在這裡也就是說,Usafe再進行getAndAddInt的時候,首先是先加1,然後對底層對象的地址做出了更改。這個地址是什麼呢?這就是涉及到我們的第三個疑問參數了。
(3)valueOffset的含義
這個valueOffset是long類型的,代表的含義就是對象的地址的偏移量。下面我們重新解釋一下這行代碼。
unsafe.getAndAddInt(this, valueOffset, 1) + 1。這行代碼的含義是,usafe通過getAndAddInt方法,對原先對象的地址進行了加1操作。現在應該明白了。我們return的時候,也是直接返回的最新的值。這一點我們對比另外一個方法incrementAndGet就能看出。
/** * Atomically increments by one the current value. * * @return the previous value */ public final int getAndIncrement() { return unsafe.getAndAddInt(this, valueOffset, 1); }
在這個方法的源代碼中我們可以看到最後的+1操作沒有了,也就是說,直接返回的是舊地址的值,然後再進行自增操作。如何去拿的地址的偏移量呢?是通過下面這個代碼。
static { try { valueOffset = unsafe.objectFieldOffset (AtomicInteger.class.getDeclaredField("value")); } catch (Exception ex) { throw new Error(ex); } }
OK,到了這一步相信你已經知道了,usafe對a的值使用getAndAddInt方法進行了加1操作。然後返回最新的值。那麼這個getAndAddInt方法是如何實現的呢?我們可以在進入看看:
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; }
這段代碼的含義也很清晰。底層還是通過compareAndSwapInt這個CAS機制來完成的增加操作,
第一個參數var1表示的是當前對象,也就是a。
第二個參數var2表示的是地址偏移量
第三個參數var3表示的是我們要增加的值,這裡表示為1
對於AtomicInteger的原理就是這,主要是通過Usafe的方式來完成的。Usafe又是通過CAS機制來實現的,因此想要弄清整個原子系列的真正實現,就是要搞清楚CAS機制。不過我會在下一章節進行講解。
3、其他方法
對於其他方法其實也是同樣的道理,我們可以給出幾個看看。
public final int getAndAdd(int delta) { return unsafe.getAndAddInt(this, valueOffset, delta); } public final int incrementAndGet() { return unsafe.getAndAddInt(this, valueOffset, 1) + 1; } public final int decrementAndGet() { return unsafe.getAndAddInt(this, valueOffset, -1) - 1; } public final int addAndGet(int delta) { return unsafe.getAndAddInt(this, valueOffset, delta) + delta; }
我們可以看到底層基本上還是Usafe來實現的。Usafe又是經過CAS實現。
三、總結
對於jdk1.8的併發包來說,底層基本上就是通過Usafe和CAS機制來實現的。有好處也肯定有一個壞處。從好的方面來講,就是上面AtomicInteger類可以保持其原子性。但是從壞的方面來看,Usafe因為直接操作的底層地址,肯定不是那麼安全,而且CAS機制也伴隨著大量的問題,比如說有名的ABA問題等等。關於CAS機制,我也會在後續的文章中專門講解。大家可以先根據那個給兒子訂婚的例子有一個基本的認識。