上一次我们说到了可见性,原子性,有序性,今天我们看看如何解决这个问题,今天我们先看看可见性和有序性,因此我们先要知道java内存模型
什么是java内存模型
我们上一节已经知道,可见性是由于缓存导致,有序性是由于编译优化导致,因此我们会只需要禁止缓存和编译优化,原理上是可行的,但是系统的性能让我们堪忧
因此按需禁止缓存和编译优化才能真正的解决问题,按需禁止是按照程序的想法进行,只要提供按需禁止的方法即可,Java内存模型是一个很复杂的模型,实际Java内存模型规范了jvm如何提供按需禁止缓存和编译优化,具体表现volatile,sychnorized,final,以及Happens-Before六项原则
使用volatile困惑
volatile并不是Java的产物在古老的C语言也有,他的含义就是禁止CPU缓存
volatile int x = 0
上面变量含义是x不能从CPU缓存,从主内存读取和写入,看上去很完美,但是实际上是怎么样的呢
// 以下代码来源于【参考1】 class VolatileExample { int x = 0; volatile boolean v = false; public void writer() { x = 42; v = true; } public void reader() { if (v == true) { // 这里x会是多少呢? } } }
上面代码,线程A在调用writer方法,从主内存写入v=true,然后线程B调用reader从主内存中读取v=true,但是这个时候x的值是多少的?
按照我们的直觉想当然的认为是x=42,但是呢,在1.5jdk版本之前可能是42也可能是0,但是在1.5版本以上的版本x=42,是不是很疑惑,那是因为1.5进行了增强,怎么增强的,答案是Happens-Before规则.
Happens-Berore规则
Happens-Before都意味是前一个操作发生在后一个操作之前,这样认为就是错误的,其真正意义是前一个操作对后一个操作可见,才是真正的正解,正如心灵感应一样,一个人的想法,隔着千里之外也能看到,Happens-Before就是约束了编译优化的行为,且允许优化,但是要按照Happens-Before的规则
// 以下代码来源于【参考1】 class VolatileExample { int x = 0; volatile boolean v = false; public void writer() { x = 42; v = true; } public void reader() { if (v == true) { // 这里x会是多少呢? } } }
对上一段代码讲解一下
再此是不是可以理解上面1.5版本的增强操作了
4.管程中的锁规则
要理解这个规则,首先要知道管程是什么,管程是一种同步原语,
而synchronized就是Java的管程的实现
synchronized (this) { //此处自动加锁 // x是共享变量,初始值=10 if (this.x < 12) { this.x = 12; } } //此处自动解锁
当进入synchronized相当就进行加锁,离开的时候自动释放掉,加锁是隐式的。结合规则4,线程A进入synchronized之后初始化x=10,离开自动解锁,而线程B进入代码的时候,就会看到x=12,符合我们的直接,也很好理解
5.线程start()规则
这是关于线程的规则,指线程A调用线程B的start之后,线程B可以看到主线程A在启动线程B的所有操作,
Thread B = new Thread(()->{ // 此处对共享变量var修改 var = 66; }); // 例如此处对共享变量修改, // 则这个修改结果对线程B可见 // 主线程启动子线程 B.start(); B.join() // 子线程所有对共享变量的修改 // 在主线程调用B.join()之后皆可见 // 此例中,var==66
正如上面代码,在主线程启动线程B,在启动之前的变量修改,在线程B启动之后都是可见的。
6.线程join规则
这个是指线程等待,是指线程A等待线程B执行完成(使用B.join实现等待),当子线程启动完之后(主线程中join的返回),主线程能能够看到子线程的操作,即看到对共享变量的操作。
如下代码
Thread B = new Thread(()->{ // 此处对共享变量var修改 var = 66; }); // 例如此处对共享变量修改, // 则这个修改结果对线程B可见 // 主线程启动子线程 B.start(); B.join() // 子线程所有对共享变量的修改 // 在主线程调用B.join()之后皆可见 // 此例中,var==66
换句话说,如果线程A中,调用线程的join并成功返回,那么线程B中的任意操作Happens-before于join方法返回.
final作用
前面我们看到volatile是为了禁止缓存和编译优化,但是有没有告诉编译器优化的好一些呢,这个可以有,使用final,告诉编译器这个变量生而不变,可以使劲优化,但是要注意逸出的问题。
// 以下代码来源于【参考1】 final int x; // 错误的构造函数 public FinalFieldExample() { x = 3; y = 4; // 此处就是讲this逸出, global.obj = this; }
如上面代码,this复制给了全局变量global.obj,这样的就是逸出,线程通过全局变量global.obj取到的x可能等于0.因此我们要避免逸出的情况。(有可能通过global.obj 可能访问到还没有初始化的this对象,将this赋值给global.obj时,this还没有初始化完,this还没有初始化完,this还没有初始化完)
本文分享自微信公众号 - 洁癖是一只狗(rookie-dog),作者:洁癖汪
原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。
原始发表时间:2020-10-19
本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。
我来说两句