這是java高並發系列第23篇文章,環境:jdk1.8。
本文主要內容
- JUC中的原子類介紹
- 介紹基本類型原子類
- 介紹數組類型原子類
- 介紹引用類型原子類
- 介紹對象屬性修改相關原子類
預備知識
JUC中的原子類都是都是依靠volatile、CAS、Unsafe類配合來實現的,需要了解的請移步:
JUC中原子類介紹
什么是原子操作?
atomic 翻譯成中文是原子的意思。在化學上,我們知道原子是構成一般物質的最小單位,在化學反應中是不可分割的。在我們這里 atomic 是指一個操作是不可中斷的。即使是在多個線程一起執行的時候,一個操作一旦開始,就不會被其他線程干擾,所以,所謂原子類說簡單點就是具有原子操作特征的類,原子操作類提供了一些修改數據的方法,這些方法都是原子操作的,在多線程情況下可以確保被修改數據的正確性。
JUC中對原子操作提供了強大的支持,這些類位於java.util.concurrent.atomic包中,如下圖:

JUC中原子類思維導圖

基本類型原子類
使用原子的方式更新基本類型
- AtomicInteger:int類型原子類
- AtomicLong:long類型原子類
- AtomicBoolean :boolean類型原子類
上面三個類提供的方法幾乎相同,這里以 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 設置之后可能導致其他線程在之后的一小段時間內還是可以讀到舊的值。
部分源碼
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;
2個關鍵字段說明:
value:使用volatile修飾,可以確保value在多線程中的可見性。
valueOffset:value屬性在AtomicInteger中的偏移量,通過這個偏移量可以快速定位到value字段,這個是實現AtomicInteger的關鍵。
getAndIncrement源碼:
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
內部調用的是Unsafe類中的getAndAddInt方法,我們看一下getAndAddInt源碼:
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
說明:
this.getIntVolatile:可以確保從主內存中獲取變量最新的值。compareAndSwapInt:CAS操作,CAS的原理是拿期望的值和原本的值作比較,如果相同則更新成新的值,可以確保在多線程情況下只有一個線程會操作成功,不成功的返回false。
上面有個do-while循環,compareAndSwapInt返回false之后,會再次從主內存中獲取變量的值,繼續做CAS操作,直到成功為止。
getAndAddInt操作相當於線程安全的count++操作,如同:
synchronize(lock){
count++;
}
count++操作實際上是被拆分為3步驟執行:
- 獲取count的值,記做A:A=count
- 將A的值+1,得到B:B = A+1
- 讓B賦值給count:count = B
多線程情況下會出現線程安全的問題,導致數據不准確。synchronize的方式會導致占時無法獲取鎖的線程處於阻塞狀態,性能比較低。CAS的性能比synchronize要快很多。
示例
使用AtomicInteger實現網站訪問量計數器功能,模擬100人同時訪問網站,每個人訪問10次,代碼如下:
package com.itsoku.chat23;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 跟着阿里p7學並發,微信公眾號:javacode2018
*/
public class Demo1 {
//訪問次數
static AtomicInteger count = new AtomicInteger();
//模擬訪問一次
public static void request() throws InterruptedException {
//模擬耗時5毫秒
TimeUnit.MILLISECONDS.sleep(5);
//對count原子+1
count.incrementAndGet();
}
public static void main(String[] args) throws InterruptedException {
long starTime = System.currentTimeMillis();
int threadSize = 100;
CountDownLatch countDownLatch = new CountDownLatch(threadSize);
for (int i = 0; i < threadSize; i++) {
Thread thread = new Thread(() -> {
try {
for (int j = 0; j < 10; j++) {
request();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
countDownLatch.countDown();
}
});
thread.start();
}
countDownLatch.await();
long endTime = System.currentTimeMillis();
System.out.println(Thread.currentThread().getName() + ",耗時:" + (endTime - starTime) + ",count=" + count);
}
}
輸出:
main,耗時:158,count=1000
通過輸出中可以看出incrementAndGet在多線程情況下能確保數據的正確性。
數組類型原子類介紹
使用原子的方式更新數組里的某個元素,可以確保修改數組中數據的線程安全性。
- 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 設置之后可能導致其他線程在之后的一小段時間內還是可以讀到舊的值。
示例
統計網站頁面訪問量,假設網站有10個頁面,現在模擬100個人並行訪問每個頁面10次,然后將每個頁面訪問量輸出,應該每個頁面都是1000次,代碼如下:
package com.itsoku.chat23;
import java.util.Arrays;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicIntegerArray;
/**
* 跟着阿里p7學並發,微信公眾號:javacode2018
*/
public class Demo2 {
static AtomicIntegerArray pageRequest = new AtomicIntegerArray(new int[10]);
/**
* 模擬訪問一次
*
* @param page 訪問第幾個頁面
* @throws InterruptedException
*/
public static void request(int page) throws InterruptedException {
//模擬耗時5毫秒
TimeUnit.MILLISECONDS.sleep(5);
//pageCountIndex為pageCount數組的下標,表示頁面對應數組中的位置
int pageCountIndex = page - 1;
pageRequest.incrementAndGet(pageCountIndex);
}
public static void main(String[] args) throws InterruptedException {
long starTime = System.currentTimeMillis();
int threadSize = 100;
CountDownLatch countDownLatch = new CountDownLatch(threadSize);
for (int i = 0; i < threadSize; i++) {
Thread thread = new Thread(() -> {
try {
for (int page = 1; page <= 10; page++) {
for (int j = 0; j < 10; j++) {
request(page);
}
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
countDownLatch.countDown();
}
});
thread.start();
}
countDownLatch.await();
long endTime = System.currentTimeMillis();
System.out.println(Thread.currentThread().getName() + ",耗時:" + (endTime - starTime));
for (int pageIndex = 0; pageIndex < 10; pageIndex++) {
System.out.println("第" + (pageIndex + 1) + "個頁面訪問次數為" + pageRequest.get(pageIndex));
}
}
}
輸出:
main,耗時:635
第1個頁面訪問次數為1000
第2個頁面訪問次數為1000
第3個頁面訪問次數為1000
第4個頁面訪問次數為1000
第5個頁面訪問次數為1000
第6個頁面訪問次數為1000
第7個頁面訪問次數為1000
第8個頁面訪問次數為1000
第9個頁面訪問次數為1000
第10個頁面訪問次數為1000
說明:
代碼中將10個面的訪問量放在了一個int類型的數組中,數組大小為10,然后通過
AtomicIntegerArray來操作數組中的每個元素,可以確保操作數據的原子性,每次訪問會調用incrementAndGet,此方法需要傳入數組的下標,然后對指定的元素做原子+1操作。輸出結果都是1000,可以看出對於數組中元素的並發修改是線程安全的。如果線程不安全,則部分數據可能會小於1000。
其他的一些方法可以自行操作一下,都非常簡單。
引用類型原子類介紹
基本類型原子類只能更新一個變量,如果需要原子更新多個變量,需要使用 引用類型原子類。
- AtomicReference:引用類型原子類
- AtomicStampedRerence:原子更新引用類型里的字段原子類
- AtomicMarkableReference :原子更新帶有標記位的引用類型
AtomicReference 和 AtomicInteger 非常類似,不同之處在於 AtomicInteger是對整數的封裝,而AtomicReference則是對應普通的對象引用,它可以確保你在修改對象引用時的線程安全性。在介紹AtomicReference的同時,我們先來了解一個有關原子操作邏輯上的不足。
ABA問題
之前我們說過,線程判斷被修改對象是否可以正確寫入的條件是對象的當前值和期望值是否一致。這個邏輯從一般意義上來說是正確的,但是可能出現一個小小的例外,就是當你獲得當前數據后,在准備修改為新值錢,對象的值被其他線程連續修改了兩次,而經過這2次修改后,對象的值又恢復為舊值,這樣,當前線程就無法正確判斷這個對象究竟是否被修改過,這就是所謂的ABA問題,可能會引發一些問題。
舉個例子
有一家蛋糕店,為了挽留客戶,決定為貴賓卡客戶一次性贈送20元,刺激客戶充值和消費,但條件是,每一位客戶只能被贈送一次,現在我們用AtomicReference來實現這個功能,代碼如下:
package com.itsoku.chat22;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
/**
* 跟着阿里p7學並發,微信公眾號:javacode2018
*/
public class Demo3 {
//賬戶原始余額
static int accountMoney = 19;
//用於對賬戶余額做原子操作
static AtomicReference<Integer> money = new AtomicReference<>(accountMoney);
/**
* 模擬2個線程同時更新后台數據庫,為用戶充值
*/
static void recharge() {
for (int i = 0; i < 2; i++) {
new Thread(() -> {
for (int j = 0; j < 5; j++) {
Integer m = money.get();
if (m == accountMoney) {
if (money.compareAndSet(m, m + 20)) {
System.out.println("當前余額:" + m + ",小於20,充值20元成功,余額:" + money.get() + "元");
}
}
//休眠100ms
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
/**
* 模擬用戶消費
*/
static void consume() throws InterruptedException {
for (int i = 0; i < 5; i++) {
Integer m = money.get();
if (m > 20) {
if (money.compareAndSet(m, m - 20)) {
System.out.println("當前余額:" + m + ",大於10,成功消費10元,余額:" + money.get() + "元");
}
}
//休眠50ms
TimeUnit.MILLISECONDS.sleep(50);
}
}
public static void main(String[] args) throws InterruptedException {
recharge();
consume();
}
}
輸出:
當前余額:19,小於20,充值20元成功,余額:39元
當前余額:39,大於10,成功消費10元,余額:19元
當前余額:19,小於20,充值20元成功,余額:39元
當前余額:39,大於10,成功消費10元,余額:19元
當前余額:19,小於20,充值20元成功,余額:39元
當前余額:39,大於10,成功消費10元,余額:19元
當前余額:19,小於20,充值20元成功,余額:39元
從輸出中可以看到,這個賬戶被先后反復多次充值。其原因是賬戶余額被反復修改,修改后的值和原有的數值19一樣,使得CAS操作無法正確判斷當前數據是否被修改過(是否被加過20)。雖然這種情況出現的概率不大,但是依然是有可能出現的,因此,當業務上確實可能出現這種情況時,我們必須多加防范。JDK也為我們考慮到了這種情況,使用
AtomicStampedReference可以很好地解決這個問題。
使用AtomicStampedRerence解決ABA的問題
AtomicReference無法解決上述問題的根本原因是,對象在被修改過程中丟失了狀態信息,比如充值20元的時候,需要同時標記一個狀態,用來標注用戶被充值過。因此我們只要能夠記錄對象在修改過程中的狀態值,就可以很好地解決對象被反復修改導致線程無法正確判斷對象狀態的問題。
AtomicStampedRerence正是這么做的,他內部不僅維護了對象的值,還維護了一個時間戳(我們這里把他稱為時間戳,實際上它可以使用任何一個整形來表示狀態值),當AtomicStampedRerence對應的數值被修改時,除了更新數據本身外,還必須要更新時間戳。當AtomicStampedRerence設置對象值時,對象值及時間戳都必須滿足期望值,寫入才會成功。因此,即使對象值被反復讀寫,寫回原值,只要時間戳發生變量,就能防止不恰當的寫入。
AtomicStampedRerence的幾個Api在AtomicReference的基礎上新增了有關時間戳的信息。
//比較設置,參數依次為:期望值、寫入新值、期望時間戳、新時間戳
public boolean compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp);
//獲得當前對象引用
public V getReference();
//獲得當前時間戳
public int getStamp();
//設置當前對象引用和時間戳
public void set(V newReference, int newStamp);
現在我們使用AtomicStampedRerence來修改一下上面充值的問題,代碼如下:
package com.itsoku.chat22;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicStampedReference;
/**
* 跟着阿里p7學並發,微信公眾號:javacode2018
*/
public class Demo4 {
//賬戶原始余額
static int accountMoney = 19;
//用於對賬戶余額做原子操作
static AtomicStampedReference<Integer> money = new AtomicStampedReference<>(accountMoney, 0);
/**
* 模擬2個線程同時更新后台數據庫,為用戶充值
*/
static void recharge() {
for (int i = 0; i < 2; i++) {
int stamp = money.getStamp();
new Thread(() -> {
for (int j = 0; j < 50; j++) {
Integer m = money.getReference();
if (m == accountMoney) {
if (money.compareAndSet(m, m + 20, stamp, stamp + 1)) {
System.out.println("當前時間戳:" + money.getStamp() + ",當前余額:" + m + ",小於20,充值20元成功,余額:" + money.getReference() + "元");
}
}
//休眠100ms
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
/**
* 模擬用戶消費
*/
static void consume() throws InterruptedException {
for (int i = 0; i < 50; i++) {
Integer m = money.getReference();
int stamp = money.getStamp();
if (m > 20) {
if (money.compareAndSet(m, m - 20, stamp, stamp + 1)) {
System.out.println("當前時間戳:" + money.getStamp() + ",當前余額:" + m + ",大於10,成功消費10元,余額:" + money.getReference() + "元");
}
}
//休眠50ms
TimeUnit.MILLISECONDS.sleep(50);
}
}
public static void main(String[] args) throws InterruptedException {
recharge();
consume();
}
}
輸出:
當前時間戳:1,當前余額:19,小於20,充值20元成功,余額:39元
當前時間戳:2,當前余額:39,大於10,成功消費10元,余額:19元
結果正常了。
關於這個時間戳的,在數據庫修改數據中也有類似的用法,比如2個編輯同時編輯一篇文章,同時提交,只允許一個用戶提交成功,提示另外一個用戶:博客已被其他人修改,如何實現呢?
博客表:t_blog(id,content,stamp),stamp默認值為0,每次更新+1
A、B 二個編輯同時對一篇文章進行編輯,stamp都為0,當點擊提交的時候,將stamp和id作為條件更新博客內容,執行的sql如下:
update t_blog set content = 更新的內容,stamp = stamp+1 where id = 博客id and stamp = 0;
這條update會返回影響的行數,只有一個會返回1,表示更新成功,另外一個提交者返回0,表示需要修改的數據已經不滿足條件了,被其他用戶給修改了。這種修改數據的方式也叫樂觀鎖。
對象的屬性修改原子類介紹
如果需要原子更新某個類里的某個字段時,需要用到對象的屬性修改原子類。
- AtomicIntegerFieldUpdater:原子更新整形字段的值
- AtomicLongFieldUpdater:原子更新長整形字段的值
- AtomicReferenceFieldUpdater :原子更新應用類型字段的值
要想原子地更新對象的屬性需要兩步:
-
第一步,因為對象的屬性修改類型原子類都是抽象類,所以每次使用都必須使用靜態方法 newUpdater()創建一個更新器,並且需要設置想要更新的類和屬性。
-
第二步,更新的對象屬性必須使用 public volatile 修飾符。
上面三個類提供的方法幾乎相同,所以我們這里以AtomicReferenceFieldUpdater為例子來介紹。
調用AtomicReferenceFieldUpdater靜態方法newUpdater創建AtomicReferenceFieldUpdater對象
public static <U,W> AtomicReferenceFieldUpdater<U,W> newUpdater(Class<U> tclass,
Class<W> vclass,
String fieldName)
說明:
三個參數
tclass:需要操作的字段所在的類
vclass:操作字段的類型
fieldName:字段名稱
示例
多線程並發調用一個類的初始化方法,如果未被初始化過,將執行初始化工作,要求只能初始化一次
代碼如下:
package com.itsoku.chat22;
import com.sun.org.apache.xpath.internal.operations.Bool;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
/**
* 跟着阿里p7學並發,微信公眾號:javacode2018
*/
public class Demo5 {
static Demo5 demo5 = new Demo5();
//isInit用來標注是否被初始化過
volatile Boolean isInit = Boolean.FALSE;
AtomicReferenceFieldUpdater<Demo5, Boolean> updater = AtomicReferenceFieldUpdater.newUpdater(Demo5.class, Boolean.class, "isInit");
/**
* 模擬初始化工作
*
* @throws InterruptedException
*/
public void init() throws InterruptedException {
//isInit為false的時候,才進行初始化,並將isInit采用原子操作置為true
if (updater.compareAndSet(demo5, Boolean.FALSE, Boolean.TRUE)) {
System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + ",開始初始化!");
//模擬休眠3秒
TimeUnit.SECONDS.sleep(3);
System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + ",初始化完畢!");
} else {
System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + ",有其他線程已經執行了初始化!");
}
}
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(() -> {
try {
demo5.init();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
}
輸出:
1565159962098,Thread-0,開始初始化!
1565159962098,Thread-3,有其他線程已經執行了初始化!
1565159962098,Thread-4,有其他線程已經執行了初始化!
1565159962098,Thread-2,有其他線程已經執行了初始化!
1565159962098,Thread-1,有其他線程已經執行了初始化!
1565159965100,Thread-0,初始化完畢!
說明:
- isInit屬性必須要volatille修飾,可以確保變量的可見性
- 可以看出多線程同時執行
init()方法,只有一個線程執行了初始化的操作,其他線程跳過了。多個線程同時到達updater.compareAndSet,只有一個會成功。
java高並發系列目錄
- 第1天:必須知道的幾個概念
- 第2天:並發級別
- 第3天:有關並行的兩個重要定律
- 第4天:JMM相關的一些概念
- 第5天:深入理解進程和線程
- 第6天:線程的基本操作
- 第7天:volatile與Java內存模型
- 第8天:線程組
- 第9天:用戶線程和守護線程
- 第10天:線程安全和synchronized關鍵字
- 第11天:線程中斷的幾種方式
- 第12天JUC:ReentrantLock重入鎖
- 第13天:JUC中的Condition對象
- 第14天:JUC中的LockSupport工具類,必備技能
- 第15天:JUC中的Semaphore(信號量)
- 第16天:JUC中等待多線程完成的工具類CountDownLatch,必備技能
- 第17天:JUC中的循環柵欄CyclicBarrier的6種使用場景
- 第18天:JAVA線程池,這一篇就夠了
- 第19天:JUC中的Executor框架詳解1
- 第20天:JUC中的Executor框架詳解2
- 第21天:java中的CAS,你需要知道的東西
- 第22天:JUC底層工具類Unsafe,高手必須要了解
java高並發系列連載中,總計估計會有四五十篇文章。
阿里p7一起學並發,公眾號:路人甲java,每天獲取最新文章!

