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

相关推荐
ayt0074 分钟前
Netty 4.2核心类解析:SingleThreadIoEventLoop的设计哲学与实现
java·网络
无名-CODING6 分钟前
Java 爬虫进阶:动态网页、多线程与 WebMagic 框架实战
java·爬虫·okhttp
weixin_704266058 分钟前
Spring 注解驱动开发与 Spring Boot 核心知识点梳理
java·spring boot·spring
开开心心就好8 分钟前
伪装文件历史记录!修改时间的黑科技软件
java·前端·科技·r语言·edge·pdf·语音识别
8Qi811 分钟前
Redis哨兵模式(Sentinel)深度解析
java·数据库·redis·分布式·缓存·sentinel
wangchunting18 分钟前
数据结构-树
java·数据结构
无籽西瓜a21 分钟前
【西瓜带你学设计模式 | 第五期 - 建造者模式】建造者模式 —— 产品构建实现、优缺点与适用场景及模式区别
java·后端·设计模式·软件工程·建造者模式
wzl2026121324 分钟前
《基于企微会话存档的精准发送策略:从互动数据分析到防折叠群发》
java·数据分析·企业微信
xhuiting1 小时前
MySQL专题总结(四)—— 高可用
java·数据库·mysql
不吃蘑菇!1 小时前
LeetCode Hot 100-1(两数之和)
java·数据结构·算法·leetcode·哈希表