聊聊高并发(三十六)Java内存模型那些事(四)理解Happens-before规则

在前几篇将Java内存模型的那些事基本上把这个域底层的概念都解释清楚了,聊聊高并发(三十五)Java内存模型那些事(三)理解内存屏障 这篇分析了在X86平台下,volatile,synchronized, CAS操作都是基于Lock前缀的汇编指令来实现的,关于Lock指令有两个要点:

1. lock会锁总线,总线是互斥的,所以lock后面的写操作会写入缓存和内存,可以理解为在lock后面的写缓存和写内存这两个动作称为了一个原子操作。当总线被锁时,其他的CPU是无法使用总线的,也就让其他的读写都等待lock的释放

2. Lock写完后,发起它的CPU的缓存和内存都是最新值,其他CPU相关的缓存行都会invalidate,后续的读/写就会发生缓存不命中,从内存重新加载最新值。

这里有个隐含的点,我没找到具体的资料,但是按照很多资料的说法: volatile的写操作相当于释放锁,volatile的读操作相当于进入锁可以做下面的推断:

volatile操作的是一个变量,而锁保护的程序段中涉及到的变量可以是多个,既然两者的效果是一样的,那么很可能lock后面的写会让高速缓存/写缓存区的所有脏数据都刷新回主存。只有这样volatile在可见性方面和锁保护的程序段的可见性才是行为一致的。

理解这个很重要,因为和这篇讲的Happens-before传递性有关系。Happens-before刚看到的时候从语言上看很难理解,觉得是废话,但是它实际描述的问题其实是可见性的问题,顺带着有一些由于防止重排序而带来的有序性的问题聊聊高并发(三十三)Java内存模型那些事(一)从一致性(Consistency)的角度理解Java内存模型
这篇说了,内存模型是一致性这个问题域里面的,一致性问题只涉及到了可见性和有序性这两种特性,不包含原子性,所以Happens-before实际上是一系列的一致性的约束,所以它涉及到了可见性和有序性的意思,但没有原子性的含义。

happens-before俗解 这篇文章已经写的很清楚了,我这边再结合上一篇内存屏障的一些概念锦上添花一下,进一步说明这个问题

下面这些Happens-before的规则是从JSR 133 (Java Memory Model) FAQ 摘出来的,一条条看

  • Each action in a thread happens before every action in that thread that comes later in the program‘s order.
  • 可以理解为对于单个线程来说,前面的写操作对后面都是可见的,这里肯定有人问那指令重排序之后怎么保证这点呢,我也有这个疑问,所以我理解的是如果这个写是同步的,那么对单线程来说,所有同步的写都是按照program order的,这个也是顺序一致性的第一层含义。要理解的是,Java在使用了同步手段之后,被同步保护的点都是保证顺序一致性的。因为同步的底层实现比如内存屏障 / lock都有防止重排序的含义
  • An unlock on a monitor happens before every subsequent lock on that same monitor.
  • 可以理解为一个锁的释放后它前面的写操作对后续进入同一个锁的线程可见,对锁来说这个太肯定了,释放时会lock cmpxchg一次,进入时会lock cmpxchg一次,两次都保证了可见性
  • A write to a volatile field happens before every subsequent read of that same volatile.
  • 可以理解为volatile的写操作对后续的读可见,也是lock addl操作保证了写volatile的可见性
  • A call to start() on a thread happens before any actions in the started thread.
  • 可以理解为线程start()写线程开始状态对后续线程的其他动作可见,JVM内部处理了,实际实现肯定也是用了lock/内存屏障来实现的,其实在聊聊JVM(九)理解进入safepoint时如何让Java线程全部阻塞 中我们提到了线程状态的改变,在JVM里面是对一个线程状态变量进行原子的修改,这个状态的改变是原子的,并且可见的,当然就具备了Happens-before的能力
  • All actions in a thread happen before any other thread successfully returns from a
    join() on that thread.
  • 可以理解为一个被join的线程中所有的写操作在它join结束后回到原来的线程时,对原来的线程可见。这个和上面的原理差不多,就是JVM在修改线程状态的时候是一次原子操作,并且保证了可见性(估计是一次CAS),所以连带着修改状态前面的修改也都对后续的操作可见了

其他还有一些Happens-before规则,比如CAS操作,原子变量的修改都有Happens-before的含义,另外Happens-before具备传递性,比如 A happens beofre B, B happens before C, 那么A肯定 happens before C。

为什么具备传递性呢,原因还是在开篇的时候说的,lock/内存屏障不仅仅把当前的地址的数据原子的写到缓存和内存,肯定也把这之前CPU缓存/write buffer的脏数据写回到主内存了,这样就实现了Happens before的传递性。

所以所有用到volatile ,synchronized, CAS的地方都具备Happens before的传递性,显式锁和原子变量底层都是基于CAS来实现的,当然用到它们的时候也具备了Happens before的传递性。

所以下面这个例子就很好理解了,比如 y是volatile变量或者是原子变量/同步器类等等用到CAS的

线程A      线程B

x = 1        a = y

y = 2        b = x

如果在时间顺序上y=2这个对被同步的变量的写先发生于 a = y 这个对被同步的变量的读,那么可以肯定的说 b = x = 1。

有人问 x = 1会不会被重排到 y =2 之后,答案是不会,因为y是个被同步的变量,防止重排序, x 不会跨越内存屏障排到y=2之后,所以

b = x同样也不会被重排序到 a = y前面,因为 y是被同步的变量,内存屏障同样不会让屏障后面的操作跨越到前面去

所以只要 y =2 写操作发生在 a = y读操作之前,那么最后 x = 1 肯定先于 b=x,所以 b = 1

参考资料:

happens-before俗解

时间: 2024-10-18 01:43:00

聊聊高并发(三十六)Java内存模型那些事(四)理解Happens-before规则的相关文章

聊聊高并发(十六)实现一个简单的可重入锁

可重入锁指的是如果一个线程已经获得了一个锁,那么它可以多次进入这个锁,当然前提是线程需要先获得这个锁. 可重入锁是最常使用的锁,Java的内置锁就是可重入锁,使用synchronized关键字可以启用内置锁机制,比如说一个类有两个synchronized方法A和B,在A方法中调用了B方法,如果锁不是可重入的,那么访问B时需要再次竞争锁,这样会带来死锁. public synchronized void A(){ B(); } public synchronized void B(){ } 可重入

聊聊高并发(十八)理解AtomicXXX.lazySet方法

看过java.util.concurrent.atomic包里面各个AtomicXXX类实现的同学应该见过lazySet方法,比如AtomicBoolean类的lazySet方法 public final void lazySet(boolean newValue) { int v = newValue ? 1 : 0; unsafe.putOrderedInt(this, valueOffset, v); } 它的底层实现调用了Unsafe的putOrderedInt方法,来看看putOrde

聊聊高并发(十二)分析java.util.concurrent.atomic.AtomicStampedReference源码来看如何解决CAS的ABA问题

在聊聊高并发(十一)实现几种自旋锁(五)中使用了java.util.concurrent.atomic.AtomicStampedReference原子变量指向工作队列的队尾,为何使用AtomicStampedReference原子变量而不是使用AtomicReference是因为这个实现中等待队列的同一个节点具备不同的状态,而同一个节点会多次进出工作队列,这就有可能出现出现ABA问题. 熟悉并发编程的同学应该知道CAS操作存在ABA问题.我们先看下CAS操作. CAS(Compare and

聊聊高并发(十九)理解并发编程的几种"性" -- 可见性,有序性,原子性

这篇的主题本应该放在最初的几篇,讨论的是并发编程最基础的几个核心概念,但是这几个概念又牵扯到很多的实际技术,比如Java内存模型,各种锁的实现,volatile的实现,原子变量等等,每一个都可以展开写很多,尤其是Java内存模型,网上已经能够有很几篇不错的文章,暂时不想重复造轮子,这里推荐几篇Jave内存模型的资料: 1. JSR-133 FAQ 2. JSR-133 Cookbook 3. Synchronization and Java Memory Model 4. 深入理解Java内存模

聊聊高并发(十九)理解并发编程的几种"性" -- 可见性,有序性,原子性

这篇的主题本应该放在最初的几篇.讨论的是并发编程最基础的几个核心概念.可是这几个概念又牵扯到非常多的实际技术.比方Java内存模型.各种锁的实现,volatile的实现.原子变量等等,每个都可以展开写非常多,尤其是Java内存模型,网上已经可以有非常几篇不错的文章,临时不想反复造轮子.这里推荐几篇Jave内存模型的资料: 1. JSR-133 FAQ 2. JSR-133 Cookbook 3. Synchronization and Java Memory Model 4. 深入理解Java内

java并发学习--第十章 java内存模型的内存语义

一.锁的内存语义 所为的java内存模型的内存语义指的就是在JVM中的实现原则. 锁的内存语义:锁除了让临界区互斥执行外,还可以让释放锁的线程向获取同一个锁的线程发送消息. 我们把上面这句话再整理下: 当线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中. 当线程获取锁时,JMM会把该线程对应的本地内存置为无效.从而使得被监视器保护的临界区代码必须要从主内存中去读取共享变量. 锁的内存语义实现: synchronized.ReentrantLock: 二.volatile内存

Java内存模型与线程 深入理解Java虚拟机总结

在许多情况下,让计算机同时去做几件事情,不仅是因为计算机的运算能力强大了,还有一个很重要的原因是计算机的运算速度与它的存储和通信子系统速度的差距太大, 大量的时间都花费在磁盘I/O.网络通信或者数据库访问上. 如果不希望处理器在大部分时间里都处于等待其他资源的状态,就必须使用一些手段去把处理器的运算能力 " 压榨 " 出来, 否则就会造成很大的浪费,而计算机同时处理几项任务则是最容易想到.也被证明是非常有效的 " 压榨 " 手段. 除了充分利用计算机处理器的能力外,

深入理解JMM(Java内存模型) --(四)volatile

volatile的特性 当我们声明共享变量为volatile后,对这个变量的读/写将会很特别.理解volatile特性的一个好方法是:把对volatile变量的单个读/写,看成是使用同一个监视器锁对这些单个读/写操作做了同步.下面我们通过具体的示例来说明,请看下面的示例代码: [java] view plain copy class VolatileFeaturesExample { volatile long vl = 0L;  //使用volatile声明64位的long型变量 public

性能测试三十六:内存溢出和JVM常见参数及JVM参数调优

堆内存溢出: 此种溢出,加内存只能缓解问题,不能根除问题,需优化代码堆内存中存在大量对象,这些对象都有被引用,当所有对象占用空间达到堆内存的最大值,就会出现内存溢出OutOfMemory:Java heap space 永久代溢出 如果发生,则是在初始化的时候,空间太小,解决办法,扩大空间类的一些信息,如类名.访问修饰符.字段描述.方法描述等,所占空间大于永久代最大值,就会出现OutOfMemoryError:PermGen space 内存溢出的检测方法:pid=1730 Jdk/bin目录下