原子變量最主要的一個特點就是所有的操作都是原子的,synchronized關鍵字也可以做到對變量的原子操作。只是synchronized的成本相對較高,需要獲取鎖對象,釋放鎖對象,如果不能獲取到鎖,還需要阻塞在阻塞隊列上進行等待。而如果單單只是為了解決對變量的原子操作,建議使用原子變量。關於原子變量的介紹,主要涉及以下內容:
- 原子變量的基本概念
- 通過AtomicInteger了解原子變量的基本使用
- 通過AtomicInteger了解原子變量的基本原理
- AtomicReference的基本使用
- 使用FieldUpdater操作非原子變量的字段屬性
- 經典的ABA問題的解決
一、原子變量的基本概念
原子變量保證了該變量的所有操作都是原子的,不會因為多線程的同時訪問而導致臟數據的讀取問題。我們先看一段synchronized關鍵字保證變量原子性的代碼:
public class Counter {
private int count;
public synchronized void addCount(){
this.count++;
}
}
簡單的count++操作,線程對象首先需要獲取到Counter 類實例的對象鎖,然后完成自增操作,最后釋放對象鎖。整個過程中,無論是獲取鎖還是釋放鎖都是相當消耗成本的,一旦不能獲取到鎖,還需要阻塞當前線程等等。
對於這種情況,我們可以將count變量聲明成原子變量,那么對於count的自增操作都可以以原子的方式進行,就不存在臟數據的讀取了。Java給我們提供了以下幾種原子類型:
- AtomicInteger和AtomicIntegerArray:基於Integer類型
- AtomicBoolean:基於Boolean類型
- AtomicLong和AtomicLongArray:基於Long類型
- AtomicReference和AtomicReferenceArray:基於引用類型
在本文的余下內容中,我們將主要介紹AtomicInteger和AtomicReference兩種類型,AtomicBoolean和AtomicLong的使用和內部實現原理幾乎和AtomicInteger一樣。
二、AtomicInteger的基本使用
首先看它的兩個構造函數:
private volatile int value;
public AtomicInteger(int initialValue) {
value = initialValue;
}
public AtomicInteger() {
}
可以看到,我們在通過構造函數構造AtomicInteger原子變量的時候,如果指定一個int的參數,那么該原子變量的值就會被賦值,否則就是默認的數值0。
也有獲取和設置這個value值的方法:
public final int get()
public final void set(int newValue)
當然,這兩個方法並不是原子的,所以一般也很少使用,而以下的這些基於原子操作的方法則相對使用的頻繁,至於它們的具體實現是怎樣的,我們將在本文的后續小節中進行簡單的學習。
//基於原子操作,獲取當前原子變量中的值並為其設置新值
public final int getAndSet(int newValue)
//基於原子操作,比較當前的value是否等於expect,如果是設置為update並返回true,否則返回false
public final boolean compareAndSet(int expect, int update)
//基於原子操作,獲取當前的value值並自增一
public final int getAndIncrement()
//基於原子操作,獲取當前的value值並自減一
public final int getAndDecrement()
//基於原子操作,獲取當前的value值並為value加上delta
public final int getAndAdd(int delta)
//還有一些反向的方法,比如:先自增在獲取值的等等
下面我們實現一個計數器的例子,之前我們使用synchronized實現過,現在我們使用原子變量再次實現該問題。
//自定義一個線程類
public class MyThread extends Thread {
public static AtomicInteger value = new AtomicInteger();
@Override
public void run(){
try {
Thread.sleep((long) ((Math.random())*100));
//原子自增
value.incrementAndGet();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//main函數中啟動100條線程並讓他們啟動
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[100];
for (int i=0;i<100;i++){
threads[i] = new MyThread();
threads[i].start();
}
for (int j=0;j<100;j++){
threads[j].join();
}
System.out.println("value:"+MyThread.value);
}
多次運行會得到相同的結果:
很顯然,使用原子變量要比使用synchronized要簡潔的多並且效率也相對較高。
三、AtomicInteger的內部基本原理
AtomicInteger的實現原理有點像我們的包裝類,內部主要操作的是value字段,這個字段保存就是原子變量的數值。value字段定義如下:
private volatile int value;
首先value字段被volatile修飾,即不存在內存可見性問題。由於其內部實現原子操作的代碼幾乎類似,我們主要學習下incrementAndGet方法的實現。
在揭露該方法的實現原理之前,我們先看另一個方法:
public final boolean compareAndSet(int expect, int update{
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
compareAndSet方法又被稱為CAS,該方法調用unsave的一個compareAndSwapInt方法,這個方法是native,我們看不到源碼,但是我們需要知道該方法完成的一個目標:比較當前原子變量的值是否等於expect,如果是則將其修改為update並返回true,否則直接返回false。當然,這個操作本身就是原子的,較為底層的實現。
在jdk1.7之前,我們的incrementAndGet方法是這樣實現的:
public final int incrementAndGet() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return next;
}
}
方法體是一個死循環,current獲取到當前原子變量中的值,由於value被修飾volatile,所以不存在內存可見性問題,數據一定是最新的。然后current加一后賦值給next,調用我們的CAS原子操作判斷value是否被別的線程修改過,如果還是原來的值,那么將next的值賦值給value並返回next,否則重新獲取當前value的值,再次進行判斷,直到操作完成。
incrementAndGet方法的一個很核心的思想是,在加一之前先去看看value的值是多少,真正加的時候再去看一下,如果發現變了,不操作數據,否則為value加一。
但是在jdk1.8以后,做了一些優化,但是最后還是調用的compareAndSwapInt方法。但基本思想還是沒變。
四、AtomicReference的基本使用
對於一些自定義類或者字符串等這些引用類型,Java並發包也提供了原子變量的接口支持。AtomicReference內部使用泛型來實現的。
private volatile V value;
public AtomicReference(V initialValue) {
value = initialValue;
}
public AtomicReference() {
}
有關其他的一些原子方法如下:
//獲取並設置value的值為newvalue
public final V getAndSet(V newValue) {
return (V)unsafe.getAndSetObject(this, valueOffset, newValue);
}
AtomicReference中少了一些自增自減的操作,但是對於value的修改依然是原子的。
五、使用FieldUpdater操作非原子變量的字段屬性
FieldUpdater允許我們不必將字段設置為原子變量,利用反射直接以原子方式操作字段。例如:
//定義一個計數器
public class Counter {
private volatile int count;
public int getCount() {
return count;
}
public void addCount(){
AtomicIntegerFieldUpdater<Counter> updater = AtomicIntegerFieldUpdater.newUpdater(Counter.class,"count");
updater.getAndIncrement(this);
}
}
然后我們創建一百個線程隨機調用同一個Counter對象的addCount方法,無論運行多少次,結果都是一百。這種方式實現的原子操作,對於被操作的變量不需要被包裝成原子變量,但是卻可以直接以原子方式操作它的數值。
六、經典的ABA問題
我們的原子變量都依賴一個核心的方法,那就是CAS。這個方法最核心的思想就是,更改變量值之前先獲取該變量當前最新的值,然后在實際更改的時候再次獲取該變量的值,如果沒有被修改,那么進行更改,否則循環上述操作直至更改操作完成。假如一個線程想要對變量count進行修改,實際操作之前獲取count的值為A,此時來了一個線程將count值修改為B,又來一個線程獲取count的值為B並將count修改為A,此時第一個線程全然不知道count的值已經被修改兩次了,雖然值還是A,但是實際上數據已經是臟的。
這就是典型的ABA問題,一個解決辦法是,對count的每次操作都記錄下當前的一個時間戳,這樣當我們原子操作count之前,不僅查看count的最新數值,還記錄下該count的時間戳,在實際操作的時候,只有在count的數值和時間戳都沒有被更改的情況之下才完成修改操作。
public static void main(String[] args){
int count=0;
int stamp = 1;
AtomicStampedReference reference = new AtomicStampedReference(count,stamp);
int next = count++;
reference.compareAndSet(count, next, stamp, stamp);
}
AtomicStampedReference 的CAS方法要求傳入四個參數,該方法的內部會同時比較count和stamp,只有這兩個值都沒有發生改變的前提下,CAS才會修改count的值。
上述我們介紹了有關原子變量的最基本內容,最后我們比較下原子變量和synchronized關鍵字的區別。
從思維模式上看,原子變量代表一種樂觀的非阻塞式思維,它假定沒有別人會和我同時操作某個變量,於是在實際修改變量的值的之前不會鎖定該變量,但是修改變量的時候是使用CAS進行的,一旦發現沖突,繼續嘗試直到成功修改該變量。
而synchronized關鍵字則是一種悲觀的阻塞式思維,它認為所有人都會和我同時來操作某個變量,於是在將要操作該變量之前會加鎖來鎖定該變量,進而繼續操作該變量。