专栏首页洁癖是一只狗如何解决可见性,有序性,原子性

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

上一次我们说到了可见性,原子性,有序性,今天我们看看如何解决这个问题,今天我们先看看可见性和有序性,因此我们先要知道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. 程序的顺序性 按照程序的顺序,前面的操作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的管程的实现

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

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 创建多少线程是合适的

    面试中经常有人被问到线程池的数据设置多少合适呢,今天我们就看一下这个问题,首先我们需要知道两个问题

    小土豆Yuki
  • Semaphore,ReadWriteLock,StampedLock

    上面三个方法都是原子性的,并且这个原子性是由信号量模型实现放保证的,在java中信号量的实现是有类Semaphore实现的,下面看看下面代码,

    小土豆Yuki
  • Lock同步锁优化

    上一次我们介绍了Synchronized的优化,除此之外在JDK1.5之后,也提供了另外一种锁Lock,今天我们就看看这个有什么优势

    小土豆Yuki
  • 2020-10-06:java中垃圾回收器让工作线程停顿下来是怎么做的?

    用户线程暂停,GC 线程要开始工作,但是要确保用户线程暂停的这行字节码指令是不会导致引用关系的变化。所以 JVM 会在字节码指令中,选一些指令, 作为“安全点”...

    福大大架构师每日一题
  • 2020-12-01:java中,什么是安全点和安全区域?

    用户线程暂停,GC 线程要开始工作,但是要确保用户线程暂停的这行字节码指令是不会导致引用关系的变化。所以 JVM 会在字节码指令中,选一些指令,作为“安全点”,...

    福大大架构师每日一题
  • 2018跳槽面试必备之深入理解 Java 多线程核心知识

    导语:多线程相对于其他 Java 知识点来讲,有一定的学习门槛,并且了解起来比较费劲。在平时工作中如若使用不当会出现数据错乱、执行效率低(还不如单线程去运行)或...

    技术zhai
  • 那些有趣的代码(一) —— 有点萌的 Tomcat 的线程池

    最近抓紧时间看看了看tomcat 的源代码。发现了一些有趣的代码,应该会写一系列文章和大家分享一下。

    用户2060079
  • 一文教你安全的关闭线程池

    上篇文章 ShutdownHook- Java 优雅停机解决方案 提到应用停机时需要释放资源,关闭连接。对于一些定时任务或者网络请求服务将会使用线程池,当应用停...

    andyxh
  • Java 并发编程 71 道面试题及答案

    任何线程都可以设置为守护线程和用户线程,通过方法Thread.setDaemon(bool on);true则把该线程设置为守护线程,反之则为用户线程。Thre...

    一个优秀的废人
  • 图文介绍进程和线程的区别

    先了解一下操作系统的一些相关概念,大部分操作系统(如Windows、Linux)的任务调度是采用时间片轮转的抢占式调度方式,也就是说一个任务执行一小段时间后强制...

    趣学程序-shaofeer

扫码关注云+社区

领取腾讯云代金券