目录
Volatile
掌握好Volatile的概念是理解Doug Lea的AQS的前提,如果你学习AQS的时候,不想一脸茫然和胸塞虚竹的话,还是先来看一下JMM(java内存模型)对volatile的写和读的内存语义的描述。
volatile写和锁的释放有相同的内存语义,当写一个volatile变量时,JMM会把该线程对应的本地内存(工作内存)中的共享变量值刷新到主内存。
volatile读与锁的获取有相同的内存语义,当读一个volatile变量时,JMM会把该线程对应的本地内存(工作内存)置为无效。线程接下来将从主内存中读取共享变量。
摆概念谁都会,关键是关联式地摆概念,这可能是专栏编辑工作者为了写好文章一定要会的方法。
java内存模型(JMM)?听起来好有术语感。是的,JMM是一个专业术语词汇,它包括的概念有:主内存、工作内存、原子性、可见性、顺序一致性、重排序、volatile、锁、先行发生原则(happens-before)等等。JMM的提出和建立,主要是解决一个问题:在什么条件下,写入内存中一个共享变量对其他线程可见,从而使得并发访问内存的操作不会产生歧义。volatile这个概念也是如此。
一下子牵引出来这么多概念,对于大多数工程师来讲,关联式地铺陈这么多概念可能都会觉着有点儿心累。我们这里只提携有关volatile有关的概念。
当然了,我们现在知道对一个内存中的变量不加控制地(就是指java内存模型的管控)读和写,都会遇到多线程安全的问题,因为我们尝试过。而多线程安全一般是由工作内存和重排序导致。
再来拉近一下工作内存(本地内存)。从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在,它涵盖了缓存(例如高速缓存,L1,L2,L3)、写缓冲区、寄存器以及其他的硬件和编译器优化,说到硬件,在多处理器的计算机物理系统中,有以下的处理器、高速缓存和主内存的交互关系:
这里的高速缓存和缓存一致性协议也是属于工作内存。
然而,JMM概念中的线程、工作内存和主内存三者的交互关系,也是与上图中物理系统相类似,也可以说是软件模型的设计借鉴了硬件系统。
上图中,每条线程(类比处理器)都有自己的工作内存(类比高速缓存),线程对共享变量的操作都是先在工作内存中完成的,不能直接读写主内存中的共享变量,线程之间无法操作对方的工作内存,线程间至于通过主内存(这里的主内存概念可以实指硬件上的主内存)来传递变量值。
有了工作内存这个概念后,那么现在来问:在什么条件下,一个线程写入内存中一个共享变量对另一个线程线程可见?答:一个线程要把写入工作内存中的值刷新到主内存,另一个线程无视工作内存要去主内存中读取。
从整体来看,这两个步骤实质上是线程A在向线程B发送消息,而且这个通信过程必须要经过主内存。JMM通过控制主内存与每个线程的本地内存之间的交互,来为Java程序员提供内存可见性保证,Volatile修饰的共享变量它就提供了内存可见性保证。
JMM还对Volatile提供了禁止重排序的功能,下面来认识一下重排序这个概念。
从Java源代码到最终实际执行的指令序列,会分别经历下面3种重排序:
上述的1属于编译器重排序,2和3属于处理器重排序。对于编译器,JMM的编译器重排序规则会禁止特定类型的编译器重排序(不是所有的编译器重排序都要禁止)。对于处理器重排序,JMM的处理器重排序规则会要求Java编译器在生成指令序列时,插入特定类型的内存屏障(Memory Barriers,Intel称之为Memory Fence)指令,通过内存屏障指令来禁止特定类型的处理器重排序。
好了,来看内存屏障,为了保证内存可见性,Java编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序。JMM把内存屏障指令分为4类,如下表所示:
StoreLoad Barriers是一个“全能型”的屏障,它同时具有其他3个屏障的效果。现代的多处理器大多支持该屏障(其他类型的屏障不一定被所有处理器支持)。执行该屏障开销会很昂贵,因为当前处理器通常要把写缓冲区中的数据全部刷新到内存中(Buffer Fully Flush)。
而常见的处理器的重排序规则如下:
a=1;b=a;其中b对a有“数据依赖”,几乎所有处理器都有禁止数据依赖的重排序规则。
JMM为了实现volatile写/读的内存语义,针对编译器制定了volatile重排序规则表:
从表我们可以看出。
当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。这个规则确保volatile写之前的操作不会被编译器重排序到volatile写之后。
当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。这个规则确保volatile读之后的操作不会被编译器重排序到volatile读之前。
当第一个操作是volatile写,第二个操作是volatile读时,不能重排序。
为实现volatile的内存语义(包括上面的重排规则表),编译器在生成字节码时,会JMM采取保守策略在指令序列中插入内存屏障来禁止特定类型的处理器重排序,因为保守策略它可以保证在任意处理器平台,任意的程序中都能得到正确的volatile内存语义。保守策略如下:
·在每个volatile写操作的前面插入一个StoreStore屏障。
·在每个volatile写操作的后面插入一个StoreLoad屏障。
·在每个volatile读操作的后面插入一个LoadLoad屏障。
·在每个volatile读操作的后面插入一个LoadStore屏障。
实现了volatile重排序的一些规则,我们再来重申一下volatile的内存语义:
volatile写和锁的释放有相同的内存语义,当写一个volatile变量时,JMM会把该线程对应的本地内存(工作内存)中的共享变量值刷新到主内存。
volatile读与锁的获取有相同的内存语义,当读一个volatile变量时,JMM会把该线程对应的本地内存(工作内存)置为无效。线程接下来将从主内存中读取共享变量。
由于不能重排对普通共享变量的写和之后对volatile变量的写,所以当写一个volatile变量时,JMM会把该线程对普通共享变量写的值也会刷新到主内存;且不能重排对volatile变量的读和之后对普通共享变量的读,所以当读一个volatile变量时,JMM会把该线程对应的工作内存置为无效,线程接下来将从主内存中读取普通共享变量,这时候读取的变量值就是上次无论在哪个线程刷新到主内存的值。
看来禁止重排序是一种解决可见性的方法。
在 C/C++ 中,对Volatile变量的读,不会依赖缓存中的值(有可能是在寄存器中),而是直接去对应的内存中获取,对它的写则写回内存(易变性);告诉编译器该变量不可优化,不要对我这个变量进行各种激进的优化,甚至将变量直接消除,保证程序员写在代码中的指令,一定会被执行(不可优化性);Volatile变量与非Volatile变量之间的操作,是可能被编译器交换顺序的, Volatile变量间的操作,是不会被编译器交换顺序的,也就是能够保证Volatile变量间的顺序性,编译器不会进行乱序优化,但是不保证处理器会进行乱序优化(顺序性)。
Volatile关键字就重排序优化这一项,java有更严格的实现,不仅因为JMM实现了C/C++ 中Volatile的三个特性,还因为JMM实现了Volatile具有锁释放、锁获取的语义以及关于Volatile的重排序规则表。
happens-before
JSR-133使用happens-before的概念来阐述操作之间的内存可见性。happens-before规则简单易懂,它避免Java程序员为了理解JMM提供的内存可见性保证而去学习复杂的重排序规则以及这些规则的具体实现方法。
与程序员密切相关的happens-before规则如下。
程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
happens-before规则其实就是JMM通过内存屏障来禁止某种类型的编译器重排序和禁止某种类型的处理器重排序来具体实现的。
你的打赏是我奋笔疾书的动力!
支付宝打赏:
微信打赏: