前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >如何解决可见性,有序性,原子性

如何解决可见性,有序性,原子性

作者头像
小土豆Yuki
发布2020-11-03 11:34:22
6030
发布2020-11-03 11:34:22
举报
文章被收录于专栏:洁癖是一只狗洁癖是一只狗

上一次我们说到了可见性,原子性,有序性,今天我们看看如何解决这个问题,今天我们先看看可见性和有序性,因此我们先要知道java内存模型

什么是java内存模型

我们上一节已经知道,可见性是由于缓存导致,有序性是由于编译优化导致,因此我们会只需要禁止缓存和编译优化,原理上是可行的,但是系统的性能让我们堪忧

因此按需禁止缓存和编译优化才能真正的解决问题,按需禁止是按照程序的想法进行,只要提供按需禁止的方法即可,Java内存模型是一个很复杂的模型,实际Java内存模型规范了jvm如何提供按需禁止缓存和编译优化,具体表现volatile,sychnorized,final,以及Happens-Before六项原则

使用volatile困惑

volatile并不是Java的产物在古老的C语言也有,他的含义就是禁止CPU缓存

代码语言:javascript
复制
volatile int x = 0

上面变量含义是x不能从CPU缓存,从主内存读取和写入,看上去很完美,但是实际上是怎么样的呢

代码语言:javascript
复制
// 以下代码来源于【参考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的规则

代码语言:javascript
复制
// 以下代码来源于【参考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. 程序的顺序性 按照程序的顺序,前面的操作happen-before后面的任意操作,比如上面的例子第6行x=42,Happens-before第7行v=true,规则一比较好理解,程序前面的修改对后面的操作是可见
  2. volatile变量规则 这条规则是对volatile变量的写操作,hanppens-before后面操作的读操作.这样看起来和1.5版本的含义没有区别呀,但看这样就是禁止缓存的,但是你在看第三条规则,就会明白,1.5版本之后的增强
  3. 传递性 是指AHappens-Before B,B hanppens-before C,则A Happens-Before C, 我们结合下面图片来看

对上一段代码讲解一下

  1. X=42happens-before 写变量v=true,规则1
  2. 写v=true happens-before读变量v=true,规则2
  3. 在根据传递性,则x=42 happens-before 读v=true

再此是不是可以理解上面1.5版本的增强操作了

4.管程中的锁规则

要理解这个规则,首先要知道管程是什么,管程是一种同步原语,

而synchronized就是Java的管程的实现

代码语言:javascript
复制
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的所有操作,

代码语言:javascript
复制

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的返回),主线程能能够看到子线程的操作,即看到对共享变量的操作。

如下代码

代码语言:javascript
复制
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,告诉编译器这个变量生而不变,可以使劲优化,但是要注意逸出的问题。

代码语言:javascript
复制

// 以下代码来源于【参考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还没有初始化完)

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-10-19,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 洁癖是一只狗 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档