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

相关推荐
Haven-1 小时前
Java-面试八股文-JVM篇
java·jvm·面试
我真的是大笨蛋1 小时前
JVM调优总结
java·jvm·数据库·redis·缓存·性能优化·系统架构
wjs0401 小时前
Git常用的命令
java·git·gitlab
superlls2 小时前
(算法 哈希表)【LeetCode 349】两个数组的交集 思路笔记自留
java·数据结构·算法
田里的水稻2 小时前
C++_队列编码实例,从末端添加对象,同时把头部的对象剔除掉,中的队列长度为设置长度NUM_OBJ
java·c++·算法
ponnylv2 小时前
深入剖析Spring Boot启动流程
java·开发语言·spring boot·spring
前行的小黑炭3 小时前
Android 协程的使用:结合一个环境噪音检查功能的例子来玩玩
android·java·kotlin
李少兄3 小时前
解决IntelliJ IDEA 提交代码时无复选框问题
java·ide·intellij-idea
cyforkk3 小时前
Spring Boot @RestController 注解详解
java·spring boot·后端
叫我阿柒啊4 小时前
从Java全栈到前端框架:一次真实面试的深度复盘
java·spring boot·typescript·vue·database·testing·microservices