JUC 中的 Atomic 原子類總結
1 Atomic 原子類介紹
Atomic 翻譯成中文是原子的意思。在化學上,我們知道原子是構成一般物質的最小單位,在化學反應中是不可分割的。在我們這裡 Atomic 是指一個操作是不可中斷的。即使是在多個執行緒一起執行的時候,一個操作一旦開始,就不會被其他執行緒干擾。
所以,所謂原子類說簡單點就是具有原子/原子操作特徵的類。
併發包 java.util.concurrent
的原子類都存放在java.util.concurrent.atomic
下,如下圖所示。
根據操作的資料型別,可以將JUC包中的原子類分為4類
基本型別
使用原子的方式更新基本型別
- AtomicInteger:整型原子類
- AtomicLong:長整型原子類
- AtomicBoolean :布林型原子類
陣列型別
使用原子的方式更新數組裡的某個元素
- AtomicIntegerArray:整型陣列原子類
- AtomicLongArray:長整型陣列原子類
- AtomicReferenceArray :引用型別陣列原子類
引用型別
- AtomicReference:引用型別原子類
- AtomicReferenceFieldUpdater:原子更新引用型別裡的欄位
- AtomicMarkableReference :原子更新帶有標記位的引用型別
物件的屬性修改型別
- AtomicIntegerFieldUpdater:原子更新整型欄位的更新器
- AtomicLongFieldUpdater:原子更新長整型欄位的更新器
- AtomicStampedReference :原子更新帶有版本號的引用型別。該類將整數值與引用關聯起來,可用於解決原子的更新資料和資料的版本號,可以解決使用 CAS 進行原子更新時可能出現的 ABA 問題。
- AtomicMarkableReference:原子更新帶有標記的引用型別。該類將 boolean 標記與引用關聯起來,也可以解決使用 CAS 進行原子更新時可能出現的 ABA 問題。
CAS ABA 問題
- 描述: 第一個執行緒取到了變數 x 的值 A,然後巴拉巴拉幹別的事,總之就是隻拿到了變數 x 的值 A。這段時間內第二個執行緒也取到了變數 x 的值 A,然後把變數 x 的值改為 B,然後巴拉巴拉幹別的事,最後又把變數 x 的值變為 A (相當於還原了)。在這之後第一個執行緒終於進行了變數 x 的操作,但是此時變數 x 的值還是 A,所以 compareAndSet 操作是成功。
- 例子描述(可能不太合適,但好理解): 年初,現金為零,然後通過正常勞動賺了三百萬,之後正常消費了(比如買房子)三百萬。年末,雖然現金零收入(可能變成其他形式了),但是賺了錢是事實,還是得交稅的!
- 程式碼例子(以
AtomicInteger
為例)
import java.util.concurrent.atomic.AtomicInteger; public class AtomicIntegerDefectDemo { public static void main(String[] args) { defectOfABA(); } static void defectOfABA() { final AtomicInteger atomicInteger = new AtomicInteger(1); Thread coreThread = new Thread( () -> { final int currentValue = atomicInteger.get(); System.out.println(Thread.currentThread().getName() + " ------ currentValue=" + currentValue); // 這段目的:模擬處理其他業務花費的時間 try { Thread.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); } boolean casResult = atomicInteger.compareAndSet(1, 2); System.out.println(Thread.currentThread().getName() + " ------ currentValue=" + currentValue + ", finalValue=" + atomicInteger.get() + ", compareAndSet Result=" + casResult); } ); coreThread.start(); // 這段目的:為了讓 coreThread 執行緒先跑起來 try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } Thread amateurThread = new Thread( () -> { int currentValue = atomicInteger.get(); boolean casResult = atomicInteger.compareAndSet(1, 2); System.out.println(Thread.currentThread().getName() + " ------ currentValue=" + currentValue + ", finalValue=" + atomicInteger.get() + ", compareAndSet Result=" + casResult); currentValue = atomicInteger.get(); casResult = atomicInteger.compareAndSet(2, 1); System.out.println(Thread.currentThread().getName() + " ------ currentValue=" + currentValue + ", finalValue=" + atomicInteger.get() + ", compareAndSet Result=" + casResult); } ); amateurThread.start(); } }
輸出內容如下:
Thread-0 ------ currentValue=1 Thread-1 ------ currentValue=1, finalValue=2, compareAndSet Result=true Thread-1 ------ currentValue=2, finalValue=1, compareAndSet Result=true Thread-0 ------ currentValue=1, finalValue=2, compareAndSet Result=true
下面我們來詳細介紹一下這些原子類。
2 基本型別原子類
2.1 基本型別原子類介紹
使用原子的方式更新基本型別
- AtomicInteger:整型原子類
- AtomicLong:長整型原子類
- AtomicBoolean :布林型原子類
上面三個類提供的方法幾乎相同,所以我們這裡以 AtomicInteger 為例子來介紹。
AtomicInteger 類常用方法
public final int get() //獲取當前的值 public final int getAndSet(int newValue)//獲取當前的值,並設定新的值 public final int getAndIncrement()//獲取當前的值,並自增 public final int getAndDecrement() //獲取當前的值,並自減 public final int getAndAdd(int delta) //獲取當前的值,並加上預期的值 boolean compareAndSet(int expect, int update) //如果輸入的數值等於預期值,則以原子方式將該值設定為輸入值(update) public final void lazySet(int newValue)//最終設定為newValue,使用 lazySet 設定之後可能導致其他執行緒在之後的一小段時間內還是可以讀到舊的值。
2.2 AtomicInteger 常見方法使用
import java.util.concurrent.atomic.AtomicInteger; public class AtomicIntegerTest { public static void main(String[] args) { // TODO Auto-generated method stub 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 } }
2.3 基本資料型別原子類的優勢
通過一個簡單例子帶大家看一下基本資料型別原子類的優勢
①多執行緒環境不使用原子類保證執行緒安全(基本資料型別)
class Test { private volatile int count = 0; //若要執行緒安全執行執行count++,需要加鎖 public synchronized void increment() { count++; } public int getCount() { return count; } }
②多執行緒環境使用原子類保證執行緒安全(基本資料型別)
class Test2 { private AtomicInteger count = new AtomicInteger(); public void increment() { count.incrementAndGet(); } //使用AtomicInteger之後,不需要加鎖,也可以實現執行緒安全。 public int getCount() { return count.get(); } }
2.4 AtomicInteger 執行緒安全原理簡單分析
AtomicInteger 類的部分原始碼:
// setup to use Unsafe.compareAndSwapInt for updates(更新操作時提供“比較並替換”的作用) private static final Unsafe unsafe = Unsafe.getUnsafe(); private static final long valueOffset; static { try { valueOffset = unsafe.objectFieldOffset (AtomicInteger.class.getDeclaredField("value")); } catch (Exception ex) { throw new Error(ex); } } private volatile int value;
AtomicInteger 類主要利用 CAS (compare and swap) + volatile 和 native 方法來保證原子操作,從而避免 synchronized 的高開銷,執行效率大為提升。
CAS的原理是拿期望的值和原本的一個值作比較,如果相同則更新成新的值。UnSafe 類的 objectFieldOffset() 方法是一個本地方法,這個方法是用來拿到“原來的值”的記憶體地址。另外 value 是一個volatile變數,在記憶體中可見,因此 JVM 可以保證任何時刻任何執行緒總能拿到該變數的最新值。
3 陣列型別原子類
3.1 陣列型別原子類介紹
使用原子的方式更新數組裡的某個元素
- AtomicIntegerArray:整形陣列原子類
- AtomicLongArray:長整形陣列原子類
- AtomicReferenceArray :引用型別陣列原子類
上面三個類提供的方法幾乎相同,所以我們這裡以 AtomicIntegerArray 為例子來介紹。
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 delta) //獲取 index=i 位置元素的值,並加上預期的值 boolean compareAndSet(int expect, int update) //如果輸入的數值等於預期值,則以原子方式將 index=i 位置的元素值設定為輸入值(update) public final void lazySet(int i, int newValue)//最終 將index=i 位置的元素設定為newValue,使用 lazySet 設定之後可能導致其他執行緒在之後的一小段時間內還是可以讀到舊的值。
3.2 AtomicIntegerArray 常見方法使用
import java.util.concurrent.atomic.AtomicIntegerArray; public class AtomicIntegerArrayTest { public static void main(String[] args) { // TODO Auto-generated method stub 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.println(i.get(j)); } 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); } }
4 引用型別原子類
4.1 引用型別原子類介紹
基本型別原子類只能更新一個變數,如果需要原子更新多個變數,需要使用 引用型別原子類。
- AtomicReference:引用型別原子類
- AtomicStampedReference:原子更新引用型別裡的欄位原子類
- AtomicMarkableReference :原子更新帶有標記位的引用型別
上面三個類提供的方法幾乎相同,所以我們這裡以 AtomicReference 為例子來介紹。
4.2 AtomicReference 類使用示例
import java.util.concurrent.atomic.AtomicReference; public class AtomicReferenceTest { public static void main(String[] args) { AtomicReference<Person> ar = new AtomicReference<Person>(); Person person = new Person("SnailClimb", 22); ar.set(person); Person updatePerson = new Person("Daisy", 20); ar.compareAndSet(person, updatePerson); System.out.println(ar.get().getName()); System.out.println(ar.get().getAge()); } } class Person { private String name; private int age; public Person(String name, int age) { super(); this.name = name; this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } }
上述程式碼首先建立了一個 Person 物件,然後把 Person 物件設定進 AtomicReference 物件中,然後呼叫 compareAndSet 方法,該方法就是通過 CAS 操作設定 ar。如果 ar 的值為 person 的話,則將其設定為 updatePerson。實現原理與 AtomicInteger 類中的 compareAndSet 方法相同。執行上面的程式碼後的輸出結果如下:
Daisy 20
4.3 AtomicStampedReference 類使用示例
import java.util.concurrent.atomic.AtomicStampedReference; public class AtomicStampedReferenceDemo { public static void main(String[] args) { // 例項化、取當前值和 stamp 值 final Integer initialRef = 0, initialStamp = 0; final AtomicStampedReference<Integer> asr = new AtomicStampedReference<>(initialRef, initialStamp); System.out.println("currentValue=" + asr.getReference() + ", currentStamp=" + asr.getStamp()); // compare and set final Integer newReference = 666, newStamp = 999; final boolean casResult = asr.compareAndSet(initialRef, newReference, initialStamp, newStamp); System.out.println("currentValue=" + asr.getReference() + ", currentStamp=" + asr.getStamp() + ", casResult=" + casResult); // 獲取當前的值和當前的 stamp 值 int[] arr = new int[1]; final Integer currentValue = asr.get(arr); final int currentStamp = arr[0]; System.out.println("currentValue=" + currentValue + ", currentStamp=" + currentStamp); // 單獨設定 stamp 值 final boolean attemptStampResult = asr.attemptStamp(newReference, 88); System.out.println("currentValue=" + asr.getReference() + ", currentStamp=" + asr.getStamp() + ", attemptStampResult=" + attemptStampResult); // 重新設定當前值和 stamp 值 asr.set(initialRef, initialStamp); System.out.println("currentValue=" + asr.getReference() + ", currentStamp=" + asr.getStamp()); // [不推薦使用,除非搞清楚註釋的意思了] weak compare and set // 困惑!weakCompareAndSet 這個方法最終還是呼叫 compareAndSet 方法。[版本: jdk-8u191] // 但是註釋上寫著 "May fail spuriously and does not provide ordering guarantees, // so is only rarely an appropriate alternative to compareAndSet." // todo 感覺有可能是 jvm 通過方法名在 native 方法裡面做了轉發 final boolean wCasResult = asr.weakCompareAndSet(initialRef, newReference, initialStamp, newStamp); System.out.println("currentValue=" + asr.getReference() + ", currentStamp=" + asr.getStamp() + ", wCasResult=" + wCasResult); } }
輸出結果如下:
currentValue=0, currentStamp=0 currentValue=666, currentStamp=999, casResult=true currentValue=666, currentStamp=999 currentValue=666, currentStamp=88, attemptStampResult=true currentValue=0, currentStamp=0 currentValue=666, currentStamp=999, wCasResult=true
4.4 AtomicMarkableReference 類使用示例
import java.util.concurrent.atomic.AtomicMarkableReference; public class AtomicMarkableReferenceDemo { public static void main(String[] args) { // 例項化、取當前值和 mark 值 final Boolean initialRef = null, initialMark = false; final AtomicMarkableReference<Boolean> amr = new AtomicMarkableReference<>(initialRef, initialMark); System.out.println("currentValue=" + amr.getReference() + ", currentMark=" + amr.isMarked()); // compare and set final Boolean newReference1 = true, newMark1 = true; final boolean casResult = amr.compareAndSet(initialRef, newReference1, initialMark, newMark1); System.out.println("currentValue=" + amr.getReference() + ", currentMark=" + amr.isMarked() + ", casResult=" + casResult); // 獲取當前的值和當前的 mark 值 boolean[] arr = new boolean[1]; final Boolean currentValue = amr.get(arr); final boolean currentMark = arr[0]; System.out.println("currentValue=" + currentValue + ", currentMark=" + currentMark); // 單獨設定 mark 值 final boolean attemptMarkResult = amr.attemptMark(newReference1, false); System.out.println("currentValue=" + amr.getReference() + ", currentMark=" + amr.isMarked() + ", attemptMarkResult=" + attemptMarkResult); // 重新設定當前值和 mark 值 amr.set(initialRef, initialMark); System.out.println("currentValue=" + amr.getReference() + ", currentMark=" + amr.isMarked()); // [不推薦使用,除非搞清楚註釋的意思了] weak compare and set // 困惑!weakCompareAndSet 這個方法最終還是呼叫 compareAndSet 方法。[版本: jdk-8u191] // 但是註釋上寫著 "May fail spuriously and does not provide ordering guarantees, // so is only rarely an appropriate alternative to compareAndSet." // todo 感覺有可能是 jvm 通過方法名在 native 方法裡面做了轉發 final boolean wCasResult = amr.weakCompareAndSet(initialRef, newReference1, initialMark, newMark1); System.out.println("currentValue=" + amr.getReference() + ", currentMark=" + amr.isMarked() + ", wCasResult=" + wCasResult); } }
輸出結果如下:
currentValue=null, currentMark=false currentValue=true, currentMark=true, casResult=true currentValue=true, currentMark=true currentValue=true, currentMark=false, attemptMarkResult=true currentValue=null, currentMark=false currentValue=true, currentMark=true, wCasResult=true
5 物件的屬性修改型別原子類
5.1 物件的屬性修改型別原子類介紹
如果需要原子更新某個類裡的某個欄位時,需要用到物件的屬性修改型別原子類。
- AtomicIntegerFieldUpdater:原子更新整形欄位的更新器
- AtomicLongFieldUpdater:原子更新長整形欄位的更新器
- AtomicStampedReference :原子更新帶有版本號的引用型別。該類將整數值與引用關聯起來,可用於解決原子的更新資料和資料的版本號,可以解決使用 CAS 進行原子更新時可能出現的 ABA 問題。
要想原子地更新物件的屬性需要兩步。第一步,因為物件的屬性修改型別原子類都是抽象類,所以每次使用都必須使用靜態方法 newUpdater()建立一個更新器,並且需要設定想要更新的類和屬性。第二步,更新的物件屬性必須使用 public volatile 修飾符。
上面三個類提供的方法幾乎相同,所以我們這裡以 AtomicIntegerFieldUpdater
為例子來介紹。
5.2 AtomicIntegerFieldUpdater 類使用示例
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; public class AtomicIntegerFieldUpdaterTest { public static void main(String[] args) { AtomicIntegerFieldUpdater<User> a = AtomicIntegerFieldUpdater.newUpdater(User.class, "age"); User user = new User("Java", 22); System.out.println(a.getAndIncrement(user));// 22 System.out.println(a.get(user));// 23 } } class User { private String name; public volatile int age; public User(String name, int age) { super(); this.name = name; this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } }
輸出結果:
22 23