论坛首页 Java企业应用论坛

不使用volatile,安全DCL式singleton写法

浏览 6385 次
精华帖 (0) :: 良好帖 (0) :: 新手帖 (0) :: 隐藏帖 (0)
作者 正文
   发表时间:2013-12-18  
iamiwell 写道
qiaoenxin 写道
iamiwell 写道
qiaoenxin 写道
iamiwell 写道
看来楼主还是不理解为什么要加volatile,简单说,你所说的那种方式,理论上也不是线程安全的,注意new的几个过程

看来是你没有真正理解new的过程。

A a = new A();
...//注意后面代码
new的过程只会影响a的赋值先后顺序,这是因为new的过程不是一个原子造成的。但是不管new的过程有多少,new语句后面的代码都不会在new的过程结束之前执被行。
另外还必须考虑虚拟机优化问题,这里就不仔细说了。


   ...
   20: new #3; //class Singleton
   23: dup
   24: invokespecial #4; //Method "<init>":()V
   <assigns>
   ...

new执行之后,this地址即已经可以使用了,但是构造工作并没有全部完成,在这种情况下,
不管怎么去判断和赋值,都不能保证最终拿到的instance是已经构造好了

至于你的try,这个是防止java编译的优化,而“但是不管new的过程有多少,new语句后面的代码都不会在new的过程结束之前执被行。 ”这句话,是没有根据的,其实你描述的就是一个屏障,没有规范说每个构造器都必须添加屏障,这也是为什么可以使用volatile或final,因为它们就是一个屏障,正如你所说,你也认同构造的过程并不是原子的么

总的来说,理论上这种方式不是线程安全的,但实际中,有可能某些处理器的特殊性,能够保证它的线程安全,或者发生线程安全问题的场景基本不可见

也可以看看类似的文章
http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html
or
http://ifeve.com/double-checked-locking-with-delay-initialization/


你的问题7楼已经给你解释了。你需要注意temp变量的作用。

至于你给出的文章,我刚学java的时候就已经看过内容相似的文章。起初我也和你的想法一样,后来相关文章也拜读了不少,才发现那篇文章的问题。

对于A a = new A(); 这个过程大致做了3件事情
1.给实例分配内存。

2.初始实例的构造器

3.将变量指向实例对象的内存空间。
我姑且称为new的三个过程为一个执行单元unit(1,2,3)。虚拟机总是尽可能多的将一个执行单元中可以并发执行的元素(1,2,3)同时提交给cpu乱序执行,提高执行效率。

哎,没什么话好说了,请注意,已经拿到this的地址了,当然可以直接给temp,这个时候temp也不是null,也可以赋值给singleton,但这整个过程中,都不能保证构造函数执行完成,这种问题其实讨论的也没什么意思,我只是好心指出来而已

按照你的说法
A a = new A();
a.method();//按照你的说法这里不能保证a的构造函数执行完成 a.method()是要报空指针异常了?

如果真如你说,如上代码等于如下代码

a.method();
A a = new A();
先new实例再使用还有意义?你的程序随时的都报异常了。

你好好理解我说的一个执行单元的概念,一个执行单元没有结束之前下执行单元是不会执行的。
0 请登录后投票
   发表时间:2013-12-18   最后修改:2013-12-18
iamiwell 写道
qiaoenxin 写道
iamiwell 写道
qiaoenxin 写道
iamiwell 写道
看来楼主还是不理解为什么要加volatile,简单说,你所说的那种方式,理论上也不是线程安全的,注意new的几个过程

看来是你没有真正理解new的过程。

A a = new A();
...//注意后面代码
new的过程只会影响a的赋值先后顺序,这是因为new的过程不是一个原子造成的。但是不管new的过程有多少,new语句后面的代码都不会在new的过程结束之前执被行。
另外还必须考虑虚拟机优化问题,这里就不仔细说了。


   ...
   20: new #3; //class Singleton
   23: dup
   24: invokespecial #4; //Method "<init>":()V
   <assigns>
   ...

new执行之后,this地址即已经可以使用了,但是构造工作并没有全部完成,在这种情况下,
不管怎么去判断和赋值,都不能保证最终拿到的instance是已经构造好了

至于你的try,这个是防止java编译的优化,而“但是不管new的过程有多少,new语句后面的代码都不会在new的过程结束之前执被行。 ”这句话,是没有根据的,其实你描述的就是一个屏障,没有规范说每个构造器都必须添加屏障,这也是为什么可以使用volatile或final,因为它们就是一个屏障,正如你所说,你也认同构造的过程并不是原子的么

总的来说,理论上这种方式不是线程安全的,但实际中,有可能某些处理器的特殊性,能够保证它的线程安全,或者发生线程安全问题的场景基本不可见

也可以看看类似的文章
http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html
or
http://ifeve.com/double-checked-locking-with-delay-initialization/


你的问题7楼已经给你解释了。你需要注意temp变量的作用。

至于你给出的文章,我刚学java的时候就已经看过内容相似的文章。起初我也和你的想法一样,后来相关文章也拜读了不少,才发现那篇文章的问题。

对于A a = new A(); 这个过程大致做了3件事情
1.给实例分配内存。

2.初始实例的构造器

3.将变量指向实例对象的内存空间。
我姑且称为new的三个过程为一个执行单元unit(1,2,3)。虚拟机总是尽可能多的将一个执行单元中可以并发执行的元素(1,2,3)同时提交给cpu乱序执行,提高执行效率。

哎,没什么话好说了,请注意,已经拿到this的地址了,当然可以直接给temp,这个时候temp也不是null,也可以赋值给singleton,但这整个过程中,都不能保证构造函数执行完成,这种问题其实讨论的也没什么意思,我只是好心指出来而已


你再仔细想想吧。
try 块前的代码 和 try块后的代码 会发生指令重排吗!?
楼主都已经在注释里指出了,这里用个try就是专门为了避免由虚拟机激进优化而产生的CPU指令重排。
你看看仔细。

据我目前的知识,JVM现在还不能做逃逸分析,有try就不会在发生重排。
如果你认为try block无法阻止指令重排的话,请给出证据。

间接证据:
if (temp != null) 是在运行线程内,如果像你说的重排发生,会破坏线程内程序顺序执行的逻辑。
线程内程序顺序执行是JVM的基本保障,跟多线程可见性什么的都没关系了。
0 请登录后投票
   发表时间:2013-12-18  
beowulf2005 写道
iamiwell 写道
qiaoenxin 写道
iamiwell 写道
qiaoenxin 写道
iamiwell 写道
看来楼主还是不理解为什么要加volatile,简单说,你所说的那种方式,理论上也不是线程安全的,注意new的几个过程

看来是你没有真正理解new的过程。

A a = new A();
...//注意后面代码
new的过程只会影响a的赋值先后顺序,这是因为new的过程不是一个原子造成的。但是不管new的过程有多少,new语句后面的代码都不会在new的过程结束之前执被行。
另外还必须考虑虚拟机优化问题,这里就不仔细说了。


   ...
   20: new #3; //class Singleton
   23: dup
   24: invokespecial #4; //Method "<init>":()V
   <assigns>
   ...

new执行之后,this地址即已经可以使用了,但是构造工作并没有全部完成,在这种情况下,
不管怎么去判断和赋值,都不能保证最终拿到的instance是已经构造好了

至于你的try,这个是防止java编译的优化,而“但是不管new的过程有多少,new语句后面的代码都不会在new的过程结束之前执被行。 ”这句话,是没有根据的,其实你描述的就是一个屏障,没有规范说每个构造器都必须添加屏障,这也是为什么可以使用volatile或final,因为它们就是一个屏障,正如你所说,你也认同构造的过程并不是原子的么

总的来说,理论上这种方式不是线程安全的,但实际中,有可能某些处理器的特殊性,能够保证它的线程安全,或者发生线程安全问题的场景基本不可见

也可以看看类似的文章
http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html
or
http://ifeve.com/double-checked-locking-with-delay-initialization/


你的问题7楼已经给你解释了。你需要注意temp变量的作用。

至于你给出的文章,我刚学java的时候就已经看过内容相似的文章。起初我也和你的想法一样,后来相关文章也拜读了不少,才发现那篇文章的问题。

对于A a = new A(); 这个过程大致做了3件事情
1.给实例分配内存。

2.初始实例的构造器

3.将变量指向实例对象的内存空间。
我姑且称为new的三个过程为一个执行单元unit(1,2,3)。虚拟机总是尽可能多的将一个执行单元中可以并发执行的元素(1,2,3)同时提交给cpu乱序执行,提高执行效率。

哎,没什么话好说了,请注意,已经拿到this的地址了,当然可以直接给temp,这个时候temp也不是null,也可以赋值给singleton,但这整个过程中,都不能保证构造函数执行完成,这种问题其实讨论的也没什么意思,我只是好心指出来而已


你再仔细想想吧。
try 块前的代码 和 try块后的代码 会发生指令重排吗!?
楼主都已经在注释里指出了,这里用个try就是专门为了避免由虚拟机激进优化而产生的CPU指令重排。
你看看仔细。

据我目前的知识,JVM现在还不能做逃逸分析,有try就不会在发生重排。
如果你认为try block无法阻止指令重排的话,请给出证据。

间接证据:
if (temp != null) 是在运行线程内,如果像你说的重排发生,会破坏线程内程序顺序执行的逻辑。
线程内程序顺序执行是JVM的基本保障,跟多线程可见性什么的都没关系了。

变成口水仗了,关键在于什么情况下是可以重排的,什么情况是不可以重排的,对于有数据依赖的是不可以重排,或者主动添加屏障,temp和singleton只是依赖对象的地址,用于赋值,它并没有依赖其它,而构造器也只是依赖对象地址,最终这两者没有数据依赖,理论上是可以重排的,至于说的try,catch,难道对于try, catch,有屏障的作用么,貌似还没听说过,也许我孤陋寡闻了
0 请登录后投票
   发表时间:2013-12-18   最后修改:2013-12-18
iamiwell 写道
beowulf2005 写道
iamiwell 写道
qiaoenxin 写道
iamiwell 写道
qiaoenxin 写道
iamiwell 写道
看来楼主还是不理解为什么要加volatile,简单说,你所说的那种方式,理论上也不是线程安全的,注意new的几个过程

看来是你没有真正理解new的过程。

A a = new A();
...//注意后面代码
new的过程只会影响a的赋值先后顺序,这是因为new的过程不是一个原子造成的。但是不管new的过程有多少,new语句后面的代码都不会在new的过程结束之前执被行。
另外还必须考虑虚拟机优化问题,这里就不仔细说了。


   ...
   20: new #3; //class Singleton
   23: dup
   24: invokespecial #4; //Method "<init>":()V
   <assigns>
   ...

new执行之后,this地址即已经可以使用了,但是构造工作并没有全部完成,在这种情况下,
不管怎么去判断和赋值,都不能保证最终拿到的instance是已经构造好了

至于你的try,这个是防止java编译的优化,而“但是不管new的过程有多少,new语句后面的代码都不会在new的过程结束之前执被行。 ”这句话,是没有根据的,其实你描述的就是一个屏障,没有规范说每个构造器都必须添加屏障,这也是为什么可以使用volatile或final,因为它们就是一个屏障,正如你所说,你也认同构造的过程并不是原子的么

总的来说,理论上这种方式不是线程安全的,但实际中,有可能某些处理器的特殊性,能够保证它的线程安全,或者发生线程安全问题的场景基本不可见

也可以看看类似的文章
http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html
or
http://ifeve.com/double-checked-locking-with-delay-initialization/


你的问题7楼已经给你解释了。你需要注意temp变量的作用。

至于你给出的文章,我刚学java的时候就已经看过内容相似的文章。起初我也和你的想法一样,后来相关文章也拜读了不少,才发现那篇文章的问题。

对于A a = new A(); 这个过程大致做了3件事情
1.给实例分配内存。

2.初始实例的构造器

3.将变量指向实例对象的内存空间。
我姑且称为new的三个过程为一个执行单元unit(1,2,3)。虚拟机总是尽可能多的将一个执行单元中可以并发执行的元素(1,2,3)同时提交给cpu乱序执行,提高执行效率。

哎,没什么话好说了,请注意,已经拿到this的地址了,当然可以直接给temp,这个时候temp也不是null,也可以赋值给singleton,但这整个过程中,都不能保证构造函数执行完成,这种问题其实讨论的也没什么意思,我只是好心指出来而已


你再仔细想想吧。
try 块前的代码 和 try块后的代码 会发生指令重排吗!?
楼主都已经在注释里指出了,这里用个try就是专门为了避免由虚拟机激进优化而产生的CPU指令重排。
你看看仔细。

据我目前的知识,JVM现在还不能做逃逸分析,有try就不会在发生重排。
如果你认为try block无法阻止指令重排的话,请给出证据。

间接证据:
if (temp != null) 是在运行线程内,如果像你说的重排发生,会破坏线程内程序顺序执行的逻辑。
线程内程序顺序执行是JVM的基本保障,跟多线程可见性什么的都没关系了。

变成口水仗了,关键在于什么情况下是可以重排的,什么情况是不可以重排的,对于有数据依赖的是不可以重排,或者主动添加屏障,temp和singleton只是依赖对象的地址,用于赋值,它并没有依赖其它,而构造器也只是依赖对象地址,最终这两者没有数据依赖,理论上是可以重排的,至于说的try,catch,难道对于try, catch,有屏障的作用么,貌似还没听说过,也许我孤陋寡闻了


纠结其实就在这句
由于try的存在,虚拟机无法优化temp!=null 是否为true

对于Java 6之前JIT,成立还是不成立,你我都没源码,也没证据。

如果如楼主所言,JIT放弃优化,那么也没内存屏障什么事儿了。

我认为,我写编译器的话,就不会考虑在遇到分支语句时做过于激进的优化。
因为有分支存在,重排指令顺序对性能的提升,恐怕抵不过一次错误的预判对性能的损失。

而且这种只跑一次的代码块,基本上没可能被JIT选上,并优化。
0 请登录后投票
   发表时间:2013-12-18  
使用volatile也没用。volatile只是对原子操作的并发有效。

LZ的try catch 或许有效,代码可读性貌似差了点。
// 延迟方法1
public class Singleton {

	private static Singleton instance;
	
	private static boolean init = false;
	
	private Singleton() {
	};

	/**
	 * @return
	 */
	public static Singleton getInstance() {
		if ( !init ) {
			synchronized (Singleton.class) {
				if ( !init ) {
					instance = new Singleton();
					init = true;
				}
			}
		}
		return instance;
	}
}

// 延迟方法2
public class Singleton {

	private static Singleton instance;
	
	private Singleton() {
	};

	/**
	 * @return
	 */
	private static class SingletonHolder {
		public final static Singleton instance = new Singleton();
	}

	public static Singleton getInstance() {
		return SingletonHolder.instance;
	}

}
0 请登录后投票
   发表时间:2013-12-25  
samm 写道
使用volatile也没用。volatile只是对原子操作的并发有效。

LZ的try catch 或许有效,代码可读性貌似差了点。
// 延迟方法1
public class Singleton {

	private static Singleton instance;
	
	private static boolean init = false;
	
	private Singleton() {
	};

	/**
	 * @return
	 */
	public static Singleton getInstance() {
		if ( !init ) {
			synchronized (Singleton.class) {
				if ( !init ) {
					instance = new Singleton();
					init = true;
				}
			}
		}
		return instance;
	}
}

// 延迟方法2
public class Singleton {

	private static Singleton instance;
	
	private Singleton() {
	};

	/**
	 * @return
	 */
	private static class SingletonHolder {
		public final static Singleton instance = new Singleton();
	}

	public static Singleton getInstance() {
		return SingletonHolder.instance;
	}

}



第一个不加 volatile 是错误的,因为多个线程中的init可能并不是一个值(不能保证可见性)。
其实JAVA的双重检查加锁实现的单例模式已经是不建议使用的方法了。

第二个方法是《Java 并发编程实战》建议使用的方法。
0 请登录后投票
   发表时间:2013-12-29  
// 延迟方法1 
public class Singleton { 
 
    private static Singleton instance; 

----------
确实漏了volatile。
0 请登录后投票
论坛首页 Java企业应用版

跳转论坛:
Global site tag (gtag.js) - Google Analytics