Java volatile 關(guān)鍵字介紹與使用示例詳解
大家好,我是栗箏i,這篇文章是我的 “栗箏i 的 Java 技術(shù)棧” 專(zhuān)欄的第 026 篇文章,在 “栗箏i 的 Java 技術(shù)棧” 這個(gè)專(zhuān)欄中我會(huì)持續(xù)為大家更新 Java 技術(shù)相關(guān)全套技術(shù)棧內(nèi)容。專(zhuān)欄的主要目標(biāo)是已經(jīng)有一定 Java 開(kāi)發(fā)經(jīng)驗(yàn),并希望進(jìn)一步完善自己對(duì)整個(gè) Java 技術(shù)體系來(lái)充實(shí)自己的技術(shù)棧的同學(xué)。與此同時(shí),本專(zhuān)欄的所有文章,也都會(huì)準(zhǔn)備充足的代碼示例和完善的知識(shí)點(diǎn)梳理,因此也十分適合零基礎(chǔ)的小白和要準(zhǔn)備工作面試的同學(xué)學(xué)習(xí)。當(dāng)然,我也會(huì)在必要的時(shí)候進(jìn)行相關(guān)技術(shù)深度的技術(shù)解讀,相信即使是擁有多年 Java 開(kāi)發(fā)經(jīng)驗(yàn)的從業(yè)者和大佬們也會(huì)有所收獲并找到樂(lè)趣。
–
在現(xiàn)代多線程編程中,確保數(shù)據(jù)的一致性和正確性是至關(guān)重要的。Java 作為一種廣泛使用的編程語(yǔ)言,為多線程編程提供了豐富的工具和機(jī)制,其中
volatile關(guān)鍵字是一個(gè)關(guān)鍵的概念。volatile關(guān)鍵字在 Java 中被用來(lái)修飾變量,以確保它們?cè)诙嗑€程環(huán)境下的可見(jiàn)性和有序性,但它并不保證操作的原子性。理解
volatile的工作原理及其應(yīng)用場(chǎng)景,對(duì)于編寫(xiě)高效和可靠的多線程程序至關(guān)重要。在本文中,我們將深入探討volatile關(guān)鍵字的核心特性,解釋它如何確保變量的可見(jiàn)性和有序性,以及它在解決多線程問(wèn)題中的局限性。我們還將通過(guò)示例展示如何在實(shí)際編程中使用volatile,以及如何通過(guò)其他同步機(jī)制來(lái)彌補(bǔ)volatile的不足。通過(guò)對(duì)
volatile的詳細(xì)分析,我們希望讀者能夠更好地理解在多線程環(huán)境中變量訪問(wèn)的復(fù)雜性,并掌握在實(shí)際開(kāi)發(fā)中如何正確使用volatile關(guān)鍵字,以編寫(xiě)出更加健壯和高效的并發(fā)程序。
1、volatile 關(guān)鍵字簡(jiǎn)介
volatile 關(guān)鍵字在 Java 中用于修飾變量,使其具有可見(jiàn)性和有序性。
- 可見(jiàn)性:在多線程環(huán)境下,當(dāng)一個(gè)線程修改了
volatile變量的值,新值對(duì)于其他線程是立即可見(jiàn)的。通常情況下,線程之間對(duì)變量的讀寫(xiě)操作是不可見(jiàn)的,這意味著一個(gè)線程修改了變量的值,另一個(gè)線程可能看不到這個(gè)修改,仍然使用舊值。使用volatile關(guān)鍵字可以確保所有線程看到的是變量的最新值; - 有序性:
volatile關(guān)鍵字還可以防止指令重排序優(yōu)化。編譯器和處理器通常會(huì)對(duì)指令進(jìn)行重排序,以提高性能,但這種重排序可能會(huì)破壞多線程程序的正確性。volatile變量的讀寫(xiě)操作不會(huì)被重排序,也不會(huì)與前后的讀寫(xiě)操作發(fā)生重排序。
需要注意的是 volatile 僅能保證可見(jiàn)性和有序性,不能保證原子性。例如,volatile int count 的遞增操作 count++ 仍然不是線程安全的,因?yàn)樗俗x和寫(xiě)兩個(gè)操作,可能會(huì)被其他線程打斷。
在復(fù)雜的同步場(chǎng)景中,可能需要使用 synchronized 或其他并發(fā)工具來(lái)確保線程安全。
2、volatile 保證可見(jiàn)性
在多線程編程中,線程之間共享變量的訪問(wèn)可能會(huì)出現(xiàn)可見(jiàn)性問(wèn)題,即一個(gè)線程對(duì)變量的修改可能不會(huì)被其他線程立即看到。Java 提供了 volatile 關(guān)鍵字來(lái)解決這種可見(jiàn)性問(wèn)題。
2.1、什么是可見(jiàn)性問(wèn)題
當(dāng)一個(gè)線程修改了某個(gè)變量的值,如果這個(gè)修改對(duì)其他線程是不可見(jiàn)的,可能會(huì)導(dǎo)致程序出現(xiàn)非預(yù)期的行為。例如,一個(gè)線程修改了變量 flag 的值,但其他線程仍然讀取的是舊值:
public class VisibilityProblem {
private boolean flag = true;
public void stop() {
flag = false;
}
public void run() {
while (flag) {
// 執(zhí)行任務(wù)
}
}
}在這個(gè)例子中,如果 flag 變量沒(méi)有被聲明為 volatile,當(dāng)一個(gè)線程調(diào)用 stop 方法將 flag 設(shè)置為 false 后,另一個(gè)正在運(yùn)行 run 方法的線程可能無(wú)法立即看到這個(gè)變化,仍然會(huì)在 while (flag) 循環(huán)中繼續(xù)執(zhí)行。
2.2、volatile 如何保證可見(jiàn)性
volatile 關(guān)鍵字通過(guò)以下機(jī)制確保變量的可見(jiàn)性:
- 內(nèi)存可見(jiàn)性協(xié)議:
- 每個(gè)線程都有自己的本地緩存,當(dāng)一個(gè)線程對(duì)變量進(jìn)行讀寫(xiě)操作時(shí),實(shí)際上是從本地緩存中讀取或?qū)懭氲模皇侵苯硬僮髦鲀?nèi)存中的變量。
- 當(dāng)一個(gè)變量被聲明為
volatile時(shí),所有線程對(duì)該變量的讀寫(xiě)操作都將直接操作主內(nèi)存,而不是使用本地緩存。 - 當(dāng)一個(gè)線程修改了
volatile變量的值,這個(gè)新值會(huì)立即刷新到主內(nèi)存中。 - 任何線程在讀取
volatile變量時(shí),都會(huì)從主內(nèi)存中讀取最新的值,而不是從本地緩存中讀取舊值。
- 內(nèi)存屏障:
volatile關(guān)鍵字在底層實(shí)現(xiàn)中,會(huì)在變量的讀寫(xiě)操作前后插入內(nèi)存屏障(Memory Barrier)。- 內(nèi)存屏障確保了指令的執(zhí)行順序,防止編譯器和處理器對(duì)
volatile變量的讀寫(xiě)操作進(jìn)行重排序。 - 寫(xiě)內(nèi)存屏障:確保在寫(xiě)
volatile變量之前的所有寫(xiě)操作都已經(jīng)完成,并且結(jié)果對(duì)其他線程可見(jiàn)。 - 讀內(nèi)存屏障:確保在讀
volatile變量之后的所有讀操作都能讀取到最新的值。
示例代碼:
public class VolatileExample {
private volatile boolean running = true;
public void stop() {
running = false;
}
public void run() {
while (running) {
// 執(zhí)行任務(wù)
}
}
public static void main(String[] args) {
VolatileExample example = new VolatileExample();
Thread thread = new Thread(example::run);
thread.start();
try {
Thread.sleep(1000); // 讓線程運(yùn)行一段時(shí)間
} catch (InterruptedException e) {
e.printStackTrace();
}
example.stop(); // 停止線程
}
}在這個(gè)例子中,running 變量被聲明為 volatile,確保 stop 方法對(duì) running 的修改能夠立即被 run 方法中的循環(huán)檢測(cè)到。
3、volatile 保證有序性
在多線程編程中,指令重排序(Instruction Reordering)可能會(huì)導(dǎo)致程序的執(zhí)行順序與代碼的書(shū)寫(xiě)順序不一致,從而引發(fā)不可預(yù)測(cè)的問(wèn)題。volatile 關(guān)鍵字通過(guò)內(nèi)存屏障(Memory Barrier)機(jī)制,防止指令重排序,確保代碼執(zhí)行的有序性。
3.1、什么是指令重排序
為了優(yōu)化程序的執(zhí)行速度,編譯器和處理器會(huì)對(duì)指令進(jìn)行重排序。重排序包括以下三種類(lèi)型:
- 編譯器重排序:編譯器在生成機(jī)器指令時(shí),可以重新安排代碼的執(zhí)行順序。
- 處理器重排序:處理器可以在運(yùn)行時(shí)對(duì)指令進(jìn)行重排序,以充分利用處理器流水線。
- 內(nèi)存系統(tǒng)重排序:由于緩存、寫(xiě)緩沖區(qū)等原因,內(nèi)存操作的順序可能與程序代碼的順序不同。
盡管重排序不會(huì)改變單線程程序的語(yǔ)義,但在多線程環(huán)境下,重排序可能會(huì)導(dǎo)致線程間的操作順序不一致,從而引發(fā)數(shù)據(jù)競(jìng)爭(zhēng)和線程安全問(wèn)題。
3.2、volatile 如何保證有序性
volatile 關(guān)鍵字通過(guò)插入內(nèi)存屏障,確保指令的執(zhí)行順序。內(nèi)存屏障是一種同步機(jī)制,防止特定類(lèi)型的指令在重排序時(shí)被移動(dòng)到屏障的另一側(cè)。volatile 變量的讀寫(xiě)操作前后會(huì)插入內(nèi)存屏障,確保有序性:
- 寫(xiě)內(nèi)存屏障(Store Barrier):在寫(xiě)
volatile變量之前插入,確保在此屏障之前的所有寫(xiě)操作都已完成,并且結(jié)果對(duì)其他線程可見(jiàn); - 讀內(nèi)存屏障(Load Barrier):在讀
volatile變量之后插入,確保在此屏障之后的所有讀操作能讀取到最新的值。
具體而言,volatile 保證了以下兩點(diǎn):
寫(xiě) volatile 變量之前的所有寫(xiě)操作不會(huì)被重排序到 volatile 寫(xiě)之后;讀 volatile 變量之后的所有讀操作不會(huì)被重排序到 volatile 讀之前。
示例代碼:
public class VolatileOrderingExample {
private volatile boolean flag = false;
private int a = 0;
public void writer() {
a = 1; // 寫(xiě)普通變量
flag = true; // 寫(xiě)volatile變量
}
public void reader() {
if (flag) { // 讀volatile變量
int i = a; // 讀普通變量
// `i` 將是 1,因?yàn)?`flag` 為 true 時(shí),`a` 必定已經(jīng)被寫(xiě)為 1
}
}
}在這個(gè)例子中,writer 方法中對(duì) a 的寫(xiě)操作不會(huì)被重排序到 flag 之后,因此在 reader 方法中,一旦檢測(cè)到 flag 為 true,就能確保讀取到的 a 的值是最新的 1。
4、volatile 不保證原子性的詳細(xì)介紹
在多線程編程中,volatile 關(guān)鍵字可以保證變量的可見(jiàn)性和有序性,但不能保證操作的原子性。原子性(Atomicity)指的是操作在執(zhí)行過(guò)程中不可分割,要么全部執(zhí)行,要么全部不執(zhí)行。
4.1、什么是原子性問(wèn)題
在多線程環(huán)境下,非原子操作可能會(huì)導(dǎo)致數(shù)據(jù)不一致。例如,自增操作 i++ 看似簡(jiǎn)單,但它實(shí)際上由三步組成:
- 讀取變量
i的當(dāng)前值; - 將
i的值加 1; - 將新值寫(xiě)回
i。
這三步操作在多線程環(huán)境下可能會(huì)被打斷,從而導(dǎo)致數(shù)據(jù)競(jìng)爭(zhēng)問(wèn)題。假設(shè)兩個(gè)線程同時(shí)執(zhí)行 i++ 操作:
- 線程 A 讀取
i的值為 5。 - 線程 B 讀取
i的值為 5。 - 線程 A 將
i的值加 1 并寫(xiě)回,i的值變?yōu)?6。 - 線程 B 將
i的值加 1 并寫(xiě)回,i的值變?yōu)?6。
最終結(jié)果是,雖然兩個(gè)線程都執(zhí)行了 i++ 操作,但 i 的值只增加了 1。這就是因?yàn)?i++ 操作不是原子的。
4.2、volatile 的局限性
volatile 僅能確保變量的可見(jiàn)性和有序性,但不能確保操作的原子性。換句話(huà)說(shuō),使用 volatile 修飾的變量雖然可以在多個(gè)線程之間及時(shí)同步,但多個(gè)線程對(duì)該變量的復(fù)合操作(如自增、自減)仍然會(huì)存在數(shù)據(jù)競(jìng)爭(zhēng)問(wèn)題。
以下是一個(gè)例子,說(shuō)明了 volatile 不保證原子性的問(wèn)題:
public class VolatileNonAtomic {
private volatile int count = 0;
public void increment() {
count++;
}
public static void main(String[] args) throws InterruptedException {
VolatileNonAtomic example = new VolatileNonAtomic();
Runnable task = () -> {
for (int i = 0; i < 1000; i++) {
example.increment();
}
};
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("Final count: " + example.count);
}
}在這個(gè)例子中,盡管 count 變量被聲明為 volatile,但由于 increment 方法中的 count++ 操作不是原子的,最終的 count 值可能小于 2000。
4.3、解決方法
為了確保操作的原子性,可以使用以下方法:
使用 synchronized 關(guān)鍵字:將操作包裝在同步塊中,確保操作的原子性。
public class SynchronizedExample {
private int count = 0;
public synchronized void increment() {
count++;
}
}使用原子類(lèi):Java 提供了 java.util.concurrent.atomic 包中的原子類(lèi)(如 AtomicInteger、AtomicLong)來(lái)確保操作的原子性。
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicExample {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet();
}
}到此這篇關(guān)于Java volatile 關(guān)鍵字介紹與使用的文章就介紹到這了,更多相關(guān)java volatile 關(guān)鍵字內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java中轉(zhuǎn)換器設(shè)計(jì)模式深入講解
這篇文章主要給大家介紹了關(guān)于Java中轉(zhuǎn)換器設(shè)計(jì)模式的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用Java具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-04-04
Java上傳文件進(jìn)度條的實(shí)現(xiàn)方法(附demo源碼下載)
這篇文章主要介紹了Java上傳文件進(jìn)度條的實(shí)現(xiàn)方法,可簡(jiǎn)單實(shí)現(xiàn)顯示文件上傳比特?cái)?shù)及進(jìn)度的功能,并附帶demo源碼供讀者下載參考,需要的朋友可以參考下2015-12-12
解析Java的Jackson庫(kù)中Streaming API的使用
這篇文章主要介紹了解析Java的Jackson庫(kù)中Streaming API的使用,Jackson被用于Java對(duì)象和JSON的互相轉(zhuǎn)換,需要的朋友可以參考下2016-01-01
Java中的數(shù)組流ByteArrayOutputStream用法
Java中的ByteArrayOutputStream是java.io包中的一個(gè)類(lèi),用于在內(nèi)存中創(chuàng)建字節(jié)數(shù)組緩沖區(qū),支持動(dòng)態(tài)擴(kuò)展,它繼承自O(shè)utputStream,允許以字節(jié)形式寫(xiě)入數(shù)據(jù),無(wú)需與外部設(shè)備交互,常用方法包括write()、toByteArray()、toString()等2024-09-09
Druid(新版starter)在SpringBoot下的使用教程
Druid是Java語(yǔ)言中最好的數(shù)據(jù)庫(kù)連接池,Druid能夠提供強(qiáng)大的監(jiān)控和擴(kuò)展功能,DruidDataSource支持的數(shù)據(jù)庫(kù),這篇文章主要介紹了Druid(新版starter)在SpringBoot下的使用,需要的朋友可以參考下2023-05-05
Java性能工具JMeter實(shí)現(xiàn)上傳與下載腳本編寫(xiě)
性能測(cè)試工作中,文件上傳也是經(jīng)常見(jiàn)的性能壓測(cè)場(chǎng)景之一,那么 JMeter 文件上傳下載腳本怎么做,本文詳細(xì)的來(lái)介紹一下,感興趣的可以了解一下2021-07-07

