【JUC并发编程】Volatile关键字底层原理(上)(Volatile的特性/ CPU多核硬件架构剖析/ JMM内存模型/ JMM八大同步规范/ Volatile的底层实现原理)

古城微笑少年丶 2023-09-29 04:52 125阅读 0赞

目录

    1. 什么是 Volatile
    1. Volatile的特性
    1. Volatile的用法
    1. CPU多核硬件架构剖析
    1. CPU的摩尔定律
    1. JMM内存模型
    • 6.1 主内存
    • 6.2 工作内存
    1. JMM八大同步规范
    1. Volatile的底层实现原理
    • 8.1 Java汇编指令查看
    • 8.2 MESI协议实现的原理
    • 8.3 为什么Volatile不能保证原子性
    • 8.4 为什么System.out.println 保证线程的可见性

1. 什么是 Volatile

volatile是Java提供的轻量级的同步机制,保证了可见性,不保证原子性 禁止重排序。
Java 语言包含两种内在的同步机制:同步块(或方法)和 volatile 变量,相比于synchronized(synchronized通常称为重量级锁),volatile更轻量级,因为它不会引起线程上下文的切换和调度。但是volatile 变量的同步性较差(有时它更简单并且开销更低),而且其使用也更容易出错。
volatile能解决多个cpu中高速缓存(工作内存)数据的一致性问题

2. Volatile的特性

可见性
可见性,是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的

顺序性
程序执行程序按照代码的先后顺序执行。

原子性
原子是世界上的最小单位,具有不可分割性。

3. Volatile的用法

  1. public class Demo extends Thread {
  2. /**
  3. * lock 锁 汇编的指令 强制修改值,立马刷新主内存中 另外线程立马可见刷新主内存数据
  4. */
  5. private static volatile boolean FLAG = true;
  6. @Override
  7. public void run() {
  8. while (FLAG) {
  9. }
  10. }
  11. public static void main(String[] args) throws InterruptedException {
  12. new Demo().start();
  13. Thread.sleep(1000);
  14. FLAG = false;
  15. }
  16. }

4. CPU多核硬件架构剖析

CPU每次从主内存读取数据比较慢,而现代的CPU通常涉及多级缓存,CPU读主内存
按照空间局部性原则加载。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

5. CPU的摩尔定律

https://baike.baidu.com/item/%E6%91%A9%E5%B0%94%E5%AE%9A%E5%BE%8B/350634?fr=aladdin
基本每隔18个月,可能CPU的性能会提高一倍。

6. JMM内存模型

Java内存模型定义的是一种抽象的概念,定义屏蔽java程序对不同的操作系统的内存访问差异。

6.1 主内存

存放我们共享变量的数据

6.2 工作内存

每个CPU对共享变量(主内存)的副本。

7. JMM八大同步规范

在这里插入图片描述

1.read(读取):作用于 主内存的变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用;从主内存读取数据
2.load(载入):作用于 工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中; 将主内存读取到的数据写入工作内存中
3.use(使用):作用于 工作内存的变量,把工作内存中的一个变量值传递给执行引擎;从工作内存读取数据来计算

  1. assign(赋值):作用于 工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量;将计算好的值重新赋值到工作内存中
    5.store(存储):作用于 工作内存的变量,把工作内存中的一个变量的值传送到 主内存中,以便随后的write的操作;将工作内存数据写入主内存
    6.write(写入):作用于 工作内存的变量,它把store操作从工作内存中的一个变量的值传送到 主内存的变量中; 将store过去的变量值赋值给主内存中的变量
    7.lock(锁定):作用于 主内存的变量,把一个变量标记为一条线程独占状态; 将主内存变量加锁,标识位线程独占状态。
  2. unlock(解锁):作用于 主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定;将主内存变量解锁,解锁后其他线程可以锁定该变量

8. Volatile的底层实现原理

通过汇编lock前缀指令触发底层锁的机制
锁的机制两种:总线锁/MESI缓存一致性协议
主要帮助我们解决多个不同cpu之间缓存之间数据同步

什么是总线:
cpu和内存进行交互就得通过总线

总线锁
1.最初实现就是通过总线加锁的方式也就是上面的lock与unlock操作,但是这种方式存在很大的弊端。会将我们的并行转换为串行,从而失去了多线程的意义
2.当一个cpu(线程)访问到我们主内存中的数据时候,往总线总发出一个Lock锁的信号,其他的线程不能够对该主内存做任何操作,变为阻塞状态。该模式,存在非常大的缺陷,就是将并行的程序,变为串行,没有真正发挥出cpu多核的好处。

Intel- 64 与 IA-32架构软件开发者手册对lock指令的解释:

  1. 底层实现主要通过汇编lock前缀指令,它会锁定这块内存区域的缓存(缓存行锁定)并回写到主内存。
  2. 会将当前处理器缓存行的数据立即写回系统主内存;
  3. 这个写回主内存的操作会引起在其他CPU里缓存了该内存地址的数据无效(MESI协议)

8.1 Java汇编指令查看

首先需要将该工具 hsdis-amd64.dll

E:\java8\jdk\jre\bin\server 放入 hsdis-amd64.dll

  1. -server -Xcomp -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:CompileCommand=compileonly,*Demo01.*

注意:修改类的名称(Demo01为类的名称)

  1. 0x0000000002ae6277: lock addl $0x0,(%rsp) ;*putstatic FLAG
  2. ; - com.demo.Demo01::main@17 (line 24)

8.2 MESI协议实现的原理

总线:维护解决cpu高速缓存副本数据之间一致性问题。

  1. E 独享状态:在单核的情况下 ,只有一个线程 当前线程工作内存(主内存中副本数据)与主内存数据保持一致的则 当前cpu的状态:E 独享状态。
  2. S 表示为共享 在多个cpu的情况下 每个线程中工作内存中(主内存中副本数据)与主内存数据保持一致性,则当前cpu的状态是为:S 共享状态。
  3. M 修改 当前线程线程修改了工作内存中的数据,当前cpu的副本数据与主内存数据不一致性的情况下,则当前cpu的状态为M状态。
  4. I 无效 总线嗅探机制 如果发现cpu中副本数据与主内存数据不一致的情况下,则会认为无效需要从新刷新主内存中的数据到工作内存中。

8.3 为什么Volatile不能保证原子性

Volatile 可以保证可见性(保证每个cpu中的高速缓存数据一致性问题)
但是不能够保证数据原子性
禁止重排序

  1. public class VolatileAtomThread extends Thread {
  2. private static volatile int count;
  3. public static void create() {
  4. count++;
  5. }
  6. public static void main(String[] args) {
  7. ArrayList<Thread> threads = new ArrayList<>();
  8. for (int i = 0; i < 10; i++) {
  9. Thread tempThread = new Thread(() -> {
  10. for (int j = 0; j < 1000; j++) {
  11. create();
  12. }
  13. });
  14. threads.add(tempThread);
  15. tempThread.start();
  16. }
  17. threads.forEach(thread -> {
  18. try {
  19. thread.join();
  20. } catch (InterruptedException e) {
  21. e.printStackTrace();
  22. }
  23. });
  24. System.out.println(count);
  25. }
  26. }

Volatile为了能够保证数据的可见性,但是不能够保证原子性,及时的将工作内存的数据刷新主内存中,导致其他的工作内存的数据变为无效状态,其他工作内存做的count++操作等于就是无效丢失了,这是为什么我们加上Volatile count结果在小于10000以内。

8.4 为什么System.out.println 保证线程的可见性

println底层代码中使用了Synchronized, Synchronized可以保证线程可见性和线程安全性

1、线程解锁前,必须把共享变量的最新值刷新到主内存中;
2、线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量是需要从主内存中重新读取最新的值(加锁与解锁需要统一把锁)

发表评论

表情:
评论列表 (有 0 条评论,125人围观)

还没有评论,来说两句吧...

相关阅读