原子操作類(一)原子操作類詳細介紹
引言
??Java從JDK1.5開始提供了java.util.concurrent.atomic包,方便程序員在多線程環境下,無鎖的進行原子操作。原子變量的底層使用了處理器提供的原子指令,但是不同的CPU架構可能提供的原子指令不一樣,也有可能需要某種形式的內部鎖,所以該方法不能絕對保證線程不被阻塞。
??因為變量的類型有很多種,所以在Atomic包裏一共提供了13個類,可分為4種類型的原子更新方式,分別是
- 原子更新基本類型
- 原子更新數組
- 原子更新引用
- 原子更新屬性(字段)
Atomic包裏的類基本都是使用Unsafe實現的包裝類。
一、基本類型的原子操作類
Atomic 包提供了以下3個基本類型的原子操作類:
- AtomicBoolean: 原子更新布爾類型;
- AtomicInteger: 原子更新整形;
- AtomicLong: 原子更新長整形;
以上3個類提供的方法幾乎一模一樣,所以本文僅以AtomicInteger為例,常用方法如下:
- int addAndGet(int delta): 以原子方式將輸入的數值與實例中的值(AtomicInteger裏的value)相加,並返回結果
- boolean compareAndSet(int expect,int update): 如果輸入的數值等於預期值,則以原子方式將該值設置為輸入的值
- int getAndIncrement(): 以原子方式將當前值加1,註意,這裏返回的是自增前的值,相當於
(i++)
- void lazySet(int newValue): JDK6所增加,最終會設置成newValue,使用lazySet設置值後,可能導致其他線程在之後的一小段時間內還是可以讀到舊的值。關於該方法的更多信息可以參考並發編程網翻譯的一篇文章《AtomicLong.lazySet是如何工作的?》,文章地址是“http://ifeve.com/howdoes-atomiclong-lazyset-work/”
- int getAndSet(int newValue): 以原子方式設置為newValue的值,並返回舊值。
@ Example1 ?AtomicInteger 使用例子
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
其余的基本類的原子操作類的解決方案
??java 的基本類型一共有8個,然而JDK卻只提供了三個,如何原子的更新其他的基本類型呢?特別是常用的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方法,那麽 AtomicBoolean
是如何實現的呢,我們再來看一下 AtomicBoolean
的源碼。只給出關鍵部分
public class AtomicBoolean implements java.io.Serializable {
//存儲值:1,0;1代表 true,0 代表 false
private volatile int value;
//Unsafe 對象
private static final Unsafe unsafe = Unsafe.getUnsafe();
public final boolean compareAndSet(boolean expect, boolean update) {
//boolean 類型 轉換成 整形,true 對應 1,false 對應 0;
int e = expect ? 1 : 0;
int u = update ? 1 : 0;
return unsafe.compareAndSwapInt(this, valueOffset, e, u);
}
public final void set(boolean newValue) {
value = newValue ? 1 : 0
}
public final boolean get() {
return value != 0;
}
//.......
}
??從AtomicBoolean
的源碼可以看出,它是先把 boolean
的值轉換成整型來存儲,再使用compareAndSwapInt進行CAS。所以其他基本類型的原子類型也可參照這個思路來實現。
??思路有了,接下來考慮怎麽實現。byte
、char
與 int
的轉換很容易,他們的原子操作類就很容易實現了。那float
,double
要怎樣才能以整形或者長整形來存儲值呢?
??Float
類中 提供了static兩個方法:通過 Static int floatToIntBits(float value)
(根據 IEEE 754 浮點“單一格式”位布局,返回指定浮點值的表示形式)方法,便可以將float 的值以整形的方式存儲下來。如果要取值,則通過 static float intBitsToFloat(int bits)
方法,將存儲下來的整形的浮點值轉換回原來的float
值。
??同理,Double
類也提供了類似的兩個方法:static long doubleToLongBits(double value)
產生long
類型的浮點值。static double longBitsToDouble(long bits)
將long
的浮點值轉換回double
值。
下面是 AtomicDouble 的實現例子:
@ Example2 ?AtomicDouble 的實現例子
import java.util.concurrent.atomic.AtomicLong;
public class AtomicDouble extends Number {
private AtomicLong bits;
public AtomicDouble() {
this(0d);
}
public AtomicDouble(Double value){
bits = new AtomicLong(Double.doubleToLongBits(value));
}
public final double get() {
return Double.longBitsToDouble(bits.get());
}
public final void set(double value) {
bits.set(Double.doubleToLongBits(value));
}
public final double addAndGet(double delta) {
long newBits = bits.addAndGet(Double.doubleToLongBits(delta));
return Double.longBitsToDouble(newBits);
}
public final double getAndAdd(double delta) {
long oldBits = bits.getAndAdd(Double.doubleToLongBits(delta));
return Double.longBitsToDouble(oldBits);
}
public final double getAndSet(double newValue) {
long oldBits = bits.getAndSet(Double.doubleToLongBits(newValue));
return Double.longBitsToDouble(oldBits);
}
public final boolean compareAndSet(double expect, double update) {
return bits.compareAndSet(Double.doubleToLongBits(expect), Double.doubleToLongBits(update));
}
public final boolean weakCompareAndSet(double expect, double update) {
return bits.weakCompareAndSet(Double.doubleToLongBits(expect), Double.doubleToLongBits(update));
}
public final void lazySet(double newValue) {
bits.lazySet(Double.doubleToLongBits(newValue));
}
@Override
public int intValue() { return (int) this.get(); }
@Override
public long longValue() { return (long) this.get(); }
@Override
public float floatValue() { return (float) this.get();}
@Override
public double doubleValue() { return this.get(); }
}
關於原子操作類實現其余猜想:
??除了上面的那種實現方案外,在 stackOverflow 上也發現兩個原子操作類的實現方案:
- 利用
AtomicReference
類;如Float
的原子操作類可以是AtomicReference<Float>
。不可,經過測試,這種方法是不行的,因為像compareAndSet
這類方法比較的是對象的內存地址,而不會使用equal()
方法進行比較。 - 使用JDK1.8新增的方法:
DoubleAdder
、DoubleAccumulator
。沒去深入了解,粗略看了下,這兩個類都是適合於“多寫少讀”的情況。
二、數組的原子操作類
通過原子的方式更新數組裏的某個元素,Atomic包提供了以下3個類:
- AtomicIntegerArray: 原子更新整型數組裏的元素
- AtomicLongArray: 原子更新長整型數組裏的元素
- AtomicReferenceArray: 原子更新引用類型數組裏的元素
這幾個類的方法幾乎一樣,以AtomicIntegerArray
為例,AtomicIntegerArray
類主要是提供原子的方式更新數組裏的整型;
構造方法:
AtomicIntegerArray(int length): 創建給定長度的新 AtomicIntegerArray。
AtomicIntegerArray(int[] array): 創建與給定數組具有相同長度的新 AtomicIntegerArray,並從給定數組復制其所有元素。
其常用方法:
int addAndGet(int i, int delta): 以原子方式將輸入值與數組中索引i的元素相加。
boolean compareAndSet(int i, int expect, int update): 如果當前值等於預期值,則以原子方式將數組位置i的元素設置成update值。
@ Example3 ?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]);
}
}
運行結果:
3
1
AtomicIntegerArray類需要註意的是 ,數組value通過構造方法傳遞進去,然後AtomicIntegerArray會將當前數組復制一份,所以當AtomicIntegerArray對內部的數組元素進行修改時,不會影響到傳入的數組。
三、引用類型的原子操作類
?基本類型的原子操作類只能更新基本類型的值,不能更新引用類型的對象引用。Atomic包提供了以下三個類,可以原子方式更新的對象引用。
- AtomicReference: 原子更新引用類型。
- AtomicStampedReference: 原子更新帶有版本號的引用類型。該類將整數值與引用關聯起來,可用於原子的更數據和數據的版本號,可以解決使用CAS進行原子更新時,可能出現的ABA問題。
- AtomicMarkableReference: 原子更新帶有標記位的引用類型。可以原子的更新一個布爾類型的標記位和引用類型。構造方法是AtomicMarkableReference(V initialRef, boolean initialMark)
1. AtomicReference 介紹
構造方法
AtomicReference(): 使用 null 初始值創建新的 AtomicReference
。
AtomicReference(V initialValue): 使用給定的初始值創建新的 AtomicReference
。
常用的方法: 與前面介紹的類差不多,get()
、set(V newValue)
、compareAndSet(V expect, V update)
、getAndSet(V newValue)
;
@ Example4 ?AtomicReference 的使用例子
public class AtomicReferenceTest {
public static AtomicReference<user> atomicUserRef = new AtomicReference</user><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());
}
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;
}
}
}
運行結果:
Shinichi
17
2. AtomicMarkableReference 與 AtomicStampedReference 介紹
??這兩個類的作用與 AtomicReference
的作用非常相似,都是原子更新引用類型。但他們還有一個作用:能解決CAS過程中的ABA問題。
什麽是ABA問題?
??ABA的問題就是:在多線程的環境下,線程1將A的值賦給了B,然後B的值又重新賦值了A。在這個過程中,A已經被修改了一次了,但是線程2不知道,在進行CAS時,認為A的值沒有被修改過,所以就進行修改。當然,如果只對結果敏感,而對修改的次數不敏感,那麽這個問題就無所謂了。
AtomicStampedReference 和 AtomicMarkableReference 是怎麽解決ABA問題的?
??這兩個類的解決方案也很簡答,就是多維護了一個標誌位記錄修改的狀態 或者 維護一個版本號記錄修改的次數,然後進行CAS時,也會比較標誌位或者版本號。簡單看一下源碼吧。
AtomicMarkableReference 用的是標誌位mark
(布爾類型)來標誌修改的狀態,下面是關鍵部分源碼:
//構造方法,initialRef 是 初始的引用類型,initialMark 是初始的標誌位
public AtomicMarkableReference(V initialRef, boolean initialMark);
//CAS 不僅要比較引用類型,還要比較標誌位
public boolean compareAndSet(V expectedReference, V newReference, boolean expectedMark, boolean newMark) {
Pair<V> current = pair;
return
expectedReference == current.reference &&
expectedMark == current.mark &&
((newReference == current.reference &&
newMark == current.mark) ||
casPair(current, Pair.of(newReference, newMark)));
}
AtomicStampedReference 用的則是版本號stamp
(整形int) 來記錄修改的次數,下面是關鍵部分的源代碼:
//構造方法,初始引用類型 和 初始版本號
public AtomicStampedReference(V initialRef, int initialStamp);
//CAS 不僅要比較引用類型,還要比較版本號
public boolean compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp) {
Pair<V> current = pair;
return
expectedReference == current.reference &&
expectedStamp == current.stamp &&
((newReference == current.reference &&
newStamp == current.stamp) ||
casPair(current, Pair.of(newReference, newStamp)));
}
@ Example5 ?AtomicStampedReference 的解決ABA問題的例子
看一個AtomicStampedReference
的例子。當需要執行一次更新當前用戶的操作,這裏故意將更新的用戶就是當前用戶,也就是更新後對象引用是沒有變化的。線程A、線程B 都接到這個任務,但只能執行一次,意味著得有一個線程更新失敗。User類的代碼請查看上一個例子。
public class AtomicStampedReferenceTest {
public static void main(String[] args) throws InterruptedException {
User user = new User("小赫",25);
//初始版本為1
int stamp = 1;
//設置當前用戶是 小赫
AtomicStampedReference<User> currentAtomicUser = new AtomicStampedReference<User>(user, stamp);
//更新當前用戶,更新的目標用戶就是當前用戶
UpdateUserTask task = new UpdateUserTask(stamp, currentAtomicUser, user, user);
//線程A,線程B執行相同的更新任務,但更新操作只需要執行一次。
Thread threadA = new Thread(task,"ThreadA");
Thread threadB = new Thread(task,"ThreadB");
threadA.start();
threadB.start();
}
}
class UpdateUserTask implements Runnable{
private int stamp;
private AtomicStampedReference<User> currentAtomicUser;
private User newUser;
private User oldUser;
public UpdateUserTask(int stamp, AtomicStampedReference<User> currentAtomicUser,User newUser,User oldUser) {
this.stamp = stamp;
this.currentAtomicUser = currentAtomicUser;
this.newUser = newUser;
this.oldUser = oldUser;
}
@Override
public void run() {
//更新當前用戶,版本號加一
boolean b = currentAtomicUser.compareAndSet(oldUser, newUser, stamp, stamp+1);
if(b){//更新執行成功
System.out.println("線程"+Thread.currentThread().getName()+": 成功執行更新操作");
}else{//更新失敗
System.out.println("線程"+Thread.currentThread().getName()+": 當前用戶是 "+ currentAtomicUser.getReference().getName()+" ,版本號已經過期,更新操作已經被其他線程完成");
}
}
}
運行結果:
線程ThreadB: 當前用戶是 小赫 ,版本號已經過期,更新操作已經被其他線程完成
線程ThreadA: 成功執行更新操作
??從結果可以看出,當線程B執行任務時,盡管當前用戶的對象引用沒有改變,但版本號卻已經改變了,線程B從而知道了更新操作已經被執行了,於是便不再執行更新。
四、 更新字段的原子操作類
如果我們只需要某個類裏的某個字段,那麽就需要使用原子更新字段類,Atomic包提供了以下三個類:
- AtomicIntegerFieldUpdater: 原子更新整型的字段的更新器。通過靜態工廠方法創建
newUpdater(Class<U> tclass, String fieldName)
; - AtomicLongFieldUpdater: 原子更新長整型字段的更新器。通過靜態工廠方法創建
newUpdater(Class<U> tclass, String fieldName)
; - AtomicReferenceFieldUpdater 原子更新引用類型裏的字段。比前兩個方法的範圍要廣,可以更新任意類型的字段,通過靜態的工廠方法創建
newUpdater(Class<U> tclass, Class<W> vclass, String fieldName)
;
原子更新字段類都是抽象類,每次使用都時候必須使用靜態方法newUpdater創建一個更新器。原子更新類的字段的必須使用public volatile修飾符。以上3個類提供的方法差不多,下面給出AtomicIntegerFieldUpdater
的例子:
@ Example6 ?AtomicIntegerFieldUpdater 例子
public class AtomicIntegerFieldUpdaterTest {
private static AtomicIntegerFieldUpdater<User> a = AtomicIntegerFieldUpdater
.newUpdater(User.class, "old");
public static void main(String[] args) {
User conan = new User("conan", 10);
System.out.println(a.getAndIncrement(conan));
System.out.println(a.get(conan));
}
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;
}
}
}
運行結果:
10
11
原子操作類(一)原子操作類詳細介紹