天河建设网站技术,西安做网站,自动做标题网站,做网站客户要求多很烦java内存模型的理解并发问题产生的源头缓存导致的可见性问题线程切换导致的原子性问题编译优化带来的有序性问题小结Java内存模型: 解决可见性和有序性问题Java内存模型与JVM内存模型的区别volatile关键字Happens-Before规则小结思考题参考并发问题产生的源头
缓存导致的可见性…
java内存模型的理解并发问题产生的源头缓存导致的可见性问题线程切换导致的原子性问题编译优化带来的有序性问题小结Java内存模型: 解决可见性和有序性问题Java内存模型与JVM内存模型的区别volatile关键字Happens-Before规则小结思考题参考并发问题产生的源头
缓存导致的可见性问题
可见性定义: 一个线程对共享变量的修改,另外一个线程能够立刻看到,我们称为可见性。
在单核CPU时代,所有线程都是被同一个CPU调度,因此共享同一块cpu的缓存,不存在缓存一致性问题 多核CPU时代每个线程都有可能同时被不同的cpu调度每个cpu都有各自的缓存并且读写优先走缓存这就会导致缓存一致性问题: 高速缓存和主内存之间如何保持数据一致性 线程切换导致的原子性问题
CPU能保证的原子操作是CPU指令级别的高级语言里一条语句往往需要多条 CPU 指令完成例如:count 1至少需要三条 CPU 指令 编译优化带来的有序性问题
有序性指的是程序按照代码的先后顺序执行。编译器为了优化性能有时候会改变程序中语句的先后顺序从而导致意外的bug。
最常见的例子就是双重锁检查创建单例对象了:
public class Singleton {static Singleton instance;static Singleton getInstance(){if (instance null) {synchronized(Singleton.class) {if (instance null)instance new Singleton();}}return instance;}
}new创建对象在java中分为三步:
分配一块内存 M在内存 M 上初始化 Singleton 对象然后 M 的地址赋值给 instance 变量。
但是如果编译器进行了指令重排序优化变成了下面这样:
分配一块内存 M将 M 的地址赋值给 instance 变量最后在内存 M 上初始化 Singleton 对象。
在下面场景中线程B访问未初始化过的instance,可能会触发空指针异常: 小结
java并发编程问题三大根本来源: 可见性,原子性,有序性
缓存导致的可见性问题线程切换导致的原子性问题编译优化导致的有序性问题 Java内存模型: 解决可见性和有序性问题
Java内存模型与JVM内存模型的区别 Java内存模型定义了一套规范能使JVM按需禁用cpu缓存和禁止编译优化。这套规范包括对volatile, synchronized, final三个关键字的解析和7个Happen-Before规则。 JVM内存模型是指程序计数器虚拟机栈本地方法栈堆方法区这5和要素。 volatile关键字
volatile在c语言中最原始的含义就是禁用cpu缓存,volatile修饰符表达的是: 对某个变量的读写,不能使用cpu缓存必须从内存中读取或者写入。
大家看下面这个例子: 线程A执行writer方法,按照volatile语义会把变量vtrue写入内存假设线程B执行reader方法同样按照volatile语义线程B会从内存中读取变量v, 如果线程B看到vtrue时那么线程B看到的变量x的值是多少呢
class VolatileExample {int x 0;volatile boolean v false;public void writer() {x 42;v true;}public void reader() {if (v true) {// 这里x会是多少呢}}
}答案:
jdk 1.5之前x可能是42也可能是0 因为变量x可能被cpu缓存而导致可见性问题jdk 1.5之后, x就是等于42。因为java内存模型在1.5版本对volatile语义进行了增强
怎么增强的呢
happens-before规则 Happens-Before规则
Happens-Before规则前面一个操作的结果对后续操作是可见的
Happens-Before规则约束了编译器的优化行为虽允许编译器优化但是要求编译器优化后一定遵守Happens-Before规则。
具体规则如下:
程序的顺序性规则在一个线程中前面的操作Happens-Before于后续的任意操作。volatile变量规则对一个volatile变量的写操作Happens-Before于对这个volatile变量的读操作。传递性规则A Happens-Before BB Happens-Before C那么A Happens-Before C。
从图中我们可以看到
“x42” Happens-Before 写变量 “vtrue” 这是规则 1 的内容写变量“vtrue” Happens-Before 读变量 “vtrue”这是规则 2 的内容 。
再根据这个传递性规则我们得到结果“x42” Happens-Before 读变量“vtrue”。这意味着什么呢如果线程 B 读到了“vtrue”那么线程 A 设置的“x42”对线程 B 是可见的。也就是说线程 B 能看到 “x 42” 有没有一种恍然大悟的感觉这就是 1.5 版本对 volatile 语义的增强这个增强意义重大1.5 版本的并发工具包java.util.concurrent就是靠 volatile 语义来搞定可见性的。
管程中锁的规则synchronized是Java对管程的实现隐式加锁、释放锁对一个锁的解锁Happens-Before于后续对这个锁的加锁。线程start()规则主线程A启动子线程B后start()操作 Happens-Before于子操作中的任意操作。线程join规则在线程A中调用线程B的join()并返回线程B中的任意操作 Happens-Before于join()的返回。线程中断规则对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生可以通过Thread.interrupted()方法检测到是否有中断发生。对象终结规则一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。
上述很多规则都需要配合传递性规则进行理解。 小结
Java内存模型涉及的几个关键词锁、volatile字段、final修饰符与对象的安全发布。
第一是锁锁操作是具备happens-before关系的解锁操作happens-before之后对同一把锁的加锁操作。实际上在解锁的时候JVM需要强制刷新缓存使得当前线程所修改的内存对其他线程可见。第二是volatile字段volatile字段可以看成是一种不保证原子性的同步但保证可见性的特性其性能往往是优于锁操作的。但是频繁地访问 volatile字段也会出现因为不断地强制刷新缓存而影响程序的性能的问题。第三是final修饰符final修饰的实例字段则是涉及到新建对象的发布问题。当一个对象包含final修饰的实例字段时其他线程能够看到已经初始化的final实例字段这是安全的。
Java内存模型底层怎么实现的
主要是通过内存屏障(memory barrier)禁止重排序的即时编译器根据具体的底层体系架构将这些内存屏障替换成具体的 CPU 指令。对于编译器而言内存屏障将限制它所能做的重排序优化。而对于处理器而言内存屏障将会导致缓存的刷新操作。比如对于volatile编译器将在volatile字段的读写操作前后各插入一些内存屏障。
在java中Happens-Before规则本质还是一种可见性A Happens-Before B,意味着A事件对B事件来说是可见的无论A事件和B事件是否发生在同一个线程里例如: 事件A发生在线程1事件B发生在线程2Happens-Before规则保证线程2上也能看到A事件的发送。 思考题
还是文中给出的案例大家思考会不会产生x42和vtrue重排序导致线程B读取到x0的结果呢?
class VolatileExample {int x 0;volatile boolean v false;public void writer() {x 42;v true;}public void reader() {if (v true) {// 这里x会是多少呢}}}解答:
程序顺序性规则是针对单线程的。如果只考虑单线程那么编译器可以对范例代码进行指令重排优化。但是对于多线程volatile 变量规则、传递性这 2 条规则就附加了新的限制。对于多线程这 3 条 happens-before 规则要求线程 B 在读到 v true 的时候也能见到 x 42。如果编译器仍然按照单线程的情况对这两条语句进行指令重排把 v true 放到 x 42 之前。那么线程 B 就有可能看不到 x 的值为 42。这显然违背了 happens-before 的规定。编译器为了符合规则只能不进行指令重排优化了。为了符合 happens-before 规定对于示例代码编译器不能进行指令重排的编译优化。但实际上仅仅不进行指令重排编译优化并不能保证编译后的代码的执行结果符合 happnes-before 规定。因为指令重排这一优化措施并不仅仅是编译器会做。现代 cpu 在执行机器指令的时候同样会做指令重排的优化。所以如果 cpu 在执行机器指令时发生了机器指令的重排序。上述实例代码的结果仍然有机会不符合 happens-before 规定。为了令到编译后的代码的执行结果能够符合 happens-before 规定。编译器除了不能做指令重排序编译优化之外还要在生成的机器代码中。加入特定 cpu 指令令到 cpu 只会执行完这条特定指令之后才会执行后续的其它机器指令。而这种特定指令是通过建立 “内存屏障” 来禁止 cpu 的指令重排序的。那为什么叫 “内存屏障” 呢可以理解为一种特殊指令要求 cpu 把缓存数据写回到主内存中。这就像在内存中建立了一道屏障令到后面的代码不能越过屏障提前执行。
jmm 是一个规范它用于指导编译器的行为。但它本身不会限制编译器所使用的具体编译技术。所以在 jmm 规范中不会提到 “指令重排” 或者 内存屏障” 这些具体的实现技术。这是我们在学习规范类知识的时候需要注意的。 参考
JAVA并发编程实战