併發:Java中的13個原子操作類。
當程式更新一個變數時,如果多執行緒同時更新這個變數,可能得到期望之外的值,比如變數i=1,A執行緒更新i+1,B執行緒也更新i+1,經過兩個執行緒操作之後可能i不等於3,而是等於2。因為A和B執行緒在更新變數i的時候拿到的i都是1,這就是執行緒不安全的更新操作,通常我們會使用synchronized來解決這個問題,synchronized會保證多執行緒不會同時更新變數i。
而Java從JDK 1.5開始提供了java.util.concurrent.atomic包(以下簡稱Atomic包),這個包中的原子操作類提供了一種用法簡單、效能高效、執行緒安全的更新一個變數的方式。
因為變數的型別有很多種,所以在Atomic包裡一共提供了13個類,屬於4種類型的原子更新方式,分別是原子更新基本型別、原子更新陣列、原子更新引用和原子更新屬性(欄位)。Atomic包裡的類基本都是使用Unsafe實現的包裝類。
原子更新基本型別類
使用原子的方式更新基本型別,Atomic包提供了以下3個類。
- AtomicBoolean:原子更新布林型別。
- AtomicInteger:原子更新整型。
- AtomicLong:原子更新長整型。
以上3個類提供的方法幾乎一模一樣,所以以AtomicInteger為例進行講解,AtomicInteger的常用方法如下。
- int addAndGet(int delta):以原子方式將輸入的數值與例項中的值(AtomicInteger裡的value)相加,並返回結果。
- boolean compareAndSet(int expect, int update):如果輸入的數值等於預期值,則以原子方式將該值設定為輸入的值。
- int getAndIncrement():以原子方式將當前值加1,注意,這裡返回的是自增前的值。
- void lazySet(int newValue):最終會設定成newValue,使用lazySet設定值後,可能會導致其他執行緒在之後的一段時間內還是可以讀到舊的值。
- int getAndSet(int newValue):以原子方式設定為newValue的值,並返回舊值。
AtomicInteger示例程式碼如下所示。
public class AtomicIntegerTest { static AtomicInteger ai = new AtomicInteger(1); public static void main(String[] args) { System.out.println(ai.getAndIncrement()); System.out.println(ai.get()); } }
那麼getAndIncrement是如何實現原子操作的呢?讓我們一起分析其實現原理,getAndIncrement的原始碼如下所示。
public final int getAndIncrement() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return current;
}
}
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
原始碼中for迴圈體的第一步先取得AtomicInteger裡儲存的數值,第二步對AtomicInteger的當前數值進行加1操作,關鍵的第三步呼叫compareAndSet方法來進行原子更新操作,該方法先檢查當前數值是否等於current,等於意味著AtomicInteger的值沒有被其他執行緒修改過,則將AtomicInteger的當前數值更新成next的值,如果不等compareAndSet方法會返回false,程式會進入for迴圈重新進行compareAndSet操作。
Atomic包提供了3種基本型別的原子更新,但是Java的基本型別裡還有char、float和double等。那麼問題來了,如何原子的更新其他的基本型別呢?Atomic包裡的類基本都是使用Unsafe實現的,讓我們一起看一下Unsafe的原始碼,如下所示。
/**
* 如果當前數值是expected,則原子的將Java變數更新成x
* @return 如果更新成功則返回true
*/
public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object x);
public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x);
public final native boolean compareAndSwapLong(Object o, long offset, long expected, long x);
通過程式碼,我們發現Unsafe只提供了3種CAS方法:compareANdSwapObject、compareAndSwapInt和compareAndSwapLong,再看AtomicBoolean原始碼,發現他是先把Boolean轉換成整型,再使用compareAndSwapInt進行CAS,所以原子更新char、float和double變數也可以用類似的思路來實現。
原子更新陣列
通過原子的方式更新數組裡的某個元素,Atomic包提供了以下4個類。
- AtomicIntegerArray:原子更新整型數組裡的元素。
- AtomicLongArray:原子更新整型數組裡的元素。
- AtomicReferenceArray:原子更新引用型別數組裡的元素。
AtomicIntegerArray類主要是提供原子的方式更新數組裡的整型,其常用方法如下。
- int addANdGet(int i, int delta):以原子方式將輸入值與陣列中索引i的元素相加。
- boolean compareAndSet(int i, int expect, int update):如果當前值等於預期值,則以原子方式將陣列位置i的元素設定成update值。
以上幾個類提供的方法幾乎一樣,所以這裡僅以AtomicIntegerArray為例進行講解,AtomicIntegerArray的使用示例程式碼如下所示。
public class AtomicIntegerArrayTest {
static int[] value = new int[]{1,2};
static AtomicIntegerArray ai = new AtomicIntegerArray(value);
public static void main(String[] args) {
ai.getAndSet(0, 3);
System.out.println(ai.get(0));
System.out.println(value[0]);
}
}
需要注意的是,陣列value通過構造方法傳遞進去,然後AtomicIntegerArray會將當前陣列複製一份,所以當AtomicIntegerArray對內部的陣列元素進行修改時,不會影響傳入的陣列。
原子更新引用型別
原子更新基本型別的AtomicInteger,只能更新一個變數,如果要原子更新多個變數,就需要使用這個原子更新引用型別提供的類。Atomic包提供了以下3個類。
- AtomicReference:原子更新引用型別。
- AtomicReferenceFieldUpdater:原子更新引用型別裡的欄位。
- AtomicMarkableReference:原子更新帶有標記位的引用型別。可以原子更新一個布林型別的標記位和引用型別。構造方法是AtomicMarkableReference(V initialRef, boolean initialMark)。
以上幾個類提供的方法幾乎一樣,所以這裡以AtomicReference為例進行講解,AtomicReference的使用示例程式碼如下所示。
public class AtomicReferenceTest {
public static AtomicReference<User> atomicUserRef = new AtomicReference<User>();
public static void main(String[] args) {
User user = new User("hello", 18);
atomicUserRef.set(user);
User updateUser = new User("world", 20);
atomicUserRef.compareAndSet(user, updateUser);
System.out.println(atomicUserRef.get().getName());
System.out.println(atomicUserRef.get().getOld());
}
static class User {
private String name;
private int old;
public User(String name, int old) {
this.name = name;
this.old = old;
}
public String getName() {
return name;
}
public int getOld() {
return old;
}
}
}
程式碼中首先構建一個user物件,然後把user物件設定進AtomicReference中,最後呼叫compareAndSet方法進行原子更新操作,實現原理同AtomicInteger裡的compareAndSet方法。
原子更新欄位類
如果需原子的更新某個類裡的某個欄位時,就需要使用原子更新欄位類,Atomic包提供了以下3個類進行原子欄位更新。
- AtomicIntegerFieldUpdater:原子更新整型的欄位的更新器。
- AtomicLongFieldUpdater:原子更新長整型欄位的更新器。
- AtomicStampedReference:原子更新帶有版本號的引用型別。該類將整數值與引用關聯起來,可用於原子的更新資料和資料的版本號,可以解決使用CAS進行原子更新時可能出現的ABA問題。
要想原子的更新欄位類需要兩步。第一步,因為原子更新欄位類都是抽象類,每次使用的時候必須使用靜態方法newUpdater()建立一個更新器,並且需要設定想要更新的類和屬性。第二步,更新類的欄位(屬性)必須使用public volatile修飾符。
以上3個類提供的方法幾乎一樣,所以這裡僅以AstomicIntegerFieldUpdater為例進行講解,AstomicIntegerFieldUpdater的示例程式碼如下所示。
public class AstomicIntegerFieldUpdaterTest {
// 建立原子更新器,並設定需要更新的物件類和物件的屬性
private static AtomicIntegerFieldUpdater<User> a = AtomicIntegerFieldUpdater.newUpdater(User.class, "old");
public static void main(String[] args) {
// 設定年齡是10歲
User user = new User("test", 10);
// 長了一歲,但是仍然會輸出舊的年齡
System.out.println(a.getAndIncrement(user));
// 輸出現在的年齡
System.out.println(a.get(user));
}
public static class User {
private String name;
public volatile int old;
public User(String name, int old) {
this.name = name;
this.old = old;
}
public String getName() {
return name;
}
public int getOld() {
return old;
}
}
}