前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >从 DCL 的对象安全发布谈起

从 DCL 的对象安全发布谈起

作者头像
四火
发布2022-07-18 13:44:34
3000
发布2022-07-18 13:44:34
举报
文章被收录于专栏:四火的唠叨

对于 DCL(Double Check Lock)情况下的对象安全发布,一直理解得不足够清楚;在通过和同事,以及和互联网上一些朋友的讨论之后,我觉得已经把问题搞清楚了。我把我对这个问题的理解简要记录在这里。

现在有代码 A:

代码语言:javascript
复制
class T {
	private static volatile T instance;
	public M m; // 这里没有 final 修饰

	public static T getInstance() {
		if (null == instance) {
			synchronized (T.class) {
				if (null == instance) {
					instance = new T();
					instance.m = new M();
				}
			}
		}
		return instance;
	}
}

以及代码 B: 

代码语言:javascript
复制
class T {
	private static volatile T instance;
	public M m; // 这里没有 final 修饰

	public static T getInstance() {
		if (null == instance) {
			synchronized (T.class) {
				if (null == instance) {
					T temp = new T();
					temp.m = new M();
					instance = temp;
				}
			}
		}
		return instance;
	}
}

这两段代码是否做到了对象安全发布?

这里需要稍微解释一下,所谓对象安全发布,在这里可以这样理解,有一个线程 X 调用 getInstance 方法,第一次来获取对象,instance 为空,这个时候进入同步块,初始化了 instance 并返回;这以后另一个线程 Y 也调用 getInstance 方法,不进入同步块了,获取到的 instance 对象是否一定是预期的—— 即对象的 m 属性不为空?如果是,表示对象被安全发布了,反之则不是。

happens-before 一致性

仔细读了读 JSR-133 的规范文档,里面定义了 happens-before(hb)一致性:

Happens-before consistency says that a read r of a variable v is allowed to observe a write w to v if, in the happens-before partial order of the execution trace:

  • r is not ordered before w (i.e., it is not the case that r hb w), and 
  • there is no intervening write w' to v (i.e., no write w' to v such that w hb w' hb r).

这就是说,如果任何时候在满足以下这样两个条件的情况下,对一个对象的读操作 r,都能得到对于对象的写操作 w 的结果(读的时候要能返回写的结果),我们就认为它就是满足 happens-before 一致性的:

  • 读必须不能发生在写操作之前;
  • 没有一个中间的写操作 w' 发生在 w 和 r 之间。

满足这样一致性的内存模型,是一种极度简化的内存模型,它允许 JVM 实现的时候,对于绝大多数情况下不需要满足 happens-before 的对象和操作,可以在保证单个线程运行结果正确的情况下做尽可能多的优化,比如代码乱序执行,比如从主内存中缓存某些变量到寄存器等等。

volatile 和 happends-before 的关系

A write to a volatile field happens-before every subsequent read of that volatile.

就是说,对于 volatile 修饰的属性的读写操作满足 happens-before 一致性。

再结合代码来看,代码 A 对于 m 的赋值发生在 volatile 修饰的 instance 之后,不能保证线程 X 中给 instance 的属性赋的值 new M() 能被线程 Y 看到;而代码 B 所有对于实例初始化的操作都放 instance=temp;(即对 volatile 修饰的属性 instance 的写操作)之前,这些操作的结果都是“ 可见的”。也就是说,代码 A 无法安全发布对象,但是代码 B 可以。

需要说明的是,如果对于代码 B,干脆去掉属性 m,但是也拿掉 volatile,变成如下情况呢?

代码语言:javascript
复制
class T {
	private static T instance;

	public static T getInstance() {
		if (null == instance) {
			synchronized (T.class) {
				if (null == instance) {
					instance = new T();
				}
			}
		}
		return instance;
	}
}

这种情况下对象又无法安全发布了,因为没有了 volatile 的约束,对象初始化的行为和把对象赋给 instance 的行为是乱序的(前面已经介绍过了,只需要保证结果正确即可,在这里就是保证 getInstance 方法返回的结果是正确的;但是,在 getInstance 方法内部,当 instance 不为空的时候,T 的初始化行为却未必已经完成),这个就有可能取到一个没有初始化完成的残缺的对象。

除了 volatile 关键字以外,还有哪些情况下也满足 happens-before 一致性呢?

  • Each action in a thread happens-before every subsequent action in that thread.
  • An unlock on a monitor happens-before every subsequent lock on that monitor.
  • A write to a volatile field happens-before every subsequent read of that volatile.
  • A call to start() on a thread happens-before any actions in the started thread.
  • All actions in a thread happen-before any other thread successfully returns from a join() on that thread.
  • If an action a happens-before an action b, and b happens before an action c, then a happens- before c.

简单说,就是同一个线程的后续行为,加锁,启动子线程,线程 join() 操作和满足传递性的三个操作这六种情况,其他所有的情况都不具备 happens-before 一致性。值得一提的是其中的第一条,需要理解其中的“subsequent action”(后续行为),比如调用一个方法返回的结果应当是正确的,类的每一条静态语句的执行结果也是正确的。这是 hb 内存模型在降低约束、提供更多优化可能的同时,必须要做到的正确性上的保证。

final 在 JSR-133 中的增强

由于 final 的值本身是不可被重写入的(所谓的“ 不变” 对象),那么编译器就可以针对这一点进行优化:

Compilers are allowed to keep the value of a final field cached in a register and not reload it from memory in situations where a non-final field would have to be reloaded.

编译器可以把 final 修饰的属性的值缓存在寄存器里面,并且在执行过程中不重新加载它。

但是,如果对象属性不使用 final 修饰,在构造器调用完毕之后,其他线程未必能看到在构造器中给对象实例属性赋的真实值(除非有其他可行的方式保证 happens-before 一致性,比如前面提到的代码 B):

A thread that can only see a reference to an object after that object has been completely initialized is guaranteed to see the correctly initialized values for that object’s final fields.

仅当在使用 final 修饰属性的情况下,才可以保证在对象初始化完成之后,外部能够看到对象正确的属性值。

代码语言:javascript
复制
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; // guaranteed to see 3
			int j = f.y; // could see 0
		}
	}
}

这个例子正式规范里面给出的,上面属性 x 使用了 final 修饰,而 y 没有,在某一时刻,一个线程调用 writer() 的时候,FinalFieldExample 被初始化;之后另一个线程去取得这个对象,首先最开始的时候 f 未必一定不为空,因为 f 并没有任何 happens-before 一致性保证(f 可能被赋了一个构造并未完成的对象),其次对于属性 x,由于 final 关键字的效应,f 不为空的时候,f 已经初始化完成,所以 f.x 一定为准确的 3,但是 f.y 就不一定了。

还有其它的单例对象安全发布的方式:

代码语言:javascript
复制
public class T {
  private static final T instance = new T(); // final 可少吗?
  public final M m = new M(); // final 可少吗?

  public static T getInstance() {
    return instance;
  }
}

这种是很常见的,还有一种叫做 Initialization On Demand Holder 的方式:

代码语言:javascript
复制
class T {
	public final M m = new M(); // final 可少吗?

	private static class LazyHolder {
		public static T instance = new T();
	}

	public static T getInstance() {
		return LazyHolder.instance;
	}
}

这两段代码在不使用的时候都可以保证对象安全发布的,因为这种写法下,对于属性的初始化会在对象的构造器调用前完成,这是前面说的 happens-before 的第一种(Each action in a thread happens-before every subsequent action in that thread.),属于对程序正确性的要求。

附件:JSR-133 规范下载

文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接 《四火的唠叨》

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

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

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

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