首頁  >  文章  >  Java  >  Java中的Volatile關鍵字能否保證執行緒安全?

Java中的Volatile關鍵字能否保證執行緒安全?

王林
王林轉載
2023-04-21 20:07:062842瀏覽

volatile

volatile 是 Java 中的一個相對來說比較重要的關鍵字,主要是用來修飾會被不同執行緒存取和修改的變數。

而這個變數只能保證兩個特性,一個是保證有序性,另外一個則是保證可見性。

那麼什麼是有序性,什麼又是可見性呢?

有序性

那麼什麼是有序性呢?

其實程式執行的順序是依照程式碼的先後順序執行,禁止進行指令重新排序。

看似理所當然,其實不是這樣,指令重排序是JVM為了優化指令,提高程式運作效率,在不影響單執行緒程式執行結果的前提下,盡可能地提高並行度。

但是在多執行緒環境下,有些程式碼的順序改變,有可能引發邏輯上的不正確。

而 volatile 就是因為有這個特性,所以才被大家熟知的。

volatile 又是如何保證有序性的呢?

有很多小夥伴就說,網路上說的是 volatile 可以禁止指令指令重排序,這就保證了程式碼的程式會嚴格按照程式碼的先後順序執行。這就保證了有序性。被 volatile 修飾的變數的操作,會嚴格按照程式碼順序執行,就是說當程式碼執行到 volatile 修飾的變數時,其前面的程式碼一定執行完畢,後面的程式碼一定沒有執行。

如果這時候,面試官不再繼續深挖下去的話,那麼恭喜你,可能這個問題已經回答完了,但是如果面試官繼續往下深挖,為什麼會禁止指令重排,什麼又是指令重排呢?

在從原始碼到指令的執行,一般是分成了三種重排,如圖:                             

Java中的Volatile關鍵字能否保證執行緒安全?



Java中的Volatile關鍵字能否保證執行緒安全?

Java中的Volatile關鍵字能否保證執行緒安全?

#'接下來就得看看volatile 是如何禁止指令重排的。

我們直接用程式碼來驗證:

public class ReSortDemo {

int a = 0;
boolean flag = false;

public void mehtod1(){
a = 1;
flag = true;
}

public void method2(){
if(flag){
a = a +1;
System.out.println("最后的值: "+a);
}
}
}

如果有人看到這段程式碼,一定會說,那這段程式碼出來的結果會是什麼呢?

有些人說是2,是的, 如果你只是單線程調用,那結果就是2,但是如果是多線程調用的時候,最後的輸出結果不一定是我們想像到的2,這時就要把兩個變數都設定為volatile。

如果大家對單例模式了解比較多的話,肯定也是關注過這個 volatile,為什麼呢?

大家看看如下程式碼:

###
class Singleton {
// 不是一个原子性操作
//private static Singleton instance;
//改进,Volatile 可以保持可见性,不能保证原子性,由于内存屏障,可以保证避免指令重排的现象产生!
private static volatile Singleton instance;

// 构造器私有化
private Singleton() {
}

// 提供一个静态的公有方法,加入双重检查代码,解决线程安全问题, 同时解决懒加载问题,同时保证了效率, 推荐使用
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
######上面的單例模式大家熟悉麼? #########是的,這就是**雙重檢查(DCL 懶漢式) **#######有人會說,因為有指令重新排序的存在,雙端檢索機制也不一定是線程安全的呀,對呀,所以用到了synchronized 關鍵字,讓他變成了線程安全的了。 ######可見性######其實可見性就是,在多執行緒環境中,對共享變數的修改對於其他執行緒是否立即可見的問題。 ######那麼他的可見性一般都會表現在什麼地方呢?用在什麼地方呢? ######其實一般用這個變量,很多都是為了保證他的可見性,就比如定義的一個全局變量,在其中有個循環來判斷這個變量的值,有一個線程修改了這個參數的時候,這個循環會停止,跳到之後去執行。 #########我們來看看沒有使用volatile修飾程式碼實作:#####
public class Test {

private static boolean flag = false;

public static void main(String[] args) throws Exception{
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程A开始执行:");
for (;;){
if (flag){
System.out.println("跳出循环");
break;
}
}
}
}).start();
Thread.sleep(100);

new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程B开始执行");
flag = true;
System.out.println("标识已经变更");
}
}).start();
}
}
###結果大家一定是可想而知,#########運行結果肯定是:############線程A開始執行:###線程B開始執行###標識已經變更#########確實,就是這樣的。 ###############如果我們用 volatile 呢,那麼這個程式碼的執行結果就會不一樣呢? #########我們來試試看:######
public class Test {

private static volatile boolean flag = false;

public static void main(String[] args) throws Exception{
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程A开始执行:");
for (;;){
if (flag){
System.out.println("跳出循环");
break;
}
}
}
}).start();
Thread.sleep(100);

new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程B开始执行");
flag = true;
System.out.println("标识已经变更");
}
}).start();
}
###這樣我們就能看到另外一個執行結果,在迴圈當中的輸出語句是可以被執行的。 ###############也就是說,在線程B 中,我們去修改這個被修飾的變量,那麼最終,在線程A中,就能順利讀取到我們的數據訊息了。 ######是否能夠保證原子性######不能,我們來看一點程式碼,被volatile修飾的變量,###
public class Test {

// volatile不保证原子性
// 原子性:保证数据一致性、完整性
volatile int number = 0;

public void addPlusPlus() {
number++;
}

public static void main(String[] args) {
Test volatileAtomDemo = new Test();
for (int j = 0; j < 20; j++) {
new Thread(() -> {
for (int i = 0; i < 1000; i++) {
volatileAtomDemo.addPlusPlus();
}
}, String.valueOf(j)).start();
}// 后台默认两个线程:一个是main线程,一个是gc线程
while (Thread.activeCount() > 2) {
Thread.yield();
}
// 如果volatile保证原子性的话,最终的结果应该是20000 // 但是每次程序执行结果都不等于20000
System.out.println(Thread.currentThread().getName() +
" final number result = " + volatileAtomDemo.number);
}
}
######如果能夠保原子性,那麼最終的結果應該是20000,但每次的最終結果並不能保證就是20000,例如:############main final number result = 17114###main final number result = 20000### final number result = 19317#########三次執行,都是不同的結果,######為什麼會出現這種?這就跟number 有關係了###

number 被拆分成3個指令

  • 執行GETFIELD拿到主記憶體中的原始值number

  • 執行IADD進行加1操作

  • 執行PUTFIELD把工作記憶體中的值寫回主記憶體中

當多個執行緒並發執行PUTFIELD指令的時候,會出現寫回主記憶體覆蓋問題,所以才會導致最終結果不為20000,所以volatile 不能保證原子性。

以上是Java中的Volatile關鍵字能否保證執行緒安全?的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:yisu.com。如有侵權,請聯絡admin@php.cn刪除