2. java内存模型:看java如何解决可见性和有序性问题 - 理论基础

1. 什么是java内存模型

缓存导致可见性问题,编译优化导致有序性问题,解决方案是按需禁用缓存和编译优化。java内存模型是个很复杂的规范,规范了jvm如何提供按需禁用缓存和编译优化的方法。方法包含volatilesynchronizedfinal三个关键字,以及六个Happens-Before规则。

2. 使用volatile的困惑

语义含义:例如volatile int x=0;告诉编译器不能使用CPU缓存,对变量x的读写,必须在内存中进行。
使用的困惑,先上代码。

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()方法,按照volatile语义,v=true写到内存;
  • 同样的,假设线程B执行reader()方法,从内存中读取到v==true, 此时x=0还是42?
  • jdk1,5版本之前x可能是0,1.5之后对volatile语义增强,所以是42,这里涉及到Happens-Before规则。

3. Happens-Before 规则

规则含义:前面一个操作结果对后续操作是可见的。Happens-Before 约束了编译器的优化行为,虽允许编译器优化,但是要求编译器优化后一定遵守 Happens-Before 规则。

3.1 程序的顺序性规则

指在一个线程中,按照代码顺序,前面的操作Happens-Before后续任意操作。比如上述代码x=42就发生在v=true之前,所以x变量对后续操作是可见的。

3.2 volatile变量规则

指一个volatile变量的写操作,Happens-Before后续对这个变量的读操作。关联3.3的规则。

3.3 传递性

指A Happens-Before B, B Happens-Before C, 那么A Happens-Before C。
在这里插入图片描述
从图中看到:

  1. x=42 Happens-Before v=true, 这是规则1;
  2. 写变量v=true Happens-Before 读变量v=true,这是规则2;
  3. 根据规则3,x=42 Happens-Before 读变量v=true, 换句话说,线程B可以读到x=42,x对线程B是可见的。

3.4 管程中锁的规则

指的是:对一个锁的解锁Happens-Before后续对这个锁的加锁
管程:一种通用的同步术语,在java中的实现是synchronized。
管程中的锁在java中是隐式实现的,例如下面的代码:进入代码块,自动加锁;执行完代码块自动释放锁。加锁和释放锁都是编译器实现的。

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

结合此规则:线程A执行完代码块,x=12,线程A释放锁;此时线程B获得锁,线程B能看到线程A对x的写操作,即B看到x=12。

3.5 线程start()规则

指的是:主线程A启动子线程B,那么子线程B可以看到主线程启动子线程B之前的操作。

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

3.6 线程join()规则

指的是:主线程A等待子线程B完成(A调用B的join()方法),B完成后,A能看到B的操作。

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

4. 被我们忽视的final

参考文档:JSR 133 (Java Memory Model) FAQ

final修饰的变量,变量一旦赋值之后就不再变化。两种情况如下:

  1. final int a = 1; a的值不再变化;
  2. final MyTest test = new MyTest(); test的引用不再变化。
class FinalFieldExample {
  final int x;
  int y;
  static FinalFieldExample f;
  public FinalFieldExample() {
    x = 3;
    y = 4;
  }

  static void writer() {
    f = new FinalFieldExample();
  }

  static void reader() {
    if (f != null) {
      int i = f.x;
      int j = f.y;
    }
  }
}

例子展示了final变量如何使用,当某个线程执行reader(), 可以保证线程看到f.x是3,但不保证f.y是4。
类似双重检查创建单例,构造函数的错误重排可能看到final的值发生变化。

在 1.5 以后 Java 内存模型对 final 类型变量的重排进行了约束。现在只要我们提供正确构造函数没有“逸出”,就不会出问题了。

public FinalFieldExample() { // bad!
  x = 3;
  y = 4;
  // bad construction - allowing this to escape
  // 此处就是 this 逸出,
  global.obj = this;
}

构造函数逸出,在构造函数里面将 this 赋值给了全局变量 global.obj,这就是“逸出”。其他线程通过global.obj读取x不一定是3,有可能是0.

5.总结

在 Java 语言里面,Happens-Before 的语义本质上是一种可见性,A Happens-Before B 意味着 A 事件对 B 事件来说是可见的,无论 A 事件和 B 事件是否发生在同一个线程里。例如 A 事件发生在线程 1 上,B 事件发生在线程 2 上,Happens-Before 规则保证线程 2 上也能看到 A 事件的发生。

Java 内存模型主要分为两部分,一部分面向你我这种编写并发程序的应用开发人员,另一部分是面向 JVM 的实现人员的,我们可以重点关注前者,也就是和编写并发程序相关的部分,这部分内容的核心就是 Happens-Before 规则。

6.课后思考

有一个共享变量 abc,在一个线程里设置了 abc 的值 abc=3

,你思考一下,有哪些办法可以让其他线程能够看到abc==3?

我的回答:

  1. 加volatile修饰
  2. 读写时加同一把锁
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值