浅谈volatile

volatile有三个特性:

(1)可见性

(2)不保证原子性

(3)禁止指令重排

下面我们一一介绍

(一)可见性

volatile的可见性是说共享变量只要修改,就可以被其他线程感知到,这个是怎么做到的呢?

这得从虚拟机的内存模型说起,这里直接引用《深入理解java虚拟机》书中原话:JAVA内存模型规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存保存了被该线程使用的变量的主内存副本,线程对变量的所有操作(读取、赋值)都必须在工作内存中进行,而不是直接读写主内存中的数据。

所以对于多线程场景的共享变量值的传递是需要通过主内存进行。也就是线程A对共享变量tmp(没有使用volatile)修改后,线程B不一定能马上感知到。需要等线程A中的工作内存同步到主内存中,线程B才能感知到。而volatile则会保证变量只要修改就会马上同步到主内存中,并且线程读取变量时也是会先从主内存中刷新变量值,以此来保证了变量的可见性。

(二)不保证原子性

对于volatile变量,虽然线程能马上看见它的值改变,并不能保证它的运算操作是原子的,比如:

复制代码
public class TestVolatile {
    public static volatile int count = 0;
    public static CountDownLatch countDownLatch = new CountDownLatch(5);

    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                for (int j = 0; j < 10000; j++) {
                    count++;
                }
                countDownLatch.countDown();
            }).start();
        }
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("count:" + count);
    }
}

上面这个例子执行后,如果是原子性的,那么count的值应该是50000。但实际远小于50000

因为i++本身就不是原子步骤,它实际上是i=i+1.它需要先获取i的值,然后再加1,然后再赋值给i。两个线程A,B同时获取i的值为3,然后线程A将其加1后的值4赋值给主内存。线程B这时也完成了计算同样赋值给主内存也是4。这是计算结果就少了1。

(3)禁止指令重排

指令重排是编译器在运行时在保证单个线程内结果不变的情况下,基于性能优化考虑对指令进行重新排序

这个先举个例子:

复制代码
public static boolean initFlag = false;

    public static void main(String[] args) {
        new Thread(() -> {
            // 初始化完成后再设置标志位,另一个线程感知到标志位改变后,开始执行它的业务
            doSomeInit();
            initFlag = true;
        }).start();
        new Thread(() -> {
            while (!initFlag) {
                // 还没初始化则等待
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            // 认为初始化已经完成,执行它的业务
            
        }).start();

    }

上面这段代码并不能实际运行出我们想要的效果(因为指令重排是虚拟机编译器内部逻辑,没想到一个确实可行的例子能够触发指令重排),只是简单代替我们经常可能会出现的业务场景,一个线程等待另一个线程完成初始化逻辑后,才开始启动自己的业务逻辑。上面initFlag没有标识volatile。那么initFlag赋值操作可能会因为指令重排,先于doSomeInit初始化逻辑先完成,这样就可能导致我们的业务功能有问题。因为对于单个线程来说,虚拟机认为initFlag提前执行并没有改变所在线程的结果。所以这里需要给initFlag加上volatile标识,告诉虚拟机,这里不能进行指令重排,在initFlag操作之前的代码,也必须先于initFlag操作之前执行

相关推荐
程序员清风8 分钟前
快手二面:乐观锁是怎么用它来处理多线程问题的?
java·后端·面试
一匹电信狗10 分钟前
【LeetCode_160】相交链表
c语言·开发语言·数据结构·c++·算法·leetcode·stl
曦樂~22 分钟前
【Qt】信号与槽(Signal and Slot)- 简易计算器
开发语言·数据库·qt
一线大码23 分钟前
SpringBoot 优雅实现接口的多实现类方式
java·spring boot·后端
花伤情犹在28 分钟前
Java Stream 高级应用:优雅地扁平化(FlatMap)递归树形结构数据
java·stream·function·flatmap
歪歪10031 分钟前
React Native开发Android&IOS流程完整指南
android·开发语言·前端·react native·ios·前端框架
yaoxin52112340 分钟前
212. Java 函数式编程风格 - Java 编程风格转换:命令式 vs 函数式(以循环为例)
java·开发语言
wangqiaowq41 分钟前
ImmutableList.of() 是 Google Guava 库 提供的一个静态工厂方法,用于创建一个不可变的(immutable)列表。
开发语言·windows·python
摇滚侠1 小时前
Spring Boot 3零基础教程,WEB 开发 Thymeleaf 属性优先级 行内写法 变量选择 笔记42
java·spring boot·笔记
滑水滑成滑头1 小时前
**发散创新:多智能体系统的探索与实践**随着人工智能技术的飞速发展,多智能体系统作为当今研究的热点领域,正受到越来越多关注
java·网络·人工智能·python