第一部分:并发理论基础02->java内存模型,看java如何解决可见性和有序性问题

1.前情提要

可见性,原子性,有序性,称为并发编程的bug之源

2.java的内存模型

导致可见性问题是cpu缓存引起
导致有序性问题是编译器优化
那么解决方案是什么?
禁用缓存和禁用编译优化,但是程序性能就下降了

那么如何能保证性能的同时,又解决了可见性及有序性问题?
该禁用缓存和编译优化的时候禁用,其余时候不做限制

java内存模型规范了jvm如何按需禁用缓存和按需禁用编译优化的方法。
主要包含volatile,synchronized,final三个关键字,以及6个happens-before规则

3.volatile

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执行write方法,volatile会把变量v=true写入内存,假设线程B执行reader()方法,按照volatile,线程B会从内存中读取变量v,线程看到v==true时,变量x是多少?
jdk1.5之后是42,jdk1.5之后对volatile进行了升级

4.Happens-Before规则

前面一个操作的结果对后续操作是可见的
happens-before规则榆树了编译器的优化行为

4.1程序的顺序性规则

按照程序顺序,前面操作happens-before与后续任意操作。
x=42,happens-before与v=true,符合单线程里面的思维,程序前面对某个变量的修改一定是是对叙操作可见的

4.2 volatile规则

对volatile变量的写操作,happens-before与后续对这个volatile变量的读操作
有点禁用缓存的意思

4.3 传递性

A happens-before B,且B happens-before c,那么A happens-before c
image

x=42,happens-before与写变量v=true,这是4.1程序顺序性规则的内容
v=true happens-before 读变量v==true,这是4.2 volatile规则

根据传递性原则
x=52 happens-before 读变量vtrue
如果线程B读到了v
true,那么线程A设置的x=42对线程B是可见的。

4.4 管程中锁的规则

锁的解锁happens-before与后续对这个锁的加锁操作

管程是java中提供的同步原语,就是synchronized关键字


synchronized (this) { //此处自动加锁
  // x是共享变量,初始值=10
  if (this.x < 12) {
    this.x = 12; 
  }  
} //此处自动解锁

结合4.4的规则,我们可以判断出如果x的初始值是10,线程A执行完代码库后x的值会变为12,然后自动释放锁,线程B抢到锁后,能够看到线程A对x的写操作,也就是说线程B可以看到x==12

4.5 线程start()规则

线程A调用线程B的start方法,那么该start操作happens-before与线程B中的任意操作


Thread B = new Thread(()->{
  // 主线程调用B.start()之前
  // 所有对共享变量的修改,此处皆可见
  // 此例中,var==77
});
// 此处对共享变量var修改
var = 77;
// 主线程启动子线程
B.start();

4.6 线程join规则

线程等待,主线程A等待子线程B完成,调用join,子线程B完成后,主线程能够看到子线程的操作
看到的是对共享变量的操作

线程A调用线程B的join()并成功返回,那么线程B的任意操作happens-before 与该join操作的返回


Thread B = new Thread(()->{
  // 此处对共享变量var修改
  var = 66;
});
// 例如此处对共享变量修改,
// 则这个修改结果对线程B可见
// 主线程启动子线程
B.start();
B.join()
// 子线程所有对共享变量的修改
// 在主线程调用B.join()之后皆可见
// 此例中,var==66

5.final

final 关键字可以告诉编译器优化的更好一点
final修饰变量时,告诉编译器,这个变量生而不变,可劲优化

现在final 修饰的变量,对编译重排进行了约束,不会导致有序性问题并产生空指针异常


// 以下代码来源于【参考1】
final int x;
// 错误的构造函数
public FinalFieldExample() { 
  x = 3;
  y = 4;
  // 此处就是讲this逸出,
  global.obj = this;
}

5.总结

happens-before 本质上是一种可见性

共享变量abc,在一个线程里设置了abc的值=3,那么有什么办法可以让其他线程看到abc==3

1.声明共享变量abc,并使用volatile关键字修饰
2.声明共享变量abc,在synchronized关键字对abc的赋值代码库加锁,由于happen-before管程锁规则,后续线程可以看到abc的值
3.A线程启动后,使用A.JOIN()方法来完成运行,后续线程再启动,就一定可以看到abc==3
posted @ 2021-06-25 14:02  SpecialSpeculator  阅读(92)  评论(0编辑  收藏  举报