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循环中。

相关推荐
San813_LDD1 小时前
[C语言]《Dev-C++ 报错解决手册(Day0607 精华版)》
java·前端·javascript
Anastasiozzzz2 小时前
从有限状态机到智能体图:传统 FSM 与 Agent Graph的演进
java·人工智能·python·ai
wang09079 小时前
自己动手写一个spring之IOC_2
java·后端·spring
来杯@Java9 小时前
学生选课管理系统(基于springboot+vue前后端分离的项目)计算机毕业设计java
java·spring boot·spring·vue·毕业设计·maven·mybatis
不知名的老吴10 小时前
线程的生命周期之线程“插队“
java·开发语言·python
ANnianStriver10 小时前
PetLumina-02-后端开发与前后端联调
java·ai·sa-token
杨了个杨898211 小时前
Keepalived + Nginx + HAProxy 高可用架构部署实战案例
java·nginx·架构
马士兵教育13 小时前
Java还有前景吗?Java+AI大模型学习路线及项目?
java·人工智能·python·学习·机器学习
snow@li13 小时前
Java:理解 Gradle / 后端项目的管家 / 打包SpringBoot 应用 / 完成编译、下载依赖、运行测试、打包 JAR/WAR / 速查表
java