Java中的原子性、可見(jiàn)性和有序性示例詳解
一、回答重點(diǎn)
原子性、可見(jiàn)性、有序性是 Java 并發(fā)編程的三大核心特性,任何并發(fā) bug 基本都能歸到這三類里面。

1 原子性
原子性指一個(gè)操作要么全部執(zhí)行完,要么壓根沒(méi)執(zhí)行,中間不會(huì)被其他線程打斷。比如 i++ 這個(gè)操作看著像一行代碼,實(shí)際上是讀取、加 1、寫(xiě)回三個(gè)步驟,多線程環(huán)境下就可能出問(wèn)題。
2 可見(jiàn)性
可見(jiàn)性指一個(gè)線程修改了共享變量,其他線程能立刻看到最新值。CPU 有自己的高速緩存,線程修改的值可能還躺在緩存里沒(méi)刷回主內(nèi)存,別的線程就讀到了舊值。
3 有序性
有序性指程序執(zhí)行順序和代碼寫(xiě)的順序一致。編譯器和 CPU 為了性能會(huì)對(duì)指令做重排序,單線程下沒(méi)問(wèn)題,多線程就可能出現(xiàn)詭異的 bug。

下面用一個(gè)經(jīng)典的例子演示這三個(gè)問(wèn)題是怎么搞出 bug 的:
public class Singleton {
private static Singleton instance;
public static Singleton getInstance() {
if (instance == null) { // 第一次檢查
synchronized (Singleton.class) {
if (instance == null) { // 第二次檢查
instance = new Singleton(); // 問(wèn)題就出在這
}
}
}
return instance;
}
}
這段雙重檢查鎖定看起來(lái)沒(méi)毛病,但 instance = new Singleton() 這行代碼實(shí)際上分三步:分配內(nèi)存空間、初始化對(duì)象、把引用指向內(nèi)存地址。CPU 可能把第 2 步和第 3 步重排序,導(dǎo)致另一個(gè)線程拿到一個(gè)還沒(méi)初始化完的對(duì)象,直接空指針。解決辦法就是給 instance 加上 volatile。
二、擴(kuò)展知識(shí)
1. 原子性的保障手段
Java 里保證原子性主要靠?jī)煞N方式:鎖和 CAS。
synchronized 和 Lock 是最直接的手段,進(jìn)入臨界區(qū)的線程獨(dú)占資源,其他線程只能干等著。但鎖的開(kāi)銷不小,線程切換、阻塞喚醒都是重量級(jí)操作。
CAS 是一種樂(lè)觀鎖思路,底層依賴 CPU 的 cmpxchg 指令。比如 AtomicInteger 的 incrementAndGet,它會(huì)不斷嘗試"比較當(dāng)前值是否等于預(yù)期值,等于就更新",失敗就重試。CAS 避免了線程阻塞,但在競(jìng)爭(zhēng)激烈時(shí)會(huì)瘋狂自旋,CPU 空轉(zhuǎn)。
// AtomicInteger 的自增底層就是 CAS AtomicInteger count = new AtomicInteger(0); count.incrementAndGet(); // 內(nèi)部循環(huán) CAS 直到成功
JDK 8 引入了 LongAdder,思路是把一個(gè)變量拆成多個(gè) Cell,不同線程操作不同的 Cell,最后匯總。高并發(fā)場(chǎng)景下比 AtomicLong 快很多,Elasticsearch 的計(jì)數(shù)器就用的這種方案。

2. 可見(jiàn)性的底層原理
可見(jiàn)性問(wèn)題的根源在于 CPU 緩存?,F(xiàn)代 CPU 都有 L1、L2、L3 多級(jí)緩存,每個(gè)核心有自己的 L1/L2,線程讀寫(xiě)變量時(shí)優(yōu)先操作緩存。如果線程 A 在 CPU0 上改了變量,值還在 L1 緩存里,線程 B 在 CPU1 上讀的還是舊值。
volatile 的作用就是強(qiáng)制刷新緩存。寫(xiě) volatile 變量時(shí),JVM 會(huì)插入 StoreStore 屏障和 StoreLoad 屏障,把緩存里的數(shù)據(jù)刷回主內(nèi)存;讀 volatile 變量時(shí),會(huì)插入 LoadLoad 屏障和 LoadStore 屏障,強(qiáng)制從主內(nèi)存讀取。
synchronized 也能保證可見(jiàn)性。線程退出 synchronized 塊時(shí),會(huì)把所有修改刷回主內(nèi)存;進(jìn)入 synchronized 塊時(shí),會(huì)清空本地緩存,強(qiáng)制從主內(nèi)存重新加載。所以 synchronized 塊里的代碼不需要額外加 volatile。
final 字段也有可見(jiàn)性保證。JVM 保證對(duì)象構(gòu)造完成后,final 字段的值對(duì)其他線程可見(jiàn),不需要額外同步。這就是為什么 String 的 value 數(shù)組是 final 的。
3. 有序性與指令重排
編譯器和 CPU 都會(huì)做指令重排序,目的是充分利用 CPU 流水線,提高執(zhí)行效率。
編譯器重排是在生成字節(jié)碼或機(jī)器碼時(shí)調(diào)整指令順序。比如兩條不相關(guān)的賦值語(yǔ)句,編譯器可能調(diào)換順序來(lái)優(yōu)化寄存器使用。
CPU 重排更常見(jiàn),現(xiàn)代 CPU 都是亂序執(zhí)行。CPU 會(huì)把沒(méi)有數(shù)據(jù)依賴的指令并行執(zhí)行,執(zhí)行完再按原始順序提交結(jié)果。單線程下完全沒(méi)問(wèn)題,因?yàn)?CPU 保證了 as-if-serial 語(yǔ)義,執(zhí)行結(jié)果和順序執(zhí)行一樣。
但多線程環(huán)境下,A 線程的兩條指令對(duì) A 來(lái)說(shuō)沒(méi)依賴,對(duì) B 線程可能就有依賴。經(jīng)典的例子是上面的雙重檢查鎖定,對(duì)象初始化和引用賦值對(duì)構(gòu)造線程沒(méi)依賴,但其他線程可能在初始化完成前就拿到了引用。
JMM 定義了 happens-before 規(guī)則來(lái)約束重排序。只要操作 A happens-before 操作 B,那 A 的結(jié)果對(duì) B 一定可見(jiàn),A 的執(zhí)行順序也一定在 B 之前。
4. 三大特性的實(shí)現(xiàn)方式對(duì)比
優(yōu)缺點(diǎn)對(duì)比
| 特性 | volatile | synchronized | Lock | Atomic |
|---|---|---|---|---|
| 原子性 | 不保證 | 保證 | 保證 | 保證 |
| 可見(jiàn)性 | 保證 | 保證 | 保證 | 保證 |
| 有序性 | 禁止重排序 | 臨界區(qū)內(nèi)有序 | 臨界區(qū)內(nèi)有序 | 單個(gè)操作有序 |
| 性能 | 最輕 | 中等 | 可控 | 較輕 |
| 適用場(chǎng)景 | 狀態(tài)標(biāo)記 | 臨界區(qū)保護(hù) | 需要精細(xì)控制 | 計(jì)數(shù)器 |
三、面試官追問(wèn)
提問(wèn):volatile 能保證原子性嗎?為什么 volatile int count 的 count++ 不是線程安全的??
回答:volatile 只保證可見(jiàn)性和禁止重排序,不保證原子性。count++ 實(shí)際上是讀取、加 1、寫(xiě)回三個(gè)步驟,多個(gè)線程可能同時(shí)讀到同一個(gè)值,各自加 1 后寫(xiě)回,結(jié)果就少加了。想要原子自增得用 AtomicInteger 或者 synchronized。
提問(wèn):synchronized 和 volatile 在底層實(shí)現(xiàn)上有什么區(qū)別?
回答:volatile 是通過(guò)內(nèi)存屏障實(shí)現(xiàn)的,寫(xiě)操作插入 StoreStore 和 StoreLoad 屏障,讀操作插入 LoadLoad 和 LoadStore 屏障,純粹靠 CPU 指令保證,不涉及鎖。synchronized 底層是 monitor 機(jī)制,JVM 會(huì)在對(duì)象頭里記錄鎖狀態(tài),涉及到偏向鎖、輕量級(jí)鎖、重量級(jí)鎖的升級(jí)過(guò)程,重量級(jí)鎖要靠操作系統(tǒng)的互斥量,有線程切換開(kāi)銷。
提問(wèn):為什么雙重檢查鎖定的單例需要加 volatile,不加會(huì)出什么問(wèn)題?
回答:new 對(duì)象分三步:分配內(nèi)存、初始化、引用賦值。不加 volatile 的話,CPU 可能把初始化和引用賦值重排序,另一個(gè)線程可能在第一次 null 檢查時(shí)拿到一個(gè)非 null 但還沒(méi)初始化完的引用,直接用就空指針或者數(shù)據(jù)錯(cuò)亂。volatile 禁止了這種重排序。
總結(jié)
到此這篇關(guān)于Java中的原子性、可見(jiàn)性和有序性的文章就介紹到這了,更多相關(guān)Java原子性、可見(jiàn)性和有序性內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
在CentOS系統(tǒng)上安裝Java的openjdk的方法
這篇文章主要介紹了在CentOS系統(tǒng)上安裝Java的openjdk的方法,同樣適用于Fedora等其他RedHat系的Linux系統(tǒng),需要的朋友可以參考下2015-06-06
eclipse springboot工程打war包方法及再Tomcat中運(yùn)行的方法
這篇文章主要介紹了eclipse springboot工程打war包方法及再Tomcat中運(yùn)行的方法,本文圖文并茂給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2019-08-08
spring boot jpa寫(xiě)原生sql報(bào)Cannot resolve table錯(cuò)誤解決方法
在本篇文章里小編給大家整理的是關(guān)于spring boot jpa寫(xiě)原生sql報(bào)Cannot resolve table錯(cuò)誤的解決方法,需要的朋友學(xué)習(xí)下。2019-11-11
springboot?實(shí)現(xiàn)動(dòng)態(tài)刷新配置的詳細(xì)過(guò)程
這篇文章主要介紹了springboot實(shí)現(xiàn)動(dòng)態(tài)刷新配置,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-05-05
淺析如何在IDEA中高效使用Test注解進(jìn)行單元測(cè)試
在軟件開(kāi)發(fā)過(guò)程中,單元測(cè)試是保證代碼質(zhì)量的重要手段之一,那么如何在IDEA中高效使用Test注解進(jìn)行單元測(cè)試呢,下面小編就來(lái)和大家簡(jiǎn)單講講2025-04-04
SpringBoot整合WebSocket實(shí)現(xiàn)后端向前端主動(dòng)推送消息方式
這篇文章主要介紹了SpringBoot整合WebSocket實(shí)現(xiàn)后端向前端主動(dòng)推送消息方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-10-10
淺析Java如何利用Spire.PDF for Java實(shí)現(xiàn)將PDF轉(zhuǎn)換為Word
在日常開(kāi)發(fā)和辦公中,PDF 格式以其穩(wěn)定的版式和跨平臺(tái)兼容性廣受歡迎,本文將為你揭示如何利用強(qiáng)大的 Spire.PDF for Java 庫(kù),輕松實(shí)現(xiàn) PDF 到 Word 的轉(zhuǎn)換,感興趣的小伙伴可以了解下2026-01-01
idea輸入sout無(wú)法自動(dòng)補(bǔ)全System.out.println()的問(wèn)題
這篇文章主要介紹了idea輸入sout無(wú)法自動(dòng)補(bǔ)全System.out.println()的問(wèn)題,本文給大家分享解決方案,供大家參考,需要的朋友可以參考下2020-07-07

