一文吃透 Java 中的并發原子類!
一、簡介
在 Java 的java.util.concurrent包中,除了提供底層鎖、并發同步等工具類以外,還提供了一組原子操作類,大多以Atomic開頭,他們位于java.util.concurrent.atomic包下。
所謂原子類操作,顧名思義,就是這個操作要么全部執行成功,要么全部執行失敗,是保證并發編程安全的重要一環。
相比通過synchronized和lock等方式實現的線程安全同步操作,原子類的實現機制則完全不同。它采用的是通過無鎖(lock-free)的方式來實現線程安全(thread-safe)訪問,底層原理主要基于CAS操作來實現。
某些業務場景下,通過原子類來操作,既可以實現線程安全的要求,又可以實現高效的并發性能,同時編程方面更加簡單。
下面我們一起來看看它的具體玩法!
二、常用原子操作類
在java.util.concurrent.atomic包中,因為原子類眾多,如果按照類型進行劃分,可以分為五大類,每個類型下的原子類可以用如下圖來概括(不同 JDK 版本,可能略有不同,本文主要基于 JDK 1.8 進行采樣)。
圖片
雖然原子操作類很多,但是大體的用法基本類似,只是針對不同的數據類型進行了單獨適配,這些原子類都可以保證多線程下數據的安全性,使用起來也比較簡單。
2.1、基本類型
基本類型的原子類,也是最常用的原子操作類,JDK為開發者提供了三個基礎類型的原子類,內容如下:
- AtomicBoolean:布爾類型的原子操作類
- AtomicInteger:整數類型的原子操作類
- AtomicLong:長整數類型的原子操作類
以AtomicInteger為例,常用的操作方法如下:
方法 | 描述 |
| 獲取當前值 |
| 設置 value 值 |
| 先取得舊值,然后加1,最后返回舊值 |
| 先取得舊值,然后減1,最后返回舊值 |
| 加1,然后返回新值 |
| 減1,然后返回新值 |
| 先取得舊值,然后增加指定值,最后返回舊值 |
| 增加指定值,然后返回新值 |
| 直接使用CAS方式,將【舊值】更新成【新值】,核心方法 |
AtomicInteger的使用方式非常簡單,使用示例如下:
AtomicInteger atomicInteger = new AtomicInteger();
// 先獲取值,再自增,默認初始值為0
int v1 = atomicInteger.getAndIncrement();
System.out.println("v1:" + v1);
// 獲取自增后的ID值
int v2 = atomicInteger.incrementAndGet();
System.out.println("v2:" + v2);
// 獲取自減后的ID值
int v3 = atomicInteger.decrementAndGet();
System.out.println("v3:" + v3);
// 使用CAS方式,將就舊值更新成 10
boolean v4 = atomicInteger.compareAndSet(v3,10);
System.out.println("v4:" + v4);
// 獲取最新值
int v5 = atomicInteger.get();
System.out.println("v5:" + v5);
輸出結果:
v1:0
v2:2
v3:1
v4:true
v5:10
下面我們以對某個變量累加 10000 次為例,采用 10 個線程,每個線程累加 1000 次來實現,對比不同的實現方式執行結果的區別(預期結果值為 10000)。
方式一:線程不安全操作實現
public class Demo1 {
/**
* 初始化一個變量
*/
private static volatile int a = 0;
public static void main(String[] args) throws InterruptedException {
final int threads = 10;
CountDownLatch countDownLatch = new CountDownLatch(threads);
for (int i = 0; i < threads; i++) {
new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 1000; j++) {
a++;
}
countDownLatch.countDown();
}
}).start();
}
// 阻塞等待10個線程執行完畢
countDownLatch.await();
// 輸出結果值
System.out.println("結果值:" + a);
}
}
輸出結果:
結果值:9527
從日志上可以很清晰的看到,實際結果值與預期不符,即使變量a加了volatile關鍵字,也無法保證累加結果的正確性。
針對volatile關鍵字,在之前的文章中我們有所介紹,它只能保證變量的可見性和程序的有序性,無法保證程序操作的原子性,導致運行結果與預期不符。
方式二:線程同步安全操作實現
public class Demo2 {
/**
* 初始化一個變量
*/
private static int a = 0;
public static void main(String[] args) throws InterruptedException {
final int threads = 10;
CountDownLatch countDownLatch = new CountDownLatch(threads);
for (int i = 0; i < threads; i++) {
new Thread(new Runnable() {
@Override
public void run() {
synchronized (Demo2.class){
for (int j = 0; j < 1000; j++) {
a++;
}
}
countDownLatch.countDown();
}
}).start();
}
// 阻塞等待10個線程執行完畢
countDownLatch.await();
// 輸出結果值
System.out.println("結果值:" + a);
}
}
輸出結果:
結果值:10000
當多個線程操作同一個變量或者方法的時候,可以在方法上加synchronized關鍵字,可以同時實現變量的可見性、程序的有序性、操作的原子性,達到運行結果與預期一致的效果。
同時也可以采用Lock鎖來實現多線程操作安全的效果,執行結果也會與預期一致。
方式三:原子類操作實現
public class Demo3 {
/**
* 初始化一個原子操作類
*/
private static AtomicInteger a = new AtomicInteger();
public static void main(String[] args) throws InterruptedException {
final int threads = 10;
CountDownLatch countDownLatch = new CountDownLatch(threads);
for (int i = 0; i < threads; i++) {
new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 1000; j++) {
// 采用原子性操作累加
a.incrementAndGet();
}
countDownLatch.countDown();
}
}).start();
}
// 阻塞等待10個線程執行完畢
countDownLatch.await();
// 輸出結果值
System.out.println("結果值:" + a.get());
}
}
輸出結果:
結果值:10000
從日志結果上可見,原子操作類也可以實現在多線程環境下執行結果與預期一致的效果,關于底層實現原理,我們等會在后文中進行介紹。
與synchronized和Lock等實現方式相比,原子操作類因為采用無鎖的方式實現,因此某些場景下可以帶來更高的執行效率。
2.2、對象引用類型
上文提到的基本類型的原子類,只能更新一個變量,如果需要原子性更新多個變量,這個時候可以采用對象引用類型的原子操作類,將多個變量封裝到一個對象中,JDK為開發者提供了三個對象引用類型的原子類,內容如下:
- AtomicReference:對象引用類型的原子操作類
- AtomicStampedReference:帶有版本號的對象引用類型的原子操作類,可以解決 ABA 問題
- AtomicMarkableReference:帶有標記的對象引用類型的原子操作類
以AtomicReference為例,構造一個對象引用,具體用法如下:
public class User {
private String name;
private Integer age;
public User(String name, Integer age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
AtomicReference<User> atomicReference = new AtomicReference<>();
// 設置原始值
User user1 = new User("張三", 20);
atomicReference.set(user1);
// 采用CAS方式,將user1更新成user2
User user2 = new User("李四", 21);
atomicReference.compareAndSet(user1, user2);
System.out.println("更新后的對象:" + atomicReference.get().toString());
輸出結果:
更新后的對象:User{name='李四', age=21}
2.3、對象屬性類型
在某項場景下,可能你只想原子性更新對象中的某個屬性值,此時可以采用對象屬性類型的原子操作類,JDK為開發者提供了三個對象屬性類型的原子類,內容如下:
- AtomicIntegerFieldUpdater:屬性為整數類型的原子操作類
- AtomicLongFieldUpdater:屬性為長整數類型的原子操作類
- AtomicReferenceFieldUpdater:屬性為對象類型的原子操作類
需要注意的是,這些原子操作類需要滿足以下條件才可以使用。
- 1.被操作的字段不能是 static 類型
- 2.被操縱的字段不能是 final 類型
- 3.被操作的字段必須是 volatile 修飾的
- 4.屬性必須對于當前的 Updater 所在區域是可見的,簡單的說就是盡量使用public修飾字段
以AtomicIntegerFieldUpdater為例,構造一個整數類型的屬性引用,具體用法如下:
public class User {
private String name;
/**
* age 字段加上 volatile 關鍵字,并且改成 public 修飾
*/
public volatile int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
}
User user = new User("張三", 20);
AtomicIntegerFieldUpdater<User> fieldUpdater = AtomicIntegerFieldUpdater.newUpdater(User.class, "age");
// 將 age 的年齡原子性操作加 1
fieldUpdater.getAndIncrement(user);
System.out.println("更新后的屬性值:" + fieldUpdater.get(user));
輸出結果:
更新后的屬性值:21
2.4、數組類型
數組類型的原子操作類,并不是指對數組本身的原子操作,而是對數組中的元素進行原子性操作,這一點需要特別注意,如果要針對整個數組進行更新,可以采用對象引入類型的原子操作類進行處理。
JDK為開發者提供了三個數組類型的原子類,內容如下:
- AtomicIntegerArray:數組為整數類型的原子操作類
- AtomicLongArray:數組為長整數類型的原子操作類
- AtomicReferenceArray:數組為對象類型的原子操作類
以AtomicIntegerArray為例,具體用法如下:
int[] value = new int[]{0, 3, 5};
AtomicIntegerArray array = new AtomicIntegerArray(value);
// 將下標為[0]的元素,原子性操作加 1
array.getAndIncrement(0);
System.out.println("下標為[0]的元素,更新后的值:" + array.get(0));
輸出結果:
下標為[0]的元素,更新后的值:1
2.5、累加器類型
累加器類型的原子操作類,是從 jdk 1.8 開始加入的,專門用來執行數值類型的數據累加操作,性能更好。
它的實現原理與基本數據類型的原子類略有不同,當多線程競爭時采用分段累加的思路來實現目標值,在多線程環境中,它比AtomicLong性能要高出不少,特別是寫多的場景。
JDK為開發者提供了四個累加器類型的原子類,內容如下:
- LongAdder:長整數類型的原子累加操作類
- LongAccumulator:LongAdder的功能增強版,它支持自定義的函數操作
- DoubleAdder:浮點數類型的原子累加操作類
- DoubleAccumulator:同樣的,也是DoubleAdder的功能增強版,支持自定義的函數操作
以LongAdder為例,具體用法如下:
LongAdder adder = new LongAdder();
// 自增加 1,默認初始值為0
adder.increment();
adder.increment();
adder.increment();
System.out.println("最新值:" + adder.longValue());
輸出結果:
最新值:3
三、小結
本文主要圍繞AtomicInteger的用法進行一次知識總結,JUC包下的原子操作類非常的多,但是大體用法基本相似,只是針對不同的數據類型做了細分處理。
在實際業務開發中,原子操作類通常用于計數器,累加器等場景,比如編寫一個多線程安全的全局唯一 ID 生成器。
public class IdGenerator {
private static AtomicLong atomic = new AtomicLong(0);
public long getNextId() {
return atomic.incrementAndGet();
}
}