JAVA并發編程:volatile關鍵字

本文主要為記錄和整理為主,在文章最低下會附上原文鏈接。
把我遇到的知識點和問題梳理出來。

1.JAVA并發編程中的三個概念

1.原子性
2.可見性
3.有序性

原子性

原子性:即一個操作或者多個操作 要么全部執行并且執行的過程不會被任何因素打斷,要么就都不執行。

在Java中,對基本數據類型的變量的讀取和賦值操作是原子性操作,即這些操作是不可被中斷的,要么執行,要么不執行。

x = 10;         //語句1
y = x;         //語句2
x++;           //語句3
x = x + 1;     //語句4

這四個語句只有語句1是原子性的
其他三句都需要先讀取X變量的值,然后進行其他操作

那么在讀取X變量值后都有可能發生阻塞,這時就破壞了原子性。

也就是說,只有簡單的讀取、賦值(而且必須是將數字賦值給某個變量,變量之間的相互賦值不是原子操作)才是原子操作。

不過這里有一點需要注意:在32位平臺下,對64位數據的讀取和賦值是需要通過兩個操作來完成的,不能保證其原子性。但是好像在最新的JDK中,JVM已經保證對64位數據的讀取和賦值也是原子性操作了。

保證原子性的方法:synchronize和Lock關鍵字 利用同步鎖,保證一次只能一個線程對變量進行操作。

可見性

對于可見性,Java提供了volatile關鍵字來保證可見性。

當一個共享變量被volatile修飾時,它會保證修改的值會立即被更新到主存,當有其他線程需要讀取時,它會去內存中讀取新值。

而普通的共享變量不能保證可見性,因為普通共享變量被修改之后,什么時候被寫入主存是不確定的,當其他線程去讀取時,此時內存中可能還是原來的舊值,因此無法保證可見性。

另外,通過synchronized和Lock也能夠保證可見性,synchronized和Lock能保證同一時刻只有一個線程獲取鎖然后執行同步代碼,并且在釋放鎖之前會將對變量的修改刷新到主存當中。因此可以保證可見性。

有序性

在Java里面,可以通過volatile關鍵字來保證一定的“有序性”(具體原理在下一節講述)。另外可以通過synchronized和Lock來保證有序性,很顯然,synchronized和Lock保證每個時刻是有一個線程執行同步代碼,相當于是讓線程順序執行同步代碼,自然就保證了有序性。

下面就來具體介紹下happens-before原則(先行發生原則):

程序次序規則:一個線程內,按照代碼順序,書寫在前面的操作先行發生于書寫在后面的操作
鎖定規則:一個unLock操作先行發生于后面對同一個鎖額lock操作
volatile變量規則:對一個變量的寫操作先行發生于后面對這個變量的讀操作
傳遞規則:如果操作A先行發生于操作B,而操作B又先行發生于操作C,則可以得出操作A先行發生于操作C

下面我們來解釋一下前4條規則:

對于程序次序規則來說,我的理解就是一段程序代碼的執行在單個線程中看起來是有序的。注意,雖然這條規則中提到“書寫在前面的操作先行發生于書寫在后面的操作”,這個應該是程序看起來執行的順序是按照代碼順序執行的,因為虛擬機可能會對程序代碼進行指令重排序。雖然進行重排序,但是最終執行的結果是與程序順序執行的結果一致的,它只會對不存在數據依賴性的指令進行重排序。因此,在單個線程中,程序執行看起來是有序執行的,這一點要注意理解。事實上,這個規則是用來保證程序在單線程中執行結果的正確性,但無法保證程序在多線程中執行的正確性。

第二條規則也比較容易理解,也就是說無論在單線程中還是多線程中,同一個鎖如果出于被鎖定的狀態,那么必須先對鎖進行了釋放操作,后面才能繼續進行lock操作。

第三條規則是一條比較重要的規則,也是后文將要重點講述的內容。直觀地解釋就是,如果一個線程先去寫一個變量,然后一個線程去進行讀取,那么寫入操作肯定會先行發生于讀操作。

第四條規則實際上就是體現happens-before原則具備傳遞性。

JAVA內存模型圖(JMM)

JMM內存模型

Volatile關鍵字的兩層意思

一旦一個共享變量(類的成員變量、類的靜態成員變量)被volatile修飾之后,那么就具備了兩層語義:

1)保證了不同線程對這個變量進行操作時的可見性,即一個線程修改了某個變量的值,這新值對其他線程來說是立即可見的。

2)禁止進行指令重排序。

Volatile關鍵字 能保證原子性、可見性、有序性嗎?為什么?舉例子說明

Volatile關鍵字不能保證原子性,可以保證可見性,能保證部分的有序性。

比如在多線程環境下對變量i進行自增操作,假設初始時i的值為0,那么操作后i可能為1.

這里有2種方式去理解:
1.首先A線程讀取變量i,值為0 然后發生阻塞。此時線程B讀取i的值也為0 然后自增操作。i=1 把i的最新值1更新到本地共享變量的副本,然后再刷新到主內存中去。然后此時再回到A線程,A線程這個時候也對自己的0值進行加1操作,然后更新回副本刷新到主存中去。此時主存中i=1。這里關鍵的一個點就是,當線程B進行寫操作后,會使得其他線程的緩存行失效,然后其他線程就會去主存中讀取最新的值,這個沒錯。但是 線程A在一開始的時候已經把值0從緩存行入棧到自己的棧頂了(底層的指令集的操作),也就不需要再去讀取緩存行所以緩存行的失效對線程A沒有作用。

2.首先A線程讀取變量i,值為0 然后發生阻塞。此時線程B讀取i的值也為0,然后進行自增操作值為1.然后在進行更新變量副本之前,線程B阻塞。然后回到線程A,A也進行自增然后把最新值1更新到變量副本刷新回到主內存中去。此時回到線程B,線程B繼續更新變量副本然后把值刷新到主內存中去還是1.

保證可見性是對的,因為當volatile關鍵字修飾的變量被寫操作之后。就會對緩存行失效,其他的線程再次讀取都會使用到最新的值,保證了可見性。

為什么說是部分的有序性呢?
因為
1)當程序執行到volatile變量的讀操作或者寫操作時,在其前面的操作的更改肯定全部已經進行,且結果已經對后面的操作可見;在其后面的操作肯定還沒有進行;

2)在進行指令優化時,不能將在對volatile變量訪問的語句放在其后面執行,也不能把volatile變量后面的語句放到其前面執行。

//x、y為非volatile變量
//flag為volatile變量
 
x = 2;        //語句1
y = 0;        //語句2
flag = true;  //語句3
x = 4;         //語句4
y = -1;       //語句5

由于flag變量為volatile變量,那么在進行指令重排序的過程的時候,不會將語句3放到語句1、語句2前面,也不會講語句3放到語句4、語句5后面。但是要注意語句1和語句2的順序、語句4和語句5的順序是不作任何保證的。

并且volatile關鍵字能保證,執行到語句3時,語句1和語句2必定是執行完畢了的,且語句1和語句2的執行結果對語句3、語句4、語句5是可見的。

volatile關鍵字的一些使用場景

使用volatile必須具備以下2個條件:

1)對變量的寫操作不依賴于當前值

2)該變量沒有包含在具有其他變量的不變式中

實際上,這些條件表明,可以被寫入 volatile 變量的這些有效值獨立于任何程序的狀態,包括變量的當前狀態。

事實上,我的理解就是上面的2個條件需要保證操作是原子性操作,才能保證使用volatile關鍵字的程序在并發時能夠正確執行。

1.標記狀態量

volatile boolean flag = false;
 
while(!flag){
    doSomething();
}
 
public void setFlag() {
    flag = true;
}

保證了執行到inited賦值為true時,Context已經初始化完成,線程2再使用的時候就不會出現錯誤

volatile boolean inited = false;
//線程1:
context = loadContext();  
inited = true;            
 
//線程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);

2.單例模式double check

為什么要加volatile關鍵字,就是為了保證instance的初始化完成之后才會被使用,以免報錯。如果不使用,可能會出現,線程A先new了一個對象 分配了內存地址,但是初始化對象的工作沒有完成。此時線程B進來,instance不為空。線程B持有instance然后使用的時候報錯。

class Singleton{
    private volatile static Singleton instance = null;
     
    private Singleton() {
         
    }
     
    public static Singleton getInstance() {
        if(instance==null) {
            synchronized (Singleton.class) {
                if(instance==null)
                    instance = new Singleton();
            }
        }
        return instance;
    }
}

自增操作保證原子性的方法有哪些?

用synchronize關鍵字

public synchronized void increase() {
        inc++;
    }

用lock

public class Test {
    public  int inc = 0;
    Lock lock = new ReentrantLock();
    
    public  void increase() {
        lock.lock();
        try {
            inc++;
        } finally{
            lock.unlock();
        }
    }
}

用原子操作類

public class Test {
    public  AtomicInteger inc = new AtomicInteger();
     
    public  void increase() {
        inc.getAndIncrement();
    }
}

總結一下synchronize lock volatile 和原子性 可見性 有序性的關系

synchronize和lock能保證可見性的原因是,在釋放鎖之前會將對變量的修改刷新到主存當中。

原文參考鏈接:
Java并發編程:volatile關鍵字解析

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容