Java并发机制的底层实现之volatile关键字

1. volatile关键字

1.1. volatile的作用

volatile具有可见性、有序性,其保证不了原子性。下面我们对其可见性和原子性进行说明,有序性后面再说。

原子性:volatile修饰的变量,其运算并不具备原子性,对其进行多线程操作时,是不安全的。

java 复制代码
import java.util.concurrent.TimeUnit;

class Number {
    volatile int number = 0;
}

public class AtomicTest01 {
    public static void main(String[] args) throws InterruptedException {
        Number myNumber = new Number();

        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    myNumber.number++;
                }
            }).start();
        }

        TimeUnit.SECONDS.sleep(5);
        System.out.println(myNumber.number);
    }

}

这段代码的运行结果并不总是10000,经常会是小于10000的结果。因为number++运算,要经历多条CPU指令,如加载number的原始值到寄存器、为原始值加1、将运算结果写会CPU缓存,在多线程的情况下,线程A可能执行到为原始值加1这条指令了,线程B可能正好在执行加载number的原始值到寄存器,这样线程B加载的就是线程A还没有进行加1的值,这样最终的计算结果就会比预想的值要小。

综上可以看出,基础类型被volatile关键字修饰后,其运算也是不具备原子性的。

可见性:出现可见性问题的原因是现在进入CPU多核时代,另外为了弥补CPU与内存之间的性能差异,在每个CPU核心都设置了多级缓存,CPU在代码的过程中,会将对一些变量的修改,先放到CPU缓存中,并不立即同步到主内存。这对于代码中的局部变量还好,在每个线程中的局部变量是互不影响的。对于共享变量,就会存在比较大问题,线程A对共享变量的修改一开始只会将修改结果同步到CPU缓存中,没有写回主内存,这样线程B在读取这个共享变量的时候,就会读取到以前的旧值。

这种情况下,可以使用volatile关键字对共享变量进行修饰,这样对共享变量进行修改时,就会立即同步到主内存中,另外其他线程在读取volatile关键字修饰的共享变量时,也会主动去主内存中去查找最新的值。

java 复制代码
import java.util.concurrent.TimeUnit;

public class VisibleTest01 {

    volatile static boolean flag = true;

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = Thread.ofVirtual().name("t1_thread").start(() -> {
            while (flag) {
                for (int i = 0; i < 1000; i++) {
                    i = i + 1;
                }
            }

            System.out.println(Thread.currentThread().getName() + " stop runnuing");
        });

        TimeUnit.MILLISECONDS.sleep(2000);

        Thread t2 = Thread.ofVirtual().name("t2_thread").start(() -> {
            flag = false;
        });

        t1.join();
        t2.join();
    }

}

上面代码中,flag如果不使用volatile修饰的话,t1线程将长时间执行while循环,因为它一直捕获不到flag已经被设置为true了。

像System.out.println()(该方法中有Lock和synchronized代码块,进入这些代码块的时候,会刷新线程的CPU缓存区域)、Thread.sleep(n)等语句,很多都有类似volatile的可见性效果,如果while循环中有类似语句,也达不到上面代码的演示效果,即使flag不用volatile修饰,t1线程也不会长时间在while循环中。

相关推荐
青云交6 分钟前
Java 大视界 -- Java 大数据机器学习模型在金融市场波动预测与资产配置动态调整中的应用(355)
java·大数据·机器学习·lstm·金融市场·波动预测·资产配置
徐子童15 分钟前
初识Redis---Redis的特性介绍
java·数据库·redis
Dubhehug27 分钟前
6.String、StringBuffer、StringBuilder区别及使用场景
java·面试题·stringbuilder·string·stringbuffer
枣伊吕波1 小时前
第十八节:第七部分:java高级:注解的应用场景:模拟junit框架
java·数据库·junit
白鲸开源1 小时前
从批到流,Zoom 基于 DolphinScheduler 的流批统一调度系统演进
java·大数据·开源
白鲸开源1 小时前
二次开发必看!DolphinScheduler 3.1.9 开发环境搭建指南
java·大数据·开源
悟能不能悟1 小时前
java和ptyhon对比
java·开发语言
秋也凉1 小时前
有关Maven的个人笔记总结
java·笔记·maven
jstart千语2 小时前
【Spring AI】Advisors API—顾问(即拦截器)
java·人工智能·spring·ai
一碗绿豆汤2 小时前
JAVA+AI教程-第三天
java·spring