【多线程基础】volatile关键字作用及其底层(浅涉及操作系统)

volatile其主要功能是保持线程可见性和防止指令重排序

保持线程可见性

其底层使用MESI协议,当值发生改变时,将其写入主内存中,并使其他CPU的缓存失效
具体案例:

    public static void main(String[] args) {
        //线程进入sleep,让第三个线程缓存volatile的值
        new Thread(() -> {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            //当前缓存修改为true
            flag = true;
            System.out.println(flag);
        }).start();
        new Thread(() -> {
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            //由于t2线程晚于t1线程执行,所以读到的是t1的缓存
            System.out.println(flag);
        }).start();
        new Thread(() -> {
            int i = 0;
            while (!flag) {
                //由于最早执行,flag缓存false且缓存修改未被通知,导致一直循环
                i++;
            }
        }).start();
    }
}

防止指令重排序

在多线程环境中,Java 虚拟机和处理器为了提高性能,可能会对代码指令进行重排序(指令重排),以提高执行效率。重排序不会影响单线程程序的执行结果,但在多线程环境中,如果对共享变量的访问顺序发生了变化,就可能导致数据不一致的问题。

其底层使用内存屏障来防止指令重排序
Java 对 volatile 变量的读写会在底层生成以下两种内存屏障:

读屏障(Load Barrier):在读取 volatile 变量之前,会插入一个读屏障,确保在读取 volatile 变量时,之前对该变量的所有写操作已经完成(保证可见性)。
写屏障(Store Barrier):在写入 volatile 变量之后,会插入一个写屏障,确保在对 volatile 变量进行写操作后,该操作不会被重排序到其他写操作之后。
具体规则:
volatile 读操作前,会插入一个读屏障,保证该操作不会被重排序到之前的操作之后,确保之前的操作已经对所有线程可见。
volatile 写操作后,会插入一个写屏障,确保该操作不会被重排序到之后的操作之前,保证该操作对于其他线程可见。

volatile是否为原子性的?

否!
原子性指的是一个操作不可被打断,要么完全执行,要么完全不执行

像 i++ 这样的复合操作实际上包括了以下几个步骤:

  1. 从内存中读取变量 i 的值。
  2. 在 i 的值上加 1。
  3. 将结果写回内存。

尽管 volatile 能保证其他线程可以看到最新的 i 值,但是由于 i++ 是一个复合操作,这三个步骤不是原子的。可能发生的情况是,多个线程同时读取到相同的 i 值,分别执行加 1 操作,并将相同的结果写回内存,导致加法结果丢失,产生线程安全问题。

怎么保证原子性?

  • 使用Lock接口下的锁,如ReentranLock
  • 使用关键字sychronized
  • 使用原子类AtomicInteger等
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值