volatile

### 1.1内存可见性

由于`Java`内存模型(`JMM`)规定,所有的变量都存放在主内存中,而每个线程都有着自己的工作内存(高速内存)。

线程在工作时,需要将主内存中的数据拷贝到工作内存中。这样对数据的任何操作都是基于工作内存(效率提高),并且不能直接操作主内存以及其他线程工作内存中的数据,之后再将更新之后的数据刷新到主内存中。

> 这里所提到的主内存可以简单的认为是**堆内存**,而工作内存则可以认为是**栈内存**。

如图所示:

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

所以在并发运行时可能会出现线程B所读取到的数据是线程A更新之前的数据。

显然这肯定是会出问题的,因此`volatile`的作用出现了:

> 当一个变量被`volatile`修饰时,任何线程对它的写操作都会立即刷新到主内存中,并且会强制让缓存了该变量的线程中的数据清空,必须从主内存重新读取最新数据。

`volatile`修饰之后并不是让线程直接从主内存中获取数据,依然需要将变量拷贝到工作内存中。

### 1.2内存可见性的应用

当我们需要在两个线程间依据主内存通信时,通信的那个变量就必须用`volatile`来修饰:

```java

public class Volatile implements Runnable{

    private static volatile boolean flag = true ;

    @Override

    public void run() {

        while (flag){

        }

        System.out.println(Thread.currentThread().getName() +"执行完毕");

    }

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

        Volatile aVolatile = new Volatile();

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

        System.out.println("main 线程正在运行") ;

        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 ;

    }

}

```

主线程在修改了标志位使得线程A立即停止,如果没有用`volatile`修饰,就有可能出现延迟。

但这里有个无趣,这样的使用方式容易给人的**感觉**是:~~对`volatile`修饰的变量进行并发操作是线程安全的~~。

这里要重点强调,**`volatile`并不能保证线程安全**!

如下程序:

```java

public class VolatileInc implements Runnable{

    private static volatile int count = 0 ; //使用 volatile 修饰基本数据内存不能保证原子性

    //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);

    }

}

```

当我们三个线程(t1,t2,main)同时对一个`int`进行累加时会发现最终的值都会小于30000。

> 这是因为虽然`volatile`保证了内存可见性,每个线程拿到的值都是最新值,但`count++`这个操作并不是原子的,这里面涉及`获取值`、`自增`、`赋值`的操作并不能同时完成。

所以想达到线程安全:

- 使这三个线程串行执行(单线程,并没有发挥多线程的优势)。

- 使用`synchronized`或者锁的方式来保证原子性。

- `Atomic`包中`AtomicInteger`来替换`int`,它利用了`CAS`指令来保证了原子性。

### 1.3指令重排

内存可见只是`volatile`的其中一个语义,它还可以防止`JVM`进行指令重排优化。

```java

int a=10 ;//1

int b=20 ;//2

int c= a+b ;//3

```

理想情况下它的执行顺序是:`1 > 2 > 3`。但是有一种可能经过JVM优化之后代码顺序变为`2 > 1 > 3`。

可以发现不管JVM怎么优化,前提都是保证单线程中最终结果不变的情况。这里可能还看不出什么问题。看下一段伪代码。

```java

private static Map<String,String> value ;

private static volatile boolean flag = fasle ;

//以下方法发生在线程 A 中 初始化 Map

public void initMap(){

    //耗时操作

    value = getMapValue() ;//1

    flag = true ;//2

}

//发生在线程 B中 等到 Map 初始化成功进行其他操作

public void doSomeThing(){

    while(!flag){

        sleep() ;

    }

    //dosomething

    doSomeThing(value);

}

```

这里就能看出问题了,当`flag`没有被`volatile`修饰时,`JVM`对1和2进行重排,导致`value`都还没有被初始化就有可能被线程B使用了。所以加上`volatile`之后可以防止这样的重排优化,保证业务的正确性。

**指令重排的应用经典场景(双重懒加载单例)**

```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`关键字主要为了防止指令重排。

如果不用,`singleton = new Singleton();`,这段代码其实分为三步:

1. 分配内存空间。

2. 初始化对象。

3. 将`singleton`对象指向分配的内存地址。

加上`volatile`是为了保证以上操作顺序执行,反之有可能第二步在第三步之间被执行就有可能某个线程拿到的单例对象是还没初始化的,以至于报错。

### 总结

`volatile`在Java并发中用的很多,比如像`Atomic`包中的`value`、以及`AbstractQueuedLongSynchronizer` 中的`state`都是被定义为`volatile`来用于保证内存可见性。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容