一、寫在開頭
在前面的博文中我們學習了volatile關鍵字,知道了它可以保證有序性和可見性,但無法保障原子性,結局原子性問題推薦使用synchronized、Lock或者AtomicInteger;我們還學習過CAS演算法,在那篇博文中我們同樣也提及atomic。那麼今天,我們就來好好學一學Atomic原子庫,一個基於CAS演算法實現的高效併發工具庫!
併發包 java.util.concurrent
的原子類都存放在java.util.concurrent.atomic
中,如下圖所示:
1.1 Atomic釋義
Atomic翻譯為“原子”,何為原子?在化學領域的原子被認為是構成化學反應的最小微觀粒子,是不可分割的最小單位(當然後面又發現了更小的粒子,咱們不槓哈),偉大的Doug Lea大師,將併發的一些類以此單詞開頭命名,一語中的!
- 原子性在程式中所表達的意思是:一個或者多個操作在 CPU 執行的過程中不被中斷的特性!
- 原子操作在程式中表達的意思是:即最小不可拆分的操作,也就是說操作一旦開始,就不能被打斷,直到操作完成!
二、四大原子分類
我們根據操作的資料型別可以將JUC包中的原子類做如下的4種劃分:
2.1 基本型別
原子操作的基本型別主要可分為:
- AtomicBoolean:布林型原子類;
- AtomicInteger:整型原子類;
- AtomicLong:長整型原子類;
這三種方式用法幾乎相同,都是以原子更新的方式操作基本型別,我們在這裡以AtomicInteger為例看一下它的使用與原理。
1)AtomicInteger的常用方法
public final int get() //獲取當前的值
public final int getAndSet(int newValue)//獲取當前的值,並設定為newValue
public final int getAndIncrement()//獲取當前的值,並自增
public final int incrementAndGet()//增加 1,並獲取新值,注意與上面方法區分
public final int getAndDecrement() //獲取當前的值,並自減
public final int getAndAdd(int delta) //獲取當前的值,並加上預期的值delta
boolean compareAndSet(int expect, int update) //如果輸入的數值等於預期值,則以原子方式將該值設定為輸入值(update)
public final void lazySet(int newValue)//最終設定為newValue,使用 lazySet 設定之後可能導致其他執行緒在之後的一小段時間內還是可以讀到舊的值。
2)AtomicInteger的使用案例
public class Test {
public static void main(String[] args) {
//臨時值
int temvalue = 0;
AtomicInteger i = new AtomicInteger(0);
temvalue = i.getAndSet(3);
System.out.println("temvalue:" + temvalue + "; i:" + i); //temvalue:0; i:3
temvalue = i.getAndIncrement();
System.out.println("temvalue:" + temvalue + "; i:" + i); //temvalue:3; i:4
temvalue = i.getAndAdd(5);
System.out.println("temvalue:" + temvalue + "; i:" + i); //temvalue:4; i:9
temvalue = i.incrementAndGet();
System.out.println("temvalue:" + temvalue + "; i:" + i); //temvalue:10; i:10
}
}
這裡面需要注意的一點是getAndIncrement()方法與incrementAndGet(),一個先獲值後自增,一個先自增後獲值。
3)AtomicInteger的底層原理
我們以getAndIncrement()為例,去跟入它的底層程式碼會發現,其內部是同調傭UnSafe類的靜態方法 getUnsafe 實現的,UnSafe類我們在講解CAS演算法的時候有提及,後面找個時間再單獨學一下它,其底層是透過CAS,原子性的進行增加值。
public final int getAndIncrement() {
// 使用Unsafe類中的getAndAddInt方法原子地增加AtomicInteger的當前值
// 第一個引數this是AtomicInteger的當前例項
// 第二個引數valueOffset是一個偏移量,它指示在AtomicInteger物件中的哪個位置可以找到實際的int值
// 第三個引數1表示要加到當前值上的值(即增加的值)
// 此方法返回的是增加前的原始值
return unsafe.getAndAddInt(this, valueOffset, 1);
}
Unsafe
類是 Java 中的一個特殊類,用於執行低階、不安全的操作。getAndIncrement 方法就是利用了 Unsafe 類提供的 CAS(Compare-And-Swap)操作來實現原子的 increment 操作。CAS 是一種常用的無鎖技術,允許在多執行緒環境中原子地更新值。
2.2 陣列型別
原子操作根據陣列型別,可以分為如下幾種:
- AtomicIntegerArray:整形陣列原子類
- AtomicLongArray:長整形陣列原子類
- AtomicReferenceArray:引用型別陣列原子類
這三種同樣很類似,我們以AtomicIntegerArray為例來介紹一下。
1)AtomicIntegerArray的常用方法
public final int get(int i) //獲取 index=i 位置元素的值
public final int getAndSet(int i, int newValue)//返回 index=i 位置的當前的值,並將其設定為新值:newValue
public final int getAndIncrement(int i)//獲取 index=i 位置元素的值,並讓該位置的元素自增
public final int getAndDecrement(int i) //獲取 index=i 位置元素的值,並讓該位置的元素自減
public final int getAndAdd(int i, int delta) //獲取 index=i 位置元素的值,並加上預期的值
boolean compareAndSet(int i, int expect, int update) //如果輸入的數值等於預期值,則以原子方式將 index=i 位置的元素值設定為輸入值(update)
public final void lazySet(int i, int newValue)//最終 將index=i 位置的元素設定為newValue,使用 lazySet 設定之後可能導致其他執行緒在之後的一小段時間內還是可以讀到舊的值。
2)AtomicIntegerArray的使用案例
public class Test {
public static void main(String[] args) {
int temvalue = 0;
int[] nums = { 1, 2, 3, 4, 5, 6 };
AtomicIntegerArray i = new AtomicIntegerArray(nums);
for (int j = 0; j < nums.length; j++) {
System.out.print(i.get(j));
}
System.out.println();
temvalue = i.getAndSet(0, 2);
System.out.println("temvalue:" + temvalue + "; i:" + i);
temvalue = i.getAndIncrement(0);
System.out.println("temvalue:" + temvalue + "; i:" + i);
temvalue = i.getAndAdd(0, 5);
System.out.println("temvalue:" + temvalue + "; i:" + i);
}
}
輸出:
123456
temvalue:1; i:[2, 2, 3, 4, 5, 6]
temvalue:2; i:[3, 2, 3, 4, 5, 6]
temvalue:3; i:[8, 2, 3, 4, 5, 6]
2.3 引用型別
除了如上的2種原子類外,atomic包中還提供了引用型別原子類。大概為如下幾種:
- AtomicReference:原子更新引用型別,使用AtomicReference類保證物件之間的原子性,把多個變數放到一個物件裡面進行 CAS 操作;
- AtomicStampedReference:原子更新帶有版本號的引用型別。該類將整數值與引用關聯起來,可用於解決原子的更新資料和資料的版本號,可以解決使用 CAS 進行原子更新時可能出現的 ABA 問題;
- AtomicMarkableReference:原子更新帶有標記的引用型別,該類將 boolean 標記與引用關聯起來。
常用方法又上述兩種型別一致,這裡不再贅述,我們直接寫一個demo感受一下它的使用吧
public class TestAtomicReference {
private static AtomicReference<User> reference = new AtomicReference<>();
public static void main(String[] args) {
User user1 = new User("小明", 18);
reference.set(user1);
User user2 = new User("小華",20);
User user = reference.getAndSet(user2);
System.out.println(user);
System.out.println(reference.get());
}
static class User {
private String userName;
private int age;
public User(String userName, int age) {
this.userName = userName;
this.age = age;
}
@Override
public String toString() {
return "User{" +
"userName='" + userName + '\'' +
", age=" + age +
'}';
}
}
}
輸出:
User{userName='小明', age=18}
User{userName='小華', age=20}
透過結果我們可以看出先給該reference引用物件set一個user1,再透過getAndSet()方法進行賦值,先輸出當前物件資料,緊接著引用指向最新的物件地址,也就是“User{userName='小華', age=20}”。
2.4 物件的屬性修改型別
除了原子更新物件(引用型別)外,atomic中還提供了更新物件的屬性欄位的原子類:
- AtomicIntegerFieldUpdater:原子更新整形欄位的更新器;
- AtomicLongFieldUpdater:原子更新長整形欄位的更新器;
- AtomicReferenceFieldUpdater:原子更新引用型別裡的欄位的更新器。
如果想要原子的更新物件的屬性,實現起來較上面幾種型別略微複雜一下,大概分為兩步;
步驟1️⃣
透過靜態方法newUpdater建立一個更新器,並且設定想要更新的類和欄位;
步驟2️⃣
欄位必須使用public volatile進行修飾;
以 AtomicIntegerFieldUpdater為例,我們寫一個測試類感受一下。
public class TestAtomicIntegerFieldUpdater {
//建立一個age的更新器
private static AtomicIntegerFieldUpdater updater = AtomicIntegerFieldUpdater.newUpdater(User.class,"age");
public static void main(String[] args) {
User user = new User("小明", 17);
int oldValue = updater.getAndAdd(user, 1);
System.out.println(oldValue);//17
System.out.println(updater.get(user));//18
}
static class User {
private String userName;
public volatile int age;
public User(String userName, int age) {
this.userName = userName;
this.age = age;
}
@Override
public String toString() {
return "User{" +
"userName='" + userName + '\'' +
", age=" + age +
'}';
}
}
透過AtomicIntegerFieldUpdater.newUpdater(User.class,"age")建立一個age的更新器,然後呼叫getAndAdd(user, 1)進行年齡加1操作,從17歲變為18歲。