天天看點

java多線程volatile_java多線程——volatile

這是java多線程第三篇:

上一篇《java多線程—記憶體模型》已經講解了java線程中三特征以及happens-before 原則,這一篇主要講解一下volatile的原理以及應用,想必看完這一篇之後,你會對volatile的應用原理以及使用邊界會有更深刻的認知。本篇主要内容:

volatile 讀寫同步原理

volatile重排序原則

volatile應用

關鍵字volatile是jvm提供的輕量級的同步機制,但它并不容易了解,而且在多數情況下用不到,被多數開發者抛棄并采用synchronized代替,synchronized屬于重度鎖,如果你對性能有高的要求,那麼同等情況下,變量聲明volatile會減小更少的同步開銷。

在介紹之前,我們先抛出2個問題:

1、volatile究竟是如何保證共享變量的同步的?

2、i++操作為何對虛拟機來說不是原子操作?

一、volatile 讀寫同步原理

對變量進行volatile聲明以後,會有以下特征:

1、可見性。  保證此變量對所有線程是可見的。

2、原子性 。隻對任意單個volatile變量的讀/寫具有原子性(注意不是所有)。

3、有序性。被volatile聲明過的變量會禁止指令重排序優化

happen-before 保證可見性

volatile變量的寫-讀可以實作線程之間的通信。happens-before是java記憶體模型向我們提供的記憶體可見性保證,這也就是我們第一個問題的解答,volatiel如何保證對共享變量同步的。

我們先回憶一下happens-before原則(我們隻說和其相關的):

程式次序法則:如果在程式中,所有動作 A 出現在動作 B 之前,則線程中的每動作 A 都 happens-before 于該線程中的每一個動作 B。

Volatile 變量法則:對 Volatile 域的寫入操作 happens-before 于每個後續對同一 Volatile 的讀操作。

傳遞性:如果 A happens-before 于 B,且 B happens-before C,則 A happens-before C。

我們通過一個示例來說明這些規則的應用:

public class VolatileTest {

private int a =0;

private volatile int b=0;

public void write(){

a = 1;          //1

b = 2;          //2

}

public void read(){

int i = b;          //3

int j = a;          //4

}

}

比如現在有線程A和B,分别調取write和read方法。

第一種情況:

線程A先執行write方法之後,線程B執行read方法。那麼:

1、基于程式次序法則。1 happens-before 2; 3 happens-before 4

2、基于volatile原則。2 happens-before 3;

3、基于傳遞性原則。因為 1 happens-before 2,2 happens-before 3,3 happens-before 4。那麼可以推斷出 1 happens-before 4,2 happens-before 4。

此種情況下,我們可以認定此時線程B中可以讀取到 線程A中寫入的 a和b的值的。(a值沒用聲明volatile依然可以讀取到,這個為何我們後面講)

第二種情況:

線程B先執行read方法,之後線程A執行write方法。

1、基于程式次序法則。3 happens-before 4; 1 happens-before 2

2、基于volatile原則。無;

3、基于傳遞性原則。無傳遞;

此種情況下,我們可以此時認定線程B中沒有讀取到線程A中寫入的a和b的值。

通過上面的分析我們可以對volatiel變量如此定義:

當write一個volatile變量時,JMM會把該線程對應的本地記憶體中的共享變量重新整理到主記憶體。

當read一個volatile變量時,JMM會把該線程對應的本地記憶體置為無效。線程接下來将從主記憶體中讀取共享變量。

對于第一種情況,我們看上述示例如何write和read的:

java多線程volatile_java多線程——volatile

那麼讀到這裡,有一個困惑:上述變量a并沒有聲明為volatile ,為何能被重新整理到主記憶體中,難道不會被處理器重排序麼?

二、volatile限制重排序

上述中我們講到volatile 中有一個特性,有序性,防止jvm對其重排序,那麼究竟是如何做的,我們看一下。

重排序分為編譯器重排序和處理器重排序。為了實作volatile記憶體語義,jvm會分别限制這兩種類型的重排序類型。

編譯器重排序

針對編譯器制定的volatile重排序規則:

第一個操作

第二個操作

普通讀/寫

volatile讀

volatile寫

普通讀/寫

NO

volatile讀

NO

NO

NO

volatile寫

NO

NO

上述表中,NO表示jvm不可以重排序,保持目前順序。

比如第一行第三列中表示:第一個操作是變量的普通讀寫,第二個操作是volatile聲明的變量寫操作,那麼此時對于操作1和操作2是不可以重排序的,保持目前順序。

就好比上述示例中a 和b變量,滿足此種情況,a和b的操作順序不變。

上述規則用文字描述:

當第二個操作是volatile寫時,不管第一個操作是什麼,都不能重排序。這個規則確定volatile寫之前的操作不會被編譯器重排序到volatile寫之後。

當第一個操作是volatile讀時,不管第二個操作是什麼,都不能重排序。這個規則確定volatile讀之後的操作不會被編譯器重排序到volatile讀之前。

當第一個操作是volatile寫,第二個操作是volatile讀時,不能重排序。

注意,jvm隻保證2個操作保持如此規則,不能延伸到2個以上的操作上。

處理器重排序

為了實作上述規則,jvm編譯器在生成位元組碼的時候,會在指令序列中插入記憶體屏障來禁止特定類型的處理器重排序。

在每個volatile寫操作的前面插入一個StoreStore屏障。

在每個volatile寫操作的後面插入一個StoreLoad屏障。

在每個volatile讀操作的前面插入一個LoadLoad屏障。

在每個volatile讀操作的後面插入一個LoadStore屏障。

如此可以保證在任意處理器平台,任意的程式中都能得到正确的volatile重排序規則實作。

總結

volatile防止重排序,有什麼作用?

happens-before是java記憶體模型向我們提供的記憶體可見性保證;而volatile的禁止重排序規則,包括volatile的編譯器重排序規則和volatile的記憶體屏障插入政策,是jvm用來實作happens-before的方式。

比如上述程式中,根據happens-before的程式順序規則:1 happens-before 2 ;3 happens-before 4.

而後根據volatile規則:2 happens-before 3. 如此操作 1、2、3、4的順序得以延續。

也就是說volatile的禁止重排序規則,確定上述happens-before順序。

三、應用

i++ 不是原子

上述原理介紹中,我們有說volatile隻對隻對任意單個volatile變量的讀/寫具有原子性,比如變量a的指派操作,可以為原子的,但變量a++不為原子的,我們看個示例:

public class Test {

private volatile  int count;

public void increCount(){

count++;

}

public void setCount(int count ){

this.count=count;

}

}

我們用javap 看下increCount的編譯指令:

java多線程volatile_java多線程——volatile

我看紅色圈中的部分,increCount被分解了4個指令來操作,而 setCount隻有1個指令來處理(原子的)。我們用代碼的方式,increCount方法可以等價于以下:

public void increCount(){

//        count++;

int tmp =getCount();

tmp=tmp+1;

setCount(tmp);

}

是以說volatile隻對隻對任意單個volatile變量的讀/寫具有原子性,而i++實際上它是一個由讀取-修改-寫入操作序列組成的組合操作,屬于多個操作,是以不具備原子性。

volatile 應用原則

要使 volatile 變量提供理想的線程安全,必須同時滿足下面兩個條件:

對變量的寫操作不依賴于目前值。

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

也就是說被寫入 volatile 變量的這些有效值獨立于任何程式的狀态,包括變量的目前狀态。

是以隻有在狀态真正獨立于程式内其他内容時才能使用 volatile —— 這條規則能夠避免将這些模式擴充到不安全的用例。

應用示例

1、指派操作

上述 increCount中屬于依賴目前count值的應用了,而setCount屬于沒有依賴目前值。是以後者屬于線程安全。

2、線程取消

對一個線程取消或者中斷的時候,有人會采用interrupted方法來中斷,如果維護一個volatile變量來為何,無論外部線程如何調用,總能保證對目前線程的立即可見性。

public class  CancleThread implements Runnable{

private volatile  boolean cancle = false;

public void shutdown(){

this.cancle=true;

}

public void run() {

while (!cancle){

//.....doSomeThing

}

}

}

當想終止這個線程的操作的時候,調用shutdown方法會比較安全。

通過以上原理和應用介紹,想必對于volatile不會那麼陌生了,掌握原理,了解使用邊界,讓你的程式性能更高,可讀性更強。我們如果嚴格遵循 volatile 的使用條件 —— 即變量真正獨立于其他變量和自己以前的值 —— 在某些情況下可以使用 volatile 代替 synchronized 來簡化代碼。

-----------------------------------------------------------------------------

想看更多有趣原創的技術文章,掃描關注公衆号。

關注個人成長和遊戲研發,推動國内遊戲社群的成長與進步。

繼續閱讀