volatile

### 1.1內(nèi)存可見(jiàn)性

由于`Java`內(nèi)存模型(`JMM`)規(guī)定,所有的變量都存放在主內(nèi)存中,而每個(gè)線程都有著自己的工作內(nèi)存(高速內(nèi)存)。

線程在工作時(shí),需要將主內(nèi)存中的數(shù)據(jù)拷貝到工作內(nèi)存中。這樣對(duì)數(shù)據(jù)的任何操作都是基于工作內(nèi)存(效率提高),并且不能直接操作主內(nèi)存以及其他線程工作內(nèi)存中的數(shù)據(jù),之后再將更新之后的數(shù)據(jù)刷新到主內(nèi)存中。

> 這里所提到的主內(nèi)存可以簡(jiǎn)單的認(rèn)為是**堆內(nèi)存**,而工作內(nèi)存則可以認(rèn)為是**棧內(nèi)存**。

如圖所示:

![5d31384d22ac511765.jpg](https://upload-images.jianshu.io/upload_images/13202172-7290e86ccf0e3098.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

所以在并發(fā)運(yùn)行時(shí)可能會(huì)出現(xiàn)線程B所讀取到的數(shù)據(jù)是線程A更新之前的數(shù)據(jù)。

顯然這肯定是會(huì)出問(wèn)題的,因此`volatile`的作用出現(xiàn)了:

> 當(dāng)一個(gè)變量被`volatile`修飾時(shí),任何線程對(duì)它的寫操作都會(huì)立即刷新到主內(nèi)存中,并且會(huì)強(qiáng)制讓緩存了該變量的線程中的數(shù)據(jù)清空,必須從主內(nèi)存重新讀取最新數(shù)據(jù)。

`volatile`修飾之后并不是讓線程直接從主內(nèi)存中獲取數(shù)據(jù),依然需要將變量拷貝到工作內(nèi)存中。

### 1.2內(nèi)存可見(jiàn)性的應(yīng)用

當(dāng)我們需要在兩個(gè)線程間依據(jù)主內(nèi)存通信時(shí),通信的那個(gè)變量就必須用`volatile`來(lái)修飾:

```java

public class Volatile implements Runnable{

? ? private static volatile boolean flag = true ;

? ? @Override

? ? public void run() {

? ? ? ? while (flag){

? ? ? ? }

? ? ? ? System.out.println(Thread.currentThread().getName() +"執(zhí)行完畢");

? ? }

? ? public static void main(String[] args) throws InterruptedException {

? ? ? ? Volatile aVolatile = new Volatile();

? ? ? ? new Thread(aVolatile,"thread A").start();

? ? ? ? System.out.println("main 線程正在運(yùn)行") ;

? ? ? ? Scanner sc = new Scanner(System.in);

? ? ? ? while(sc.hasNext()){

? ? ? ? ? ? String value = sc.next();

? ? ? ? ? ? if(value.equals("1")){

? ? ? ? ? ? ? ? new Thread(new Runnable() {

? ? ? ? ? ? ? ? ? ? @Override

? ? ? ? ? ? ? ? ? ? public void run() {

? ? ? ? ? ? ? ? ? ? ? ? aVolatile.stopThread();

? ? ? ? ? ? ? ? ? ? }

? ? ? ? ? ? ? ? }).start();

? ? ? ? ? ? ? ? break ;

? ? ? ? ? ? }

? ? ? ? }

? ? ? ? System.out.println("主線程退出了!");

? ? }

? ? private void stopThread(){

? ? ? ? flag = false ;

? ? }

}

```

主線程在修改了標(biāo)志位使得線程A立即停止,如果沒(méi)有用`volatile`修飾,就有可能出現(xiàn)延遲。

但這里有個(gè)無(wú)趣,這樣的使用方式容易給人的**感覺(jué)**是:~~對(duì)`volatile`修飾的變量進(jìn)行并發(fā)操作是線程安全的~~。

這里要重點(diǎn)強(qiáng)調(diào),**`volatile`并不能保證線程安全**!

如下程序:

```java

public class VolatileInc implements Runnable{

? ? private static volatile int count = 0 ; //使用 volatile 修飾基本數(shù)據(jù)內(nèi)存不能保證原子性

? ? //private static AtomicInteger count = new AtomicInteger() ;

? ? @Override

? ? public void run() {

? ? ? ? for (int i=0;i<10000 ;i++){

? ? ? ? ? ? count ++ ;

? ? ? ? ? ? //count.incrementAndGet() ;

? ? ? ? }

? ? }

? ? public static void main(String[] args) throws InterruptedException {

? ? ? ? VolatileInc volatileInc = new VolatileInc() ;

? ? ? ? Thread t1 = new Thread(volatileInc,"t1") ;

? ? ? ? Thread t2 = new Thread(volatileInc,"t2") ;

? ? ? ? t1.start();

? ? ? ? //t1.join();

? ? ? ? t2.start();

? ? ? ? //t2.join();

? ? ? ? for (int i=0;i<10000 ;i++){

? ? ? ? ? ? count ++ ;

? ? ? ? ? ? //count.incrementAndGet();

? ? ? ? }

? ? ? ? System.out.println("最終Count="+count);

? ? }

}

```

當(dāng)我們?nèi)齻€(gè)線程(t1,t2,main)同時(shí)對(duì)一個(gè)`int`進(jìn)行累加時(shí)會(huì)發(fā)現(xiàn)最終的值都會(huì)小于30000。

> 這是因?yàn)殡m然`volatile`保證了內(nèi)存可見(jiàn)性,每個(gè)線程拿到的值都是最新值,但`count++`這個(gè)操作并不是原子的,這里面涉及`獲取值`、`自增`、`賦值`的操作并不能同時(shí)完成。

所以想達(dá)到線程安全:

- 使這三個(gè)線程串行執(zhí)行(單線程,并沒(méi)有發(fā)揮多線程的優(yōu)勢(shì))。

- 使用`synchronized`或者鎖的方式來(lái)保證原子性。

- `Atomic`包中`AtomicInteger`來(lái)替換`int`,它利用了`CAS`指令來(lái)保證了原子性。

### 1.3指令重排

內(nèi)存可見(jiàn)只是`volatile`的其中一個(gè)語(yǔ)義,它還可以防止`JVM`進(jìn)行指令重排優(yōu)化。

```java

int a=10 ;//1

int b=20 ;//2

int c= a+b ;//3

```

理想情況下它的執(zhí)行順序是:`1 > 2 > 3`。但是有一種可能經(jīng)過(guò)JVM優(yōu)化之后代碼順序變?yōu)閌2 > 1 > 3`。

可以發(fā)現(xiàn)不管JVM怎么優(yōu)化,前提都是保證單線程中最終結(jié)果不變的情況。這里可能還看不出什么問(wèn)題??聪乱欢蝹未a。

```java

private static Map<String,String> value ;

private static volatile boolean flag = fasle ;

//以下方法發(fā)生在線程 A 中 初始化 Map

public void initMap(){

? ? //耗時(shí)操作

? ? value = getMapValue() ;//1

? ? flag = true ;//2

}

//發(fā)生在線程 B中 等到 Map 初始化成功進(jìn)行其他操作

public void doSomeThing(){

? ? while(!flag){

? ? ? ? sleep() ;

? ? }

? ? //dosomething

? ? doSomeThing(value);

}

```

這里就能看出問(wèn)題了,當(dāng)`flag`沒(méi)有被`volatile`修飾時(shí),`JVM`對(duì)1和2進(jìn)行重排,導(dǎo)致`value`都還沒(méi)有被初始化就有可能被線程B使用了。所以加上`volatile`之后可以防止這樣的重排優(yōu)化,保證業(yè)務(wù)的正確性。

**指令重排的應(yīng)用經(jīng)典場(chǎng)景(雙重懶加載單例)**

```java

public class Singleton {

? ? private static volatile Singleton singleton;

? ? private Singleton() {

? ? }

? ? public static Singleton getInstance() {

? ? ? ? if (singleton == null) {

? ? ? ? ? ? synchronized (Singleton.class) {

? ? ? ? ? ? ? ? if (singleton == null) {

? ? ? ? ? ? ? ? ? ? //防止指令重排

? ? ? ? ? ? ? ? ? ? singleton = new Singleton();

? ? ? ? ? ? ? ? }

? ? ? ? ? ? }

? ? ? ? }

? ? ? ? return singleton;

? ? }

}

```

這里的`volatile`關(guān)鍵字主要為了防止指令重排。

如果不用,`singleton = new Singleton();`,這段代碼其實(shí)分為三步:

1. 分配內(nèi)存空間。

2. 初始化對(duì)象。

3. 將`singleton`對(duì)象指向分配的內(nèi)存地址。

加上`volatile`是為了保證以上操作順序執(zhí)行,反之有可能第二步在第三步之間被執(zhí)行就有可能某個(gè)線程拿到的單例對(duì)象是還沒(méi)初始化的,以至于報(bào)錯(cuò)。

### 總結(jié)

`volatile`在Java并發(fā)中用的很多,比如像`Atomic`包中的`value`、以及`AbstractQueuedLongSynchronizer` 中的`state`都是被定義為`volatile`來(lái)用于保證內(nèi)存可見(jiàn)性。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容