从底层看线程关键字

简介: 从底层看线程关键字

1、JVM 与 happen-before

1.1、为什么会存在“内存可见性”问题

      Store Buffer  和 Load Buffer 的CPU缓存体系

     L1、L2、L3和主内存之间是同步的,有缓存一致性协议的保证,但是Store Buffer、Load Buffer 和 L1之间却是异步的。也就是说,往内存中写入一个变量,这个变量会保存在Store Buffer 里面,稍后才会异步地写入L1中,同时同步 写入主内存中。

               操作系统内核视角下的CPU缓存模型

多个CPU,多个CPU多核,每个核上面可能还有多个硬件线程,对于操作系统来讲,就相当于一个个逻辑CPU。每个逻辑CPU都要自己的缓存,这些缓存和主内存之间不是完全同步的

                                     JVM 抽象内存模型

     对应到Java 里,就是JVM抽象内存模型


1.2、重排序与内存可见性的问题

重排序分类:

  • 编译器重排序:对于没有先后依赖关系的语句,编译器可以重新调整语句的执行顺序
  • cpu指令重排序:指令级别,让没有依赖关系的多条指令并行

  • cpu内存重排序:cpu有自己的缓存,指令的执行顺序和写入主内存的顺序不完全一致


cpu 内存重排序是造成“内存可见性”问题的主因

例:假设有两个线程,线程 1 执行 X = 1 命令 和 a = Y 命令,线程 2 执行

Y = 1 命令和 b = X 命令。最后a,b的结果应该是什么?

因为 线程 1 和 线程 2 的执行顺序不确定, 所以结果可能是

  • a=0,b=1

  • a=1,b=0
  • a=1,b=1

正常就这三种可能性, 但实际还可能是 a = 0,b = 0, 为什么呢?

原因是 线程 1 先执行 X = 1 后执行 a = Y,但此时 X = 1 还在自己的Store Buffer 里,但在线程 2 看来, a=Y 和 X = 1 顺序却是颠倒的。指令没有重排序,写入内存的操作被延迟了,也就是内存被重排序了,这就造成内存可见性问题。


1.3 as-if-serial 语义

   对于开发者而言,希望不要有任何的重排序, 指令执行顺序和代码顺序严格一致,写内存的顺序也严格和代码顺序一致。

对于编译器和CPU,希望尽最大可能进行重排序,提升运行效率。

   单线程程序的重排序规则:只要操作之间没有依赖性,编译器和CPU就可以任意重排序,因为执行结果不会改变。这就是as-if-serial 语义。

   多线程程序的重排序规则:编译器和CPU只能保证每个线程的as-if-serial语义。线程之间的数据依赖和线程影响, 需要编译器和CPU的上层来决定。


1.4  happen-before 是什么

     如果A happen-before B,意味着 A 的执行结果必须对 B 可见, 也就是保证跨线程的内存可见性。A happen-before B 不代表 A 一定在 B 之前执行。基于 happen-before 这种描述方法,JMM对开发者做出了一些承诺:

  • 单线程中的每个操作,happen-before 对应线程中任意后续操作

  • 对volatile 变量的写入,happen-before对应后续对这个变量的读取

  • 对synchronized的解锁,happen-before对应后续对这个锁的加锁


对于非volatile变量的写入和读取,不在这个承诺之列。通俗来讲,就是jvm对编译器和cpu来说,volatile遍历不能重排序;非volatile变量可以任意重排序。

1.5  happen-before 的传递性

     volatile、synchronized 都具有happen-before 语义

1.6  C++中的volatile 关键字

     在Java中的volatile关键字不仅具有内存可见性,还会禁止volatile变量写入和非volatile变量写入的重排序;C++中的volatile 关键字不会禁止这种重排序。

1.7  内存屏障

      为了禁止编译器重排序和CPU重排序,在编译器和CPU层面上都有对应的指令。编译器的内存屏障,只是为了告诉编译器不要对指令进行重排序。当编译完成之后,这种内存屏障就消失了。CPU内存屏障是CPU提供的指令,可以由开发者显示调用。

1.7.1  Liunx中的内存屏障

   通过函数smp_wmb()插入了一个Store Barrier屏障,从而确保了:

  • 更新指针的操作,不会被重排序到修改数据之前。
  • 更改指针的时候,Store Cache 被刷新,其他CPU可见

1.7.2  jdk 中的内存屏障

   jdk8 开始,Java 在 Unsafe类中提供了三个内存屏障函数(不是最基本的内存屏障):

public final class Unsafe{
 public native void loadFence();  //loadFence=LoadLoad+LoadStore
    public native void storeFence(); //storeFence=StoreFence+LoadStore
    public native void fullFence();  //fullFence=LoadFence+StoreFence+StoreLoad
}

在理论层面,可以把基本的CPU内存屏障分为4种:

  • 1.LoadLoad:禁止读和读的重排序
  • 2.StoreStore:禁止写和写的重排序
  • 3.LoadStore:禁止读和写的重排序
  • 4.StoreLoad:禁止写和读的重排序


1.7.3  volatile 实现原理

    实现volatile 关键字的语义参考做法:

    1、在volatile 写操作的前面插入一个StoreStore屏障。保证volatile写操作不会和之前的写操作重排序。

     2、在volatile 写操作的后面插入一个StoreLoad屏障。保证volatile写操作不会和之后的读操作重排序。

     3、在volatile读操作的后面插入一个LoadLoad屏障 + LoadStore屏障。保证volatile读操作不会和之后的读操作、写操作重排序。


1.8  final 关键字

1.8.1  构造函数溢出问题

      对于构造函数溢出,就是一个对象的构造并不是“原子的”,当一个线程正在构造对象时,另外一个线程却可以读到未构造好的“一半对象”。

1.8.2  final 的 happen-before含义

     解决构造函数溢出的方法:

     1.给变量都加上volatile关键字

     2.为read/write函数都加上synchronized关键字

     3.给变量加上final关键字

   

final 的 happen-before 语义

1.对final域的写(构造函数内部),happen-before对于后续对final域所在对象的读

2.对final域所在的对象的读,happen-before 以后续对final域的读

 通过这种happen-before 语义的限定,保证了final域的赋值,一定在构造函数之前完成,不会出现另外一个线程读取到了对象,但对象里面的变量却还没有初始化的情形,避免出现构造函数溢出的问题。

1.8.3  happen-before 规则总结

      1.单线程中每个操作,happen-before  以该线程中任意后续操作

      2.对volatile变量的写,happen-before 以后续对这个变量的读

      3.对synchronized的解锁,happen-before 以后续对这个变量的加锁

      4.对final变量的写,happen-before 于final域对象的读, happen-before 于后续对final 变量的读


四个基本规则再加上happen-before的传递性,就构成JVM对开发者的整个承诺

                                   从底向上看 volatile 背后的原理

目录
相关文章
|
设计模式 安全 Java
Java并发编程实战:使用synchronized关键字实现线程安全
Java并发编程实战:使用synchronized关键字实现线程安全
165 0
|
存储 SQL 缓存
揭秘Java并发核心:深度剖析Java内存模型(JMM)与Volatile关键字的魔法底层,让你的多线程应用无懈可击
【8月更文挑战第4天】Java内存模型(JMM)是Java并发的核心,定义了多线程环境中变量的访问规则,确保原子性、可见性和有序性。JMM区分了主内存与工作内存,以提高性能但可能引入可见性问题。Volatile关键字确保变量的可见性和有序性,其作用于读写操作中插入内存屏障,避免缓存一致性问题。例如,在DCL单例模式中使用Volatile确保实例化过程的可见性。Volatile依赖内存屏障和缓存一致性协议,但不保证原子性,需与其他同步机制配合使用以构建安全的并发程序。
195 0
|
缓存 安全 算法
Java面试题:如何通过JVM参数调整GC行为以优化应用性能?如何使用synchronized和volatile关键字解决并发问题?如何使用ConcurrentHashMap实现线程安全的缓存?
Java面试题:如何通过JVM参数调整GC行为以优化应用性能?如何使用synchronized和volatile关键字解决并发问题?如何使用ConcurrentHashMap实现线程安全的缓存?
148 0
|
10月前
|
Java 开发者
在Java多线程编程的世界里,Lock接口正逐渐成为高手们的首选,取代了传统的synchronized关键字
在Java多线程编程的世界里,Lock接口正逐渐成为高手们的首选,取代了传统的synchronized关键字
113 4
|
11月前
|
Java 开发者
在 Java 多线程编程中,Lock 接口正逐渐取代传统的 `synchronized` 关键字,成为高手们的首选
【10月更文挑战第6天】在 Java 多线程编程中,Lock 接口正逐渐取代传统的 `synchronized` 关键字,成为高手们的首选。相比 `synchronized`,Lock 提供了更灵活强大的线程同步机制,包括可中断等待、超时等待、重入锁及读写锁等高级特性,极大提升了多线程应用的性能和可靠性。通过示例对比,可以看出 Lock 接口通过 `lock()` 和 `unlock()` 明确管理锁的获取和释放,避免死锁风险,并支持公平锁选择和条件变量,使其在高并发场景下更具优势。掌握 Lock 接口将助力开发者构建更高效、可靠的多线程应用。
75 2
|
11月前
|
缓存 Java 编译器
【多线程-从零开始-伍】volatile关键字和内存可见性问题
【多线程-从零开始-伍】volatile关键字和内存可见性问题
133 0
|
存储 安全 Java
解锁Java并发编程奥秘:深入剖析Synchronized关键字的同步机制与实现原理,让多线程安全如磐石般稳固!
【8月更文挑战第4天】Java并发编程中,Synchronized关键字是确保多线程环境下数据一致性与线程安全的基础机制。它可通过修饰实例方法、静态方法或代码块来控制对共享资源的独占访问。Synchronized基于Java对象头中的监视器锁实现,通过MonitorEnter/MonitorExit指令管理锁的获取与释放。示例展示了如何使用Synchronized修饰方法以实现线程间的同步,避免数据竞争。掌握其原理对编写高效安全的多线程程序极为关键。
202 1
|
算法 Java API
多线程线程池问题之synchronized关键字在Java中的使用方法和底层实现,如何解决
多线程线程池问题之synchronized关键字在Java中的使用方法和底层实现,如何解决
|
设计模式 缓存 安全
Java面试题:工厂模式与内存泄漏防范?线程安全与volatile关键字的适用性?并发集合与线程池管理问题
Java面试题:工厂模式与内存泄漏防范?线程安全与volatile关键字的适用性?并发集合与线程池管理问题
137 1
|
缓存 Java
【多线程面试题二十三】、 说说你对读写锁的了解volatile关键字有什么用?
这篇文章讨论了Java中的`volatile`关键字,解释了它如何保证变量的可见性和禁止指令重排,以及它不能保证复合操作的原子性。