《死磕 Java 并發編程》系列連載中,大家可以關注一波,
「死磕 Java 并發編程」阿里二面,面試官:說說 Java CAS 原理?
「死磕 Java 并發編程」面試官:說說什么是 Java 記憶體模型(JMM)?
「死磕 Java 并發編程」10張圖告訴你Java并發多執行緒那些破事
目錄
執行緒安全真的是執行緒的安全嗎?
什么是 Atomic?
實作一個計數器
AtomicInteger 原始碼分析
AtomicLong 和 LongAdder 誰更牛?
總結
當我們談論『執行緒安全』的時候,肯定都會想到 Atomic 類,不錯,Atomic 相關類都是執行緒安全的,在講 Atomic 類之前我想再聊聊『執行緒安全』這個概念,
執行緒安全真的是執行緒的安全嗎?
初看『執行緒安全』這幾個字,很容易望文生義,這不就是執行緒的安全嗎?其實不是,執行緒本身沒有好壞,沒有『安全的執行緒』和『不安全的執行緒』之分,俗話說:人之初性本善,執行緒天生也是純潔善良的,真正讓執行緒變壞是因為訪問的變數的原因,變數對于作業系統來說其實就是記憶體塊,所以繞了這么一大圈,執行緒安全稱為『記憶體的安全』可能更為貼切,
簡而言之,執行緒訪問的記憶體決定了這個執行緒是否是安全的,
變數大致可以分為區域變數和共享變數,區域變數對于 JVM 來說是堆疊空間,大家都背過八股文,堆疊是執行緒私有的是非共享的,那自然也是記憶體安全的;共享變數對于 JVM 來說一般是存在于堆上,堆上的東西是所有執行緒共享的,如果不加任何限制自然是不安全的,
因為執行緒安全這個概念已經深入人心了,所以后面我們還是用執行緒安全來表達記憶體安全的含義,
那如何解決這種不安全呢?方法有很多,比如:加鎖、Atomic 原子類等,
好了,咱們今天先來看看Atomic類,
什么是 Atomic?
Java從JDK1.5開始提供java.util.concurrent.atomic包,這里包含了多個原子操作類,原子操作類提供了一個簡單、高效、安全的方式去更新一個變數,

Atomic 包下的原子操作類有很多,可以大致分為四種型別:
-
原子操作基本型別
-
原子操作陣列型別
-
原子操作參考型別
-
原子操作更新屬性
Atomic原子操作類在原始碼中都使用了Unsafe類,Unsafe類提供了硬體級別的原子操作,可以安全地直接操作記憶體變數,后面講解原始碼時再詳細介紹,
實作一個計數器
假如在業務代碼中需要實作一個計數器的功能,啪地一下,很快我們就寫出了以下的代碼:
/**
* Author: 公眾號 愛笑的架構師
*/
public class Counter {
private int count;
public void increase() {
count++;
}
}
increase方法對 count 變數進行遞增,
當代碼提交上庫進行code review時,啪地一下,很快收到了檢視意見(嚴重級別):
如果在多執行緒場景下,你的計數器可能有問題,
上大一的時候老師就講過 count++ 是非原子性的,它實際上包含了三個操作:讀資料,加一,寫回資料,
再次修改代碼,多線訪問increase方法會有問題,那就給它加個鎖吧,count變數修改了其他執行緒可能不能即時看到,那就給變數加個 volatile 吧,
吭哧吭哧,代碼如下:
/**
* Author: 公眾號 愛笑的架構師
*/
public class LockCounter {
private volatile int count;
public synchronized void increase() {
count++;
}
}
一頓操作猛如虎,再次提交代碼后,依然收到了檢視意見(建議級別):
加鎖會影響效率,可以考慮使用原子操作類,
原子操作類?「黑人問號臉」,莫不是大佬知道我晚上有約會故意整我,不想合入代碼吧,帶著將信將疑的態度,打開百度谷歌,原來 AtomicInteger 可以輕松解決這個問題,手忙腳亂一頓復制粘貼代碼搞定了,終于可以下班了,
/**
* Author: 公眾號 愛笑的架構師
*/
public class AtomicCounter {
private AtomicInteger count = new AtomicInteger(0);
public void increase() {
count.incrementAndGet();
}
}
AtomicInteger 原始碼分析
呼叫AtomicInteger類的incrementAndGet方法不用加鎖可以實作安全的遞增,這個好神奇,下面帶領大家分析一下原始碼是這么實作的,等不及了等不及了,
打開原始碼,可以看到定義的incrementAndGet方法:
/**
* 在當前值的基礎上自動加 1
*
* @return 更新后的值
*/
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
通過原始碼可以看到實際上是呼叫了 unsafe 的一個方法,unsafe 是什么待會再說,
我們再看看getAndAddInt方法的引數:第一個引數 this 是當前物件的參考;第二個引數valueOffset是用來記錄value值在記憶體中的偏移地址,第三個引數是一個常量 1;
在 AtomicInteger 中定義了一個常量valueOffset和一個可變的成員變數 value:
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;
value 變數保存當前物件的值,valueOffset 是變數的記憶體偏移地址,也是通過呼叫unsafe的方法獲取,
public final class Unsafe {
// ……省略其他方法
public native long objectFieldOffset(Field f);
}
這里再說說 Unsafe 這個類,人如其名:不安全的類,打開 Unsafe 類會看到大部分方法都標識了 native,也就是說這些都是本地方法,本地方法強依賴于作業系統平臺,一般都是采用C/C++語言撰寫,在呼叫 Unsafe 類的本地方法實際會執行這些方法,熟悉 C/C++的小伙伴可自行下載原始碼研究,
好了,我們再回到最開始,呼叫了 Unsafe 類的getAndAddInt方法:
public final class Unsafe {
// ……省略其他方法
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset);
// 回圈 CAS 操作
} while (!compareAndSwapInt(o, offset, v, v + delta));
return v;
}
// 根據記憶體偏移地址獲取當前值
public native int getIntVolatile(Object o, long offset);
// CAS 操作
public final native boolean compareAndSwapInt(Object o, long offset,
int expected,
int x);
}
通過getIntVolatile方法獲取當前 AtomicInteger 物件的value值,這是一個本地方法,
然后呼叫compareAndSwapInt進行 CAS 原子操作,嘗試在當前值的基礎上加 1,如果 CAS 失敗會回圈進行重試,
因此compareAndSwapInt方法是最核心的,詳細實作大家可以自行找原始碼看,這里我們看看方法的引數,一共有四個引數:o 是指當前物件;offset 是指當前物件值的記憶體偏移地址;expected是期望值;x是修改后的值;
compareAndSwapInt方法的思路是拿到物件 o 和 offset 后會再去取物件實際的值,如果當前值與之前取的期望值是一致的就認為 value 沒有被修改過,直接將 value 的值更新為 x,這樣就完成了一次 CAS 操作,CAS 操作是通過作業系統保證原子性的,
如果當前值與期望值不一致,說明 value 值被修改過,那么就會重試 CAS 操作直到成功,

AtomicInteger類中還有很多其他的方法,如:
decrementAndGet()
getAndDecrement()
getAndIncrement()
accumulateAndGet()
// …… 省略
這些方法實作原理都是大同小異,希望大家可以舉一反三理解其他的方法,
另外還有一些其他的類,如:AtomicLong,AtomicReference,AtomicIntegerArray等,這里也不再贅述,原理都是大同小異,
AtomicLong 和 LongAdder 誰更牛?
Java 在 jdk1.8版本 引入了 LongAdder 類,與 AtomicLong 一樣可以實作加、減、遞增、遞減等執行緒安全操作,但是在高并發競爭非常激烈的場景下 LongAdder 的效率更勝一籌,后續單獨用一篇文章進行介紹,
總結
講了半天,可能有的小伙伴還是比較懵,Atomic 類到底是如何實作執行緒安全的?
在語言層面上,Atomic 類是沒有做任何同步操作的,翻看源代碼方法沒有任何加鎖,其實最大功勞還是在 CAS 身上,CAS 利用作業系統的硬體特性實作了原子性,利用 CPU 多核能力實作了硬體層面的阻塞,
只有 CAS 的原子性保證就一定是執行緒安全的嗎?當然不是的,通過原始碼發現 value 變數還用了 volatile 修飾了,保證了執行緒可見性,
那有些小伙伴可能要問了,那是不是加鎖就沒有用了,非也,雖然基于 CAS 的執行緒安全機制很好很高效,但是這適合一些粒度比較小的需求才有效,如果遇到非常復雜的業務邏輯還是需要加鎖操作的,
大家學會了嗎?
Java 并發編程的知識非常多,同時也是 Java 面試的高頻考點,面試官必問的,需要學習 Java 并發編程其他知識的小伙伴可以去下載『阿里師兄總結的Java知識筆記 總共 283 頁,超級詳細』,
作者:雷小帥
推薦一個Github 開源專案,『Java八股文』Java面試套路,Java進階學習,打破內卷拿大廠Offer,升職加薪!
作者簡介: ?讀過幾年書:華中科技大學碩士畢業;
😂浪過幾個大廠:華為、網易、百度……
😘一直堅信技術能改變世界,愿保持初心,加油技術人!微信搜索公眾號【愛笑的架構師】,關注這個對技術有追求且有趣的打工人,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/287107.html
標籤:其他
