volatile關鍵字原理的使用介紹和底層原了解析和使用執行個體
1. volatile 關鍵字的作用
volatile 關鍵字的主要作用是保證可見性和有序性,禁止編譯器優化。
- 保證可見性:當一個變量被聲明為 volatile 之後,每次讀取這個變量的值都會從主記憶體中讀取,而不是從緩存中讀取,這就保證了不同線程對這個變量操作的可見性。
- 有序性:volatile 關鍵字保證了不同線程對一個 volatile 變量的讀寫操作的有序性。
- 禁止編譯器優化:編譯器會對代碼進行各種優化來提高性能,但是這些優化也可能讓同步代碼失效。volatile 關鍵字告訴編譯器不要對這段代碼做優化,進而避免一些不正确的優化。
2. volatile 的底層原理
volatile 關鍵字底層原理依賴于記憶體屏障和緩存一緻性協定。
- 記憶體屏障:記憶體屏障會強制讓讀和寫操作都通路主記憶體,進而實作可見性。volatile 寫操作後會加入寫屏障,volatile 讀操作前會加入讀屏障。
- 緩存一緻性協定:每個處理器都有自己的高速緩存,當某個處理器修改了共享變量,需要緩存一緻性協定來保證其他處理器也看到修改後的值。緩存一緻性協定會在讀操作後和寫操作前加入緩存重新整理操作,保證其他處理器的緩存是最新值。
3. volatile 的使用案例
volatile 關鍵字常用在 DCL(Double Check Lock)單例模式中:
public class Singleton {
private volatile static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
複制代碼
這裡使用 volatile 是為了防止指令重排序,保證 instance 初始化後其他線程可以看到。
volatile 也常用在Interruptible線程中,實作線程的中斷功能:
public class InterruptibleThread extends Thread {
private volatile boolean interrupted = false;
public void interrupt() {
interrupted = true;
}
@Override
public void run() {
while (!interrupted) {
// do something
}
}
}
複制代碼
這裡 volatile 可以保證 interrupted 的可見性,使線程立即響應中斷調用。
4. volatile 的原子性問題
volatile 關鍵字隻能保證可見性和有序性,不能保證原子性。
對一個 volatile 變量的讀寫操作并不是原子的,而是可以分為讀、改、寫三個操作:
- 讀: 讀取 volatile 變量的值
- 改:對值進行修改
- 寫:将修改後的值寫入 volatile 變量
這三個操作并不是一個原子操作,在多線程環境下可能導緻資料競争問題:
public class VolatileNoAtomicDemo {
private volatile int counter = 0;
public void increase() {
counter++; // 不是原子操作
}
}
複制代碼
這裡的 counter++ 實際上分為三步:
- 讀:讀取 counter 的值,假設為 x
- 改:x + 1
- 寫:将 x + 1 的結果寫入 counter
在多線程環境下,如果兩個線程同時執行 increase 方法,很有可能達不到預期結果,這就是因為 counter++ 不是一個原子操作導緻的。
5. 如何解決 volatile 的原子性問題
要解決 volatile 的原子性問題,可以使用 synchronized 或 Atomic 包中的類。
使用 synchronized:
public synchronized void increase() {
counter++;
}
複制代碼
使用 AtomicInteger:
private AtomicInteger counter = new AtomicInteger(0);
public void increase() {
counter.getAndIncrement();
}
複制代碼
AtomicInteger 中的方法都是原子操作,可以解決 volatile 的原子性問題。
synchronized 會影響性能,AtomicInteger 的性能更好,是以一般優先選擇 Atomic 包中的原子類。
6. volatile 的實作原理
volatile 的實作原理依賴于 JMM(Java Memory Model)中的幾個概念:
- 主記憶體:所有線程都可以通路的記憶體,存儲共享變量的值。
- 工作記憶體:每個線程私有的記憶體,用于存儲線程使用的變量值。
- 記憶體屏障:控制讀寫的順序,用于保證特定操作的完成後才允許執行後續操作。
volatile 的實作原理是:
- 當一個線程修改一個volatile變量的值時,它會在變量修改後立即重新整理回主記憶體。
- 當一個線程讀取一個volatile變量的值時,它會直接從主記憶體讀取,而不是從工作記憶體讀取。
- 它會在讀後和寫前加入記憶體屏障,以保證指令重排不會将記憶體操作重排到屏障另一側。
這樣就實作了:
- 可見性:因為每次直接讀寫主記憶體,是以每個線程都可以獲得最新值。
- 有序性:記憶體屏障會阻止重排,讀寫順序由代碼決定。
- 禁止編譯器優化:因為每次都要從主記憶體讀寫,編譯器難以對其進行優化。
JMM的這幾個概念配合volatile關鍵字的實作原理,就保證了多線程環境下volatile變量的可見性、有序性和禁止編譯器優化。
7. 小結
- volatile關鍵字主要保證可見性、有序性和禁止編譯器優化。
- volatile的底層原理是依賴記憶體屏障和緩存一緻性協定實作的。
- volatile不能保證原子性,要配合synchronized或Atomic類解決。
- volatile的實作依賴JMM中的主記憶體、工作記憶體和記憶體屏障等概念。
8. volatile的最佳實踐
根據volatile的特性,我們可以總結出一些最佳實踐:
- 不要過度使用volatile
- volatile關鍵字會影響程式性能,是以不要過度使用,隻在真正需要可見性和有序性保證的地方使用。
- 與synchronized一起使用
- 當需要保證原子性時,volatile關鍵字需要與synchronized關鍵字一起使用。synchronized可以保證代碼塊的原子性,volatile可以保證資料的可見性。
- 使用Atomic類代替synchronized和volatile
- Atomic類提供的方法都是原子操作,性能比synchronized更好,同時可以保證可見性,是以在需要保證原子性的場景可以優先選擇Atomic類。
- 禁止把long和double類型變量聲明為volatile
- 根據JMM規範,對64位資料類型的讀寫操作不一定是原子的,是以不要将long和double類型的變量聲明為volatile。可以使用AtomicLong和AtomicDouble類代替。
- volatile不保證順序
- volatile關鍵字隻能保證有序性,不能保證順序。有序性是指:在一個線程内,不會由于編譯器優化和處理器重新排序,使得對一個volatile變量的寫操作排在讀操作之前。順序是指:兩個線程通路同一個變量的順序。是以不要依賴volatile保證線程間的順序。
- volatile變量不能保護其它非volatile變量
- 在使用volatile變量控制住多線程變量的可見性時,不要認為它可以保護其它非volatile變量。每個變量都需要單獨使用volatile或synchronized來保護。
9. 案例:使用volatile實作雙重檢查鎖定
雙重檢查鎖定(Double Check Locking)是一種使用同步控制并發通路的方式,可以實作延遲初始化。它通過兩次對對象引用進行空檢查來避免同步,進而提高性能。
但是在Java中,普通的雙重檢查鎖定是不起作用的,原因是有指令重排的存在,可能導緻另一個線程看到對象引用不是null,但是對象資源還沒有完成初始化。
使用volatile關鍵字可以禁止指令重排,實作雙重檢查鎖定。代碼示例:
public class Singleton {
private volatile static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
複制代碼
這裡把instance變量聲明為volatile,可以禁止指令重排,保證在對象完成初始化後,其他線程可以正确看到instance不為null。
這種方式是實作Singleton模式的最佳方式,它隻有第一次調用getInstance方法時才會同步,這樣既可以實作線程安全,又有很高的性能。
10. 案例:使用volatile實作中斷機制
我們可以使用一個volatile變量作為中斷标志,在循環體内檢查這個變量,一次循環檢查後立即重新讀取變量的值,保證對變量修改的可見性,進而實作中斷機制。
public class VolatileInterruptionDemo extends Thread {
private volatile boolean interrupted = false;
@Override
public void run() {
while (!interrupted) {
// do something
}
System.out.println("Interrupted!");
}
public void interrupt() {
interrupted = true;
}
}
複制代碼
這裡的interrupted變量被聲明為volatile,可以保證線程可以感覺到中斷信号,從循環體内退出。
這就是使用volatile實作的一種簡單的中斷機制,利用了volatile的可見性來保證線程可以正确讀取到最新的中斷标志。
11. 案例:使用AtomicInteger代替volatile
前面提到過,volatile不能保證原子性,要解決這個問題可以使用synchronized或Atomic類。這裡我們通過一個例子來展示如何使用AtomicInteger代替volatile。
先看一個使用volatile的例子:
public class VolatileDemo {
private volatile int counter = 0;
public void increase() {
counter++;
}
public int getCounter() {
return counter;
}
}
複制代碼
這裡的counter++不是一個原子操作,在多線程環境下會存在資料競争問題。
現在使用AtomicInteger代替:
public class AtomicDemo {
private AtomicInteger counter = new AtomicInteger(0);
public void increase() {
counter.getAndIncrement();
}
public int getCounter() {
return counter.get();
}
}
複制代碼
AtomicInteger的getAndIncrement()方法是一個CAS原理的原子操作,可以保證線程安全。
AtomicInteger使用CAS操作實作原子操作,CAS操作包含三個操作:
- 擷取變量的目前值V
- 對V的值進行操作
- 使用CAS操作設定變量的值,這個設定值的操作需要提供變量的目前值V和新值,當變量的目前值還是V時才會設定新值,否則重新擷取目前值。
CAS操作可以保證如果在多個線程同時使用一個變量時,隻有一個線程可以更新變量的值,其他線程的設定值操作都會失敗,這種機制可以實作原子操作。
是以,通過這個例子我們可以看出,AtomicInteger是一個很好的替代volatile的選擇,它可以保證原子性也具有volatile所有特性,性能也更好,是實作原子操作的最佳選擇。
12. 案例:基于volatile實作一個簡單的并發容器
這裡我們實作一個簡單的線程安全的容器,它隻包含兩個方法:add()和size()。
使用volatile和synchronized實作如下:
public class VolatileContainer {
private volatile int size = 0;
private Object[] items = new Object[10];
public void add(Object item) {
synchronized (items) {
items[size] = item;
size++;
}
}
public int size() {
return size;
}
}
複制代碼
這裡使用volatile聲明size變量來保證線程安全,同時使用synchronized對items數組加鎖來保證添加操作的原子性。
size()方法隻需要簡單的讀取size變量,由于它被聲明為volatile,可以保證每次得到的都是最新大小值。
這是一個使用volatile和synchronized實作的簡單線程安全容器,利用了volatile的可見性和synchronized的互斥鎖來保證線程安全。
相比直接對整個方法加鎖,這種方式的性能會更好,因為size()方法沒有加鎖,可以并發執行,隻有在必要的add()方法進行同步,這也展現了鎖的精确性原則。
13. 小結
通過這幾個案例,加深了對volatile和AtomicInteger的了解,主要體會到:
- volatile可以保證可見性和有序性,但不能保證原子性,要用synchronized或Atomic類補充。
- AtomicInteger可以完全替代volatile,并且性能更好,是原子操作的最佳選擇。
- 合理使用volatile和鎖可以實作較高性能的線程安全程式。鎖的使用要遵循精确性原則,不要過度使用。
- volatile和AtomicInteger都是JMM的重要組成部分,了解它們的實作原理有助于使用它們。
14. 案例:使用AtomicStampedReference實作ABA問題的解決
ABA問題是這樣的:如果一個變量V初次讀取的值是A,它的值被改成了B,後來又被改回為A,那些個依賴于V沒有發生改變的線程就會産生錯誤的依賴。
這個問題通常發生在使用CAS操作的并發環境中,我們可以使用版本号的方式來解決這個問題,每次變量更新的時候版本号加1,那麼A->B->A這個過程就會被檢測出來。
AtomicStampedReference就是用過這個原理來解決ABA問題的,它包含一個值和一個版本号,我們可以這樣使用:
AtomicStampedReference<Integer> atomicRef =
new AtomicStampedReference<>(100, 0);
// 擷取目前值和版本号
int stamp = atomicRef.getStamp();
int value = atomicRef.getReference();
// 嘗試設定新值和版本号
boolean success = atomicRef.compareAndSet(value, 101, stamp, stamp + 1);
if(success) {
// 設定成功,擷取新版本号
stamp = atomicRef.getStamp();
}
複制代碼
這裡當我們重新設定值100的時候,由于版本号已經變了,是以compareAndSet會失敗,ABA問題就被解決了。
AtomicStampedReference是JUC包中用來解決ABA問題的重要工具類,實際項目中也廣泛使用,它利用版本号的方式巧妙解決了這個并發程式設計中容易産生的問題。
另外,AtomicStampedReference的版本号使用的是int類型,是以在高并發場景下也可能存在循環的問題,這個時候可以使用時間戳方式生成版本号來避免,不過一般情況下AtomicStampedReference已經可以很好解決ABA問題。
15. 總結
OK,到這裡volatile相關内容就全部介紹完了,包括:
- volatile的定義及作用:可見性、有序性和禁止優化。
- volatile的底層實作原理:JMM、緩存一緻性協定和記憶體屏障。
- volatile的使用執行個體:雙重檢查鎖定和中斷機制等。
- 如何解決volatile的原子性問題:使用synchronized和Atomic類。
- AtomicStampedReference用法和ABA問題解決。
- 一些volatile的最佳實踐。
- 使用volatile和鎖實作的一個簡單線程安全容器。
講解的内容比較廣泛,試着結合理論和實踐的方式進行解釋,希望可以對大家了解volatile和并發程式設計有所幫助。這也是我學久而久之總結的一些心得體會,與大家共同分享學習。如果 對volatile和JMM還有哪些不了解的地方,也歡迎留言讨論,我們共同進步!再次感謝閱讀這篇部落格,也希望您能夠在學習和工作中很好地應用volatile關鍵字!
原文連結:https://juejin.cn/post/7228601938459787325