7.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設定值後,可能導致其他執行緒在之後的一小段時間內還是可以讀到舊的值。關於該方法的更多資訊可以參考併發程式設計網翻譯的一篇文章《AtomicLong.lazySet是如何工作的?》,文章地址是“http://ifeve.com/how-does-atomiclong-lazyset-work/”。
-
int getAndSet(int newValue):以原子方式設定為newValue的值,並返回舊值
程式碼清單7-1 AtomicIntegerTest.java
import java.util.concurrent.atomic.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());
}
}
輸出結果如下。
1
2
那麼getAndIncrement是如何實現原子操作的呢?讓我們一起分析其實現原理,getAndIncrement的原始碼如程式碼清單7-2所示。
程式碼清單7-2 AtomicInteger.java
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的原始碼,如程式碼清單7-3所示。
程式碼清單7-3 Unsafe.java
/**
* 如果當前數值是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的使用例項程式碼如程式碼清單7-4所示。
程式碼清單7-4 AtomicIntegerArrayTest.java
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]);
}
}
以下是輸出的結果。
3
1
需要注意的是,陣列value通過構造方法傳遞進去,然後AtomicIntegerArray會將當前陣列複製一份,所以當AtomicIntegerArray對內部的陣列元素進行修改時,不會影響傳入的陣列。
三、原子更新引用型別
原子更新基本型別的AtomicInteger,只能更新一個變數,如果要原子更新多個變數,就需要使用這個原子更新引用型別提供的類。Atomic包提供了以下3個類。
-
AtomicReference:原子更新引用型別。
-
AtomicReferenceFieldUpdater:原子更新引用型別裡的欄位。
-
AtomicMarkableReference:原子更新帶有標記位的引用型別。可以原子更新一個布林型別的標記位和引用型別。構造方法是AtomicMarkableReference(V initialRef,boolean initialMark)。
以上幾個類提供的方法幾乎一樣,所以本節僅以AtomicReference為例進行講解,AtomicReference的使用示例程式碼如程式碼清單7-5所示。
程式碼清單7-5 AtomicReferenceTest.java
public class AtomicReferenceTest {
public static AtomicReference<user> atomicUserRef = new AtomicReference<user>();
public static void main(String[] args) {
User user = new User("conan", 15);
atomicUserRef.set(user);
User updateUser = new User("Shinichi", 17);
atomicUserRef.compareAndSet(user, updateUser);
System.out.println(atomicUserRef.get().getName());
System.out.println(atomicUserRef.get().getOld());
}
@data
@gouzaoqi
static class User {
private String name;
private int old;
}
}
程式碼中首先構建一個user物件,然後把user物件設定進AtomicReferenc中,最後呼叫compareAndSet方法進行原子更新操作,實現原理同AtomicInteger裡的compareAndSet方法。程式碼執行後輸出結果如下。
Shinichi
17
四、原子更新欄位類
如果需原子地更新某個類裡的某個欄位時,就需要使用原子更新欄位類,Atomic包提供了以下3個類進行原子欄位更新。
-
AtomicIntegerFieldUpdater:原子更新整型的欄位的更新器。
-
AtomicLongFieldUpdater:原子更新長整型欄位的更新器。
-
AtomicStampedReference:原子更新帶有版本號的引用型別。該類將整數值與引用關聯起來,可用於原子的更新資料和資料的版本號,可以解決使用CAS進行原子更新時可能出現的ABA問題。
要想原子地更新欄位類需要兩步。
1、第一步,因為原子更新欄位類都是抽象類,每次使用的時候必須使用靜態方法newUpdater()建立一個更新器,並且需要設定想要更新的類和屬性。
2、第二步,更新類的欄位(屬性)必須使用public volatile修飾符。
以上3個類提供的方法幾乎一樣,所以本節僅以AstomicIntegerFieldUpdater為例進行講解,AstomicIntegerFieldUpdater的示例程式碼如程式碼清單7-6所示。
程式碼清單7-6 AtomicIntegerFieldUpdaterTest.java
public class AtomicIntegerFieldUpdaterTest {
// 建立原子更新器,並設定需要更新的物件類和物件的屬性
private static AtomicIntegerFieldUpdater<User> a =
AtomicIntegerFieldUpdater.newUpdater(User.class, "old");
public static void main(String[] args) {
// 設定柯南的年齡是10歲
User conan = new User("conan", 10);
// 柯南長了一歲,但是仍然會輸出舊的年齡
System.out.println(a.getAndIncrement(conan));
// 輸出柯南現在的年齡
System.out.println(a.get(conan));
}
@data
@AllArgsConstructor
public static class User {
private String name;
public volatile int old;
}
}
程式碼執行後輸出如下。
10
11
五、本章小結
本章介紹了JDK中併發包裡的13個原子操作類以及原子操作類的實現原理,讀者需要熟悉這些類和使用場景,在適當的場合下使用它