(2.1.27.4)Java併發程式設計:原子類Atomic
在Java中的併發包中了提供了以下幾種型別的原子類來來解決執行緒安全的問題。分為
- 基本資料型別原子類
- 陣列型別原子類
- 引用型別原子類
- 欄位型別原子類。
因為其內部原理都差不多一致。這裡會對每種型別的原子類抽一個來介紹。
一、原子類的使用方式
public class AtomicTest{ //public static volatile int race=0;//1 public static AtomicInteger race=new AtomicInteger(0);//2 public static void increase(){ race.incrementAndGet(); } //public static synchronized void increase(){ // race++; //} private static final int THREADS_COUNT=20; public static void main(String[]args)throws Exception{ Thread[]threads=new Thread[THREADS_COUNT]; for(int i=0;i<THREADS_COUNT;i++){ threads[i]=new Thread(new Runnable(){ @Override public void run(){ for(int i=0;i<10000;i++){ increase(); } } }); threads[i].start(); } //所有執行緒都執行完畢後,列印結果 while(Thread.activeCount()>1) Thread.yield();//yield在於阻塞當前執行緒 System.out.println(race); } }
- 如果是1處使用的程式碼volatile,則結果並不是200. 這是由於 volatile只能保證可見性並不能保證 a++自增操作的原子性,多執行緒在讀寫時可能出現覆蓋
- 把“race++”操作或increase()方法用synchronized同步塊包裹起來當然是一個辦法,使用synchronized修飾後,increase方法變成了一個原子操作,因此是肯定能得到正確的結果。但這裡我們暫時不關注這方面
- 使用 AtomicInteger 代替int後,程式輸出了正確的結果 200,一切都要歸功於incrementAndGet()方法的原子性。
二、基本資料型別原子類
基本資料型別原子類主要為以下幾種:
- AtomicBoolen: boolean型別原子類
- AtomicInteger: int型別原子類
- AtomicLong: long型別原子類
這裡我們以AtomicInteger來進行講解,具體程式碼如下:
public class AtomicInteger extends Number implements java.io.Serializable { private static final sun.misc.Unsafe U = sun.misc.Unsafe.getUnsafe(); private static final long VALUE; private volatile int value;//注意該值用volatile修飾 public AtomicInteger(int initialValue) { value = initialValue; } //以原子的方式將輸入的值與ActomicInteger中的值進行相加, //注意:返回相加前ActomicInteger中的值 public final int getAndAdd(int delta) { return U.getAndAddInt(this, VALUE, delta); } //以原子的方式將輸入的值與ActomicInteger中的值進行相加, //注意:返回相加後的結果 public final int addAndGet(int delta) { return U.getAndAddInt(this, VALUE, delta) + delta; } //以原子方式將當前ActomicInteger中的值加1, //注意:返回相加前ActomicInteger中的值 public final int getAndIncrement() { return U.getAndAddInt(this, VALUE, 1); } //以原子方式將當前ActomicInteger中的值加1, //注意:返回相加後的結果 public final int incrementAndGet() { return U.getAndAddInt(this, VALUE, 1) + 1; } //省略部分程式碼... }
在上述程式碼中,我只留了AtomicInteger 類一部分常用的方法。大家在使用其內部方法時一定要注意其返回的結果。例如getAndAdd()與addAndGet()方法之間的返回值的區別。
2.1 原理解析
其實我們在上一章已經有所講解incrementAndGet
,這裡再重述一次getAndAddInt()
過程加深記憶
//AtomicInteger內部會呼叫其中sun.misc.Unsafe方法中getAndAddInt的方法。
public final int getAndAdd(int delta) {
return U.getAndAddInt(this, VALUE, delta);
}
//sun.misc.Unsafe方法中getAndAddInt方法又會呼叫jdk.internal.misc.Unsafe的getAndAddInt,具體程式碼如下:
public final int getAndAddInt(Object o, long offset, int delta) {
return theInternalUnsafe.getAndAddInt(o, offset, delta);
}
//jdk.internal.misc.Unsafe的getAndAddInt()方法的宣告如下:
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset);//先獲取記憶體中儲存的值
} while (!weakCompareAndSetInt(o, offset, v, v + delta));//如果不是期望的結果值,就一直迴圈
return v;
}
public final boolean weakCompareAndSetInt(Object o, long offset,
int expected,
int x) {//該函式返回值代表CAS操作是否成功
return compareAndSetInt(o, offset, expected, x);//執行CAS操作
}
getAndAddInt()方法在一個無限迴圈中(也就是CAS的自旋),不斷嘗試將一個比當前值大delta的新值賦給自己。如果失敗了,那說明在執行“獲取-設定”操作的時候值已經有了修改,於是再次迴圈進行下一次操作,直到設定成功為止。
三、陣列型別原子類
對於陣列型別的原子類,在Java中,主要通過原子的方式更新數組裡面的某個元素,陣列型別原子類主要有以下幾種:
- AtomicIntegerArray:Int陣列型別原子類
- AtomicLongArray:long陣列型別原子類
- AtomicReferenceArray:引用型別原子類(關於AtomicReferenceArray即引用型別原子類會在下文介紹)
這裡我們還是以AtomicIntegerArray為例,因為其內部原理都是迴圈CAS操作,所以我們這裡就描述其使用方式,具體程式碼如下:
class AtomicDemo {
private int[] value = new int[]{0, 1, 2};
private AtomicIntegerArray mAtomicIntegerArray = new AtomicIntegerArray(value);
private void doAdd() {
for (int i = 0; i < 5; i++) {
int value = mAtomicIntegerArray.addAndGet(0, 1);
System.out.println(Thread.currentThread().getName() + "--->" + value);
}
}
public static void main(String[] args) {
AtomicDemo demo = new AtomicDemo();
new Thread(demo::doAdd, "執行緒1").start();
new Thread(demo::doAdd, "執行緒2").start();
}
}
/程式輸出結果如下:
執行緒1--->1
執行緒1--->2
執行緒1--->4
執行緒2--->3
執行緒1--->5
執行緒2--->6
執行緒1--->7
執行緒2--->8
執行緒2--->9
執行緒2--->10
四、引用型別原子類
在Java併發程式設計之Java CAS操作文章中我們曾經提到過兩個問題
- 第一個問題:雖然我們能通過迴圈CAS操作來完成對一個變數的原子操作,但是對於多個變數進行操作時,自旋CAS操作就不能保證其原子性。
- 第二個問題:ABA問題,因為CAS在操作值的時候,需要檢查值有沒有發生變化,如果沒有發生變化則更新,但是如果一個值原來是A,變成了B,又變成了A,那麼使用CAS進行檢查時會發現她的值並沒有發生變化。那麼會導致程式出問題。
為了解決上述提到的兩個問題,Java為我們提供了AtomicReference等系列引用型別原子類,來保證引用物件之間的原子性,即可以把多個變數放在一個物件裡來進行CAS操作與ABA問題。主要型別原子類如下:
- AtomicReference:
- AtomicReferenceFieldUpdater:
- AtomicMarkableReference:
- AtomicStampedReference:
4.1 多個變數的CAS操作
關於引用型別的原子類,內部都呼叫的是compareAndSwapObject()方法來實現CAS操作的。
這裡我們先解決第一個問題,關係多個變數的CAS操作,我們先以AtomicReference來進行講解,具體程式碼如下所示:
class AtomicDemo {
Person mPerson = new Person("紅紅", 1);
private AtomicReference<Person> mAtomicReference = new AtomicReference<>(mPerson);
private class Person {
String name;
int age;
Person(String name, int age) {
this.name = name;
this.age = age;
}
}
private void updatePersonInfo(String name, int age) throws Exception {
System.out.println(Thread.currentThread().getName() + "更新前--->" + mAtomicReference.get().name + "---->" + mAtomicReference.get().age);
mAtomicReference.getAndUpdate(person -> new Person(name, age));
}
public static void main(String[] args) {
AtomicDemo demo = new AtomicDemo();
new Thread(() -> demo.updatePersonInfo("藍藍", 2), "執行緒1").start();
Thread.sleep(1000);
System.out.println("暫停一秒--->" + demo.mAtomicReference.get().name + "---->" + demo.mAtomicReference.get().age);
System.out.println("更新後---->" + demo.mAtomicReference.get().name + "---->" + demo.mAtomicReference.get().age);
}
}
//輸出結果
執行緒1更新前--->紅紅---->1
暫停一秒--->藍藍---->2
更新後---->藍藍---->2
上述程式碼中建立了Person 類,且當前AtomicReference傳入的是當前 mPerson =new Person(“紅紅”, 1),在Main方法中建立執行緒1使其呼叫mAtomicReference.getAndUpdate(new Person(“藍藍”,2))來更新Person資訊。
主執行緒中休眠一秒後,獲取更新結果並列印。從結果上來看,的確是對多個變數進行了更新的操作。
4.2 ABA問題
class AtomicDemo {
Person mPerson = new Person("紅紅", 1);
private AtomicStampedReference<Person> mAtomicReference = new AtomicStampedReference<>(mPerson, 1);
private class Person {
String name;
int age;
Person(String name, int age) {
this.name = name;
this.age = age;
}
}
/**
* 更新資訊
*
* @param name 名稱
* @param age 年齡
* @param oldStamp CAS操作比較的舊的版本
* @param newStamp 希望更新後的版本
*/
private void updatePersonInfo(String name, int age, int oldStamp, int newStamp) {
System.out.println(Thread.currentThread().getName() + "更新前--->" + mAtomicReference.getReference().name + "---->" + mAtomicReference.getReference().age);
mAtomicReference.compareAndSet(mPerson, new Person(name, age), oldStamp, newStamp);
}
public static void main(String[] args) throws Exception {
AtomicDemo demo = new AtomicDemo();
new Thread(() -> demo.updatePersonInfo("藍藍", 2, 1, 2), "執行緒1").start();
Thread.sleep(1000);
System.out.println("暫停一秒--->" + demo.mAtomicReference.getReference().name + "---->" + demo.mAtomicReference.getReference().age);
new Thread(() -> demo.updatePersonInfo("花花", 3, 1, 3), "執行緒2").start();
Thread.sleep(1000);
System.out.println("更新後---->" + demo.mAtomicReference.getReference().name + "---->" + demo.mAtomicReference.getReference().age);
}
}
//輸出結果
執行緒1更新前--->紅紅---->1
暫停一秒--->藍藍---->2
執行緒2更新前--->藍藍---->2
更新後---->藍藍---->2
在上述程式碼中,我們使用AtomicStampedReference類,其中在使用該類的時候,需要傳入一個類似於版本(你也可以叫做郵戳,時間戳等,隨你喜歡)的int型別的屬性。
在Main方法中我們分別建立了2個執行緒來進行CAS操作,其中執行緒1想做的操作是將版本為1的mPerson(“紅紅”,1)修改為版本為2的Person(“藍藍,2”)。當執行緒1執行完畢後,緊接著執行緒2開始執行,執行緒2想做的操作是將版本為1的mPerson(“紅紅”,1)修改為版本3的Person(“花花”,3)。從程式輸出結果可以看出,執行緒2的操作是沒有執行的。
4.3 欄位型別原子類
如果需要更新某個類中的某個欄位,在Actomic系列中,Java提供了以下幾個類來實現:
- AtomicIntegerFieldUpdater:int型別欄位原子類
- AtomicLongFieldUpdater:long型別欄位原子類
- AtomicReferenceFieldUpdater:引用型欄位原子類
上面所說的三個類原理都差不多,這裡我們以AtomicIntegerFieldUpdate類來講解,具體程式碼如下:
lass AtomicDemo {
Person mPerson = new Person("紅紅", 1);
private AtomicIntegerFieldUpdater<Person> mFieldUpdater = AtomicIntegerFieldUpdater.newUpdater(Person.class, "age");
private class Person {
String name;
volatile int age;//使用volatile修飾
Person(String name, int age) {
this.name = name;
this.age = age;
}
}
/**
* 更新資訊
*
* @param age 年齡
*/
private void updatePersonInfo(int age) {
System.out.println("更新前--->" + mPerson.age);
mFieldUpdater.addAndGet(mPerson, age);
}
private int getUpdateInfo() {
return mFieldUpdater.get(mPerson);
}
public static void main(String[] args) throws Exception {
AtomicDemo demo = new AtomicDemo();
new Thread(() -> demo.updatePersonInfo(12), "執行緒1").start();
Thread.sleep(1000);
System.out.println("更新後--->" + demo.getUpdateInfo());
}
}
//輸出結果
更新前--->1
更新後--->13
這裡對AtomicIntegerFieldUpdate不在進行過多的描述,大家需要主要的是在使用欄位型別原子類的時候,需要進行更新的欄位,需要通過volatile來修飾。