从DCL的对象安全发布谈起

从DCL的对象安全发布谈起

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

现在有代码A:

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: 

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,变成如下情况呢?

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修饰属性的情况下,才可以保证在对象初始化完成之后,外部能够看到对象正确的属性值。

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就不一定了。

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

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的方式:

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规范下载

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

分享到:

8 comments

  1. luchy0120 说道:

    <code>class T {
        private static T instance;
     
        public static T getInstance() {
            if (null == instance) {
                synchronized (T.class) {
                    if (null == instance) {
                        instance = new T();
                    }
                }
            }
            return instance;
        }
    }</code>
    这种写法错误的原因是什么呢?synchronized的语义保证了在退出语句块时本地内存刷新
    主内存。假设A线程内new T()是乱序的,但是B线程读取instance变量是主内存中的,应该还是null,因为A线程还没退出synchronize块,还没来得及回写主内存啊。

    • 四火 说道:

      因为synchronized关键字,线程B检查发现instance确实不为空,所以就不进入同步块,这没有问题,然而,线程B得到的instance却未必是一个已经完全实例化了的T对象,换言之,并没有任何途径保证“new T()”happens-before“给instance的赋值”。

      如果你还是无法理解,可以看看这篇文章 http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html 帮助你理解

      • luchy0120 说道:

        这个问题我理解了,是因为A线程的解锁操作和B线程对instance的第一次检查(同
        步块之前的)之间没有happens-before关系,线程A虽然执行了刷回主内存的操作,
        但是线程B无法保证能读到正确的。因为脱离了监视器之间的happens-before约束。

  2. luchy0120 说道:

    class T {
        private static volatile T instance;
     
        public static T getInstance() {
            if (null == instance) {
                synchronized (T.class) {
                    if (null == instance) {
                        instance = new T();
                    }
                }
            }
            return instance;
        }
    }
     
     
    如果是以上写法,是安全发布么?
    instance = new T();
    这一句是不是相当于
    T temp = new T();
    instance = temp;
    呢?
    instance = new T();不是一个原子写操作,会有问题么?

    • 四火 说道:

      是安全发布。就是因为volatile保证了“new T()”必定happens-before于“对instance的赋值”。

      T temp=new T(); instance=temp; 这一小段确实不是原子操作,但是,因为instance有volatile修饰,那么无论赋值前怎么个执行顺序,都一定happens-before于给instance赋值这一步之前,因此是没有问题的。

      • luchy0120 说道:

        为什么volatile能保证 new T()必定happens-before 于“对象instance的赋值”?volatile只是保证每次的读操作都能读到最后写的内容。写成T temp = new T();instance = temp;是因为可以利用Each action in a thread happens-before every subsequent action in that thread。再利用valotile的写happens-before读。可以参考http://www.infoq.com/cn/articles/java-memory-model-4。
         
         
         
         

  3. engineer 说道:

    final 怎么从jmm角度理解?没明白final是怎么保证可见性的。如果final的是一个对象呢?final保证引用不变,但如何保证属性值发生变化时的可见性呢?

    • 四火 说道:

      请阅读规范,这是我的理解:

      final假定你的对象是事实不可变的,并且保证在构造方法返回的时候,取得的是正确构造完成的对象(也包括这个对象内部的引用对外真实可见);但是这以后的时间通过调用final修饰的对象的方法,引起final对象内部的改变,规范里面并没有说明(我没有找到),因此是无法保证的。

      when the object is seen by another thread, that thread will always see the correctly constructed version of that object’s final fields. It will also see versions of any object or array referenced by those final fields that are at least as up-to-date as the final fields are.

发表评论

电子邮件地址不会被公开。

您可以使用这些 HTML 标签和属性: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>


Preview on Feedage: