美文网首页
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();
        }
    }
}

参考文献

相关文章

  • Java中的锁

    锁产生的背景 volatile和synchronized Lock接口 ReenTrantLock使用详解 同步实...

  • volatile使用详解

    一、为什么要使用volatile 我们假设一个场景,主线程启动一个子线程后,子线程一直运行着,直到主线程发出指令,...

  • volatile、synchronized、lock详解

    volatile、synchronized、lock详解 1、volatile 被volatile定义的变量被一个...

  • 并发编程系列-volatile详解

    并发编程系列-volatile详解 前言 面试过程中,常见的双层校验锁会引发出去使用volatile关键字这个问题...

  • volatile详解(二)(重排序)

    volatile保证线程安全可见性——volatile详解(一)[https://www.jianshu.com/...

  • CAS详解

    CAS在底层源码中是使用非常广的,像我之前的HashMap源码解析、volatile详解等文章都有提到CAS。本文...

  • volatile详解

    今天来介绍一下volatitle volatile是什么 volatitle是一个确保共享变量能够被准确和一致地更...

  • volatile详解

    volatile可以说是最轻量级的同步工具,但是要使用volatile来保证业务的安全和一致性,我们需要对其...

  • volatile详解

    Java内存模型 想要理解 volatile 为什么能确保可见性,就要先理解Java中的内存模型是什么 样的。 J...

  • volatile详解

    一、volatile简介 在单线程环境中,我们几乎用不到这个关键词,但是多线程环境中,这个关键词随处可见。而且也是...

网友评论

      本文标题:volatile使用详解

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