美文网首页
volatile关键字小记

volatile关键字小记

作者: sortinnauto | 来源:发表于2018-08-03 17:07 被阅读0次

volatile义为“挥发性的;不稳定的;爆炸性的;反复无常的”,可以看出用它来修饰的变量似乎存在一些不确定性,事实是这样吗?我们来看看它的作用。


内存可见性

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

线程在工作时,需要将主内存中的数据拷贝到工作内存中,这样一来,对这些数据的操作就是基于工作内存的,并不直接操作主内存或其他线程工作内存中的数据,操作完成之后将工作内存中的数据再刷新到主内存中去。

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

这样操作造成的一个问题是,在并发运行时,线程B从主内存中拿到的数据可能是“旧数据”——线程A已经将主内存中的数据拷贝并且修改了,只是还没有刷新进主内存中。这样数据不同步,显然是会出现问题的。因此volatile就派上用场了。

volatile修饰的变量,任何线程对它的写操作都会立即刷新进主内存中,并且强制让缓存了此变量的线程中的数据情况,必须重新从主内存中读取最新的数据。

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

内存可见性的应用

当两个线程必须依据主内存通信时,通信的变量必须用volatile关键字修饰。

Example

package volatiletest;

import java.util.Scanner;

public class VolatileTest implements Runnable {

    private volatile Boolean flag = true;  //flag被volatile修饰

    @Override
    public void run() {
        while (flag) {
        }
        System.out.println(Thread.currentThread().getName() + " 执行完毕...");
    }

    private void stopRun() {
        flag = false;
    }

    public static void main(String[] args) {
        VolatileTest volatileTest = new VolatileTest();
        new Thread(volatileTest, "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() {
                        volatileTest.stopRun();
                    }
                }).start();
                break;
            }
        }
        System.out.println("main 线程执行完毕...");
    }
}

输出结果:

main 线程正在运行...
1
main 线程执行完毕...
Thread A 执行完毕...

主线程在修改了flag变量之后,Thread A线程内的数据被清空,并重新从主内存中拿到被主线程main改变的flagThread A立即线程停止运行。

而如果flag没有被volatile关键字修饰的话,这个过程并不会立即完成。
输出结果:

main 线程正在运行...
1
main 线程执行完毕...
(等待中...)


其实被volatile修饰会给人一个错觉,或者说疑问:那这个变量在并发操作中是不是就是线程安全的呢?

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

Example

package volatiletest;


public class VolatileNotThreadSafe implements Runnable {
    private volatile int count = 0;

    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            count++;
        }
        System.out.println(Thread.currentThread().getName() + "更新count为: " + count);
    }

    public static void main(String[] args) {
        VolatileNotThreadSafe test = new VolatileNotThreadSafe();
        Thread A = new Thread(test, "Thread A");
        Thread B = new Thread(test, "Thread B");

        A.start();
        B.start();

        for (int i = 0; i < 10000; i++) {
            count++;
        }
        System.out.println("count最终为: " + count);
    }
}

输出结果为:

Thread A更新count为: 16257
count最终为: 17899
Thread B更新count为: 16264

count最终为: 16054
Thread A更新count为: 15845
Thread B更新count为: 16382

...

每次结果都是不同的,但是相同点是在三个线程ABmain中都给count自增10000后,count最终的结果都没有达到30000。而count是被volatile修饰的。

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

所以想到达到线程安全可以:

  • 使这三个线程串行执行(其实就是单线程,没有发挥多线程的优势)。

  • 也可以使用 synchronized或者是锁的方式来保证原子性。

  • 还可以用 Atomic 包中 AtomicInteger 来替换int,它利用了CAS 算法来保证了原子性。

下面用第三种方法在原代码上修改后如下:

package volatiletest;

import java.util.concurrent.atomic.AtomicInteger;

public class VolatileNotThreadSafe implements Runnable {
    private volatile AtomicInteger count = new AtomicInteger(0);
    
    @Override
    public void run() {

        for (int i = 0; i < 10000; i++) {
           
            //Atomically increments by one the current value.
            count.getAndIncrement();
        }
        
        System.out.println(Thread.currentThread().getName() + "更新count为: " + count);
    }


    public static void main(String[] args) {
        VolatileNotThreadSafe test = new VolatileNotThreadSafe();
        Thread A = new Thread(test, "Thread A");
        Thread B = new Thread(test, "Thread B");

        A.start();
        B.start();

        for (int i = 0; i < 10000; i++) {
            count.getAndIncrement();
        }

        System.out.println("count最终为: " + count);

    }
}

输出结果为:

Thread A更新count为: 30000
Thread B更新count为: 30000
count最终为: 30000


总结

volatile 在 Java 并发中用的很多,比如像Atomic 包中的 value、以及 AbstractQueuedLongSynchronizer 中的 state都是被定义为volatile来用于保证内存可见性。将这里理解透彻对我们编写并发程序时可以提供很大帮助。

相关文章

网友评论

      本文标题:volatile关键字小记

      本文链接:https://www.haomeiwen.com/subject/aqcuvftx.html