volatile使用详解

2020-04-25  本文已影响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的比较

四、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();
        }
    }
}

参考文献

上一篇下一篇

猜你喜欢

热点阅读