美文网首页
volatile使用详解

volatile使用详解

作者: 文景大大 | 来源:发表于2020-04-25 17:40 被阅读0次

    一、为什么要使用volatile

    我们假设一个场景,主线程启动一个子线程后,子线程一直运行着,直到主线程发出指令,让子线程停止。

    @Slf4j
    public class Test001 {
        public static void main(String[] args) throws InterruptedException {
            Runnable runnable = new MyThread();
            Thread thread1 = new Thread(runnable);
            thread1.start();
            Thread.sleep(3000);
            ((MyThread) runnable).switchRunningFlag();
            log.info("{}切换了runningFlag", Thread.currentThread().getName());
        }
    }
    
    @Slf4j
    public class MyThread implements Runnable {
        private boolean runningFlag = true;
        
        public void switchRunningFlag() {
            runningFlag = false;
        }
    
        @Override
        public void run() {
            while (runningFlag) {
                log.info("{}正在运行,当前runningFlag为{}", Thread.currentThread().getName(), runningFlag);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    log.info("sleep发生了异常:{}", e);
                    break;
                }
            }
            log.info("{}已经停止了运行,当前runningFlag为{}", Thread.currentThread().getName(), runningFlag);
        }
    }
    

    在这个例子中,变量runningFlag没有被声明为volatile,那么它就会被拷贝到主线程和子线程各自的工作内存中,主线程在修改了该变量值后,理论上子线程并不能及时读到该变量最新的值,而是从自己的工作线程中读取原来的值,因此,子线程并不能及时地停止运行。

    然而在实际的jdk1.8环境中,我们并没有看到期望的场景,子线程非常及时地停止了运行,是理论出错了吗?

    并不是,在jdk1.2及以前的时代,我们期望的场景是可以重现的,并且在给变量加上volatile后,问题确实能得到解决。然而在后续的jdk版本中,直至现在使用的jdk1.8环境中,jvm已经做了很多的优化,现在只有jvm认为当前线程需要非常频繁地读取非volatile变量的时候,才会从线程的工作内存中去加载变量的值,否则,和使用volatile的效果是一样的。即普通变量的多线程可见性问题已经不是那么地严重了。

    那jvm认为什么样子的频率是较为频繁的呢?在这里,我们将while中的内容全部注释,只保留一个空循环体,再次运行,那么就能看到期望的效果了,即程序陷入死循环,得不到结束。当我们使用volatile修饰变量runningFlag时,程序就不会陷入死循环,可以及时结束运行。

    在现代的并发编程中,为了保证变量的可见性,已经不再推荐使用volatile了,不光因为有更好的替代方式,还因为它极其容易出错。

    但是关于它的知识点,还是学习并发编程时不可绕过的话题。

    二、volatile与synchronized的比较

    • synchronized是基于锁的同步机制,它既具有原子性,也具有可见性。原子性体现在它锁定的代码同一时间只能有一个线程执行;可见性体现在它锁定的代码中如果对变量进行了修改,在释放锁之前,会把修改的值及时同步给其它的线程。而volatile仅仅具有可见性,不具有原子性。可见性体现为单个线程对变量进行修改后,jvm会强制让其它线程读取该变量时,从主内存中读取最新的值,而非读取工作内存中的值;不具有原子性体现为,volatile不能保证像i++a<b这种多步骤操作的原子性,这个会在第四节详细说明。
    • volatile是对synchronized的轻量级实现,它只能用于修饰变量,不能像synchronized一样可以修饰方法和代码块;
    • volatile在理论上性能要比synchronized好,但是随着jvm的优化,这点优势已经不是很明显了;
    • volatile不会像synchronized一样导致线程的阻塞;
    • volatile的目的是为了解决变量在多线程间的可见性,synchronized的目的是为了解决资源在多线程间的同步访问;
    • volatile和synchronized都可以禁止指令重排;

    四、volatile的非原子性

    我们在一开始的例子中,while中读取变量是个单步骤操作,因此不存在非原子性带来的线程安全问题,但是如果换成一个多步骤操作,就会出现线程安全的问题:

    @Slf4j
    public class Test001 {
        public static void main(String[] args) throws InterruptedException {
            for (int i = 0; i < 100; i++) {
                Thread thread = new Thread(new MyThread());
                thread.start();
            }
            // 等待所有线程执行完毕
            Thread.sleep(3000);
            log.info("共享变量counter的最终结果为:{}",MyThread.counter);
        }
    }
    
    @Slf4j
    public class MyThread implements Runnable {
        public volatile static int counter = 0;
    
        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                counter++;
            }
        }
    }
    

    在这个例子中,count++是一个多步骤操作,多线程环境下,极易产生脏读的非线程安全问题,原本预期结果是打印10000,结果总是小于这个值。

    倘若我们给count++加上同步代码块,就解决了非线程安全的问题,实现了这一多步骤操作的原子性,执行结果总是10000。

    @Slf4j
    public class MyThread implements Runnable {
        // 此时变量是否使用volatile都一样的,其原子性和可见性由synchronized保证
        public volatile static int counter = 0;
    
        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                // 多线程实例,需要锁类,不能锁对象
                synchronized (MyThread.class) {
                    counter++;
                }
            }
        }
    }
    

    五、volatile的替代方案

    除了使用synchronized来代替volatile之外,我们还可以使用原子类,原子类可以在没有锁的情况下,实现自身操作的原子性,从而保证线程安全。

    @Slf4j
    public class MyThread implements Runnable {
        public static AtomicInteger counter = new AtomicInteger(0);
    
        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                counter.incrementAndGet();
            }
        }
    }
    

    参考文献

    相关文章

      网友评论

          本文标题:volatile使用详解

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