以下例子参考[url]http://developer.51cto.com/art/201104/256239.htm[/url]
网上闲逛时发现一篇博文,讲的是单例同步锁时失败的可能,提到的错误自己基本都忽略了,下面以其中的例子说一下自己的理解。
单例模式是比较简单直接的:
这也是在初学单例模式时会提到的一个采用延迟加载实现的例子,实际上这个例子并不能“保证”单例,在多线程高并发的情况下,如果瞬间同时初始访问getInstance方法,返回的可能就是不同的实例。于是就有了下面的简单的同步机制:
这种方式保证了线程安全,但是我们只需要在生成实例的过程中保证其同步即可,不需要对访问也进行加锁。很明显,简单的在类实例范围内对其加锁,性能是其瓶颈。为了避免这种情况,便会有下面的想法:
其实这样便可以保证单例了,但是却保证不了单例的准确性。也就是说不同的线程可以拿到同一个实例,但是在某些情况下,会取到实例的错误状态。在本例中,初始化Singleton对象和将对象地址赋给instance的顺序是不确定的。也即可能有以下两种情况:
1、在初始化之前将对象引用赋给instance。这时,instance有了实际的引用,但对象还没有初始化。其他线程此时如果获取到instance,可能就是只有引用而还没有初始化的实例。
2、在初始化之后将对象引用赋给instance。这是正确的状态,保证了单例也保证了正确性。
这种不确定性不能“保证”单例的正确性。再看下面的改进方案:
这种方式制造了一个内存屏障,即使用了一个临时变量来保证“初始化操作和赋引用操作”的原子性。这种方式从代码方面看已经没有问题了,但是注意到在同步语句块之外的instance=temp,根据博文的解释:[color=red]同步语句块内的操作必须在语句块结束之前完成,但是代码中同步块之外的操作有可能被编译器放到块内执行(只是存在这种可能性)。[/color]这一点细节确实不容易考虑到,需要了解一些JVM的知识,这方面没什么研究,保留意见。
看最后一种方式:
这里要说一下volatile关键字,这个除了在一定程度上确保同步之外,JDK1.5扩充了volatile语义,简单点说就是保证了对象初始化以及赋引用的有序性,在本例来说就是先初始化后赋引用。这样两层机制保证了单例的唯一性和准确性。
网上闲逛时发现一篇博文,讲的是单例同步锁时失败的可能,提到的错误自己基本都忽略了,下面以其中的例子说一下自己的理解。
单例模式是比较简单直接的:
public class Singleton {
private static Singleton instance = null;
private Singleton(){}
public static Singleton getInstance() {
if(instance == null) {
instance = new Singleton();
}
return instance;
}
}
这也是在初学单例模式时会提到的一个采用延迟加载实现的例子,实际上这个例子并不能“保证”单例,在多线程高并发的情况下,如果瞬间同时初始访问getInstance方法,返回的可能就是不同的实例。于是就有了下面的简单的同步机制:
public class Singleton {
private static Singleton instance = null;
private Singleton(){}
public synchronized static Singleton getInstance() {
if(instance == null) {
instance = new Singleton();
}
return instance;
}
}
这种方式保证了线程安全,但是我们只需要在生成实例的过程中保证其同步即可,不需要对访问也进行加锁。很明显,简单的在类实例范围内对其加锁,性能是其瓶颈。为了避免这种情况,便会有下面的想法:
public class Singleton {
private static Singleton instance = null;
private Singleton(){}
public static Singleton getInstance() {
if(instance == null) {
synchronized(Singleton.class) {
if(instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
其实这样便可以保证单例了,但是却保证不了单例的准确性。也就是说不同的线程可以拿到同一个实例,但是在某些情况下,会取到实例的错误状态。在本例中,初始化Singleton对象和将对象地址赋给instance的顺序是不确定的。也即可能有以下两种情况:
1、在初始化之前将对象引用赋给instance。这时,instance有了实际的引用,但对象还没有初始化。其他线程此时如果获取到instance,可能就是只有引用而还没有初始化的实例。
2、在初始化之后将对象引用赋给instance。这是正确的状态,保证了单例也保证了正确性。
这种不确定性不能“保证”单例的正确性。再看下面的改进方案:
public class Singleton {
private static Singleton instance = null;
private Singleton(){}
public static Singleton getInstance() {
if(instance == null) {
Singleton temp;
synchronized(Singleton.class) {
temp = instance;
if(temp == null) {
synchronized(Singleton.class) {
temp = new Singleton();
}
instance = temp;
}
}
}
return instance;
}
}
这种方式制造了一个内存屏障,即使用了一个临时变量来保证“初始化操作和赋引用操作”的原子性。这种方式从代码方面看已经没有问题了,但是注意到在同步语句块之外的instance=temp,根据博文的解释:[color=red]同步语句块内的操作必须在语句块结束之前完成,但是代码中同步块之外的操作有可能被编译器放到块内执行(只是存在这种可能性)。[/color]这一点细节确实不容易考虑到,需要了解一些JVM的知识,这方面没什么研究,保留意见。
看最后一种方式:
public class Singleton {
private static volatile Singleton instance = null;
private Singleton(){}
public static Singleton getInstance() {
if(instance == null) {
synchronized(Singleton.class) {
if(instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
这里要说一下volatile关键字,这个除了在一定程度上确保同步之外,JDK1.5扩充了volatile语义,简单点说就是保证了对象初始化以及赋引用的有序性,在本例来说就是先初始化后赋引用。这样两层机制保证了单例的唯一性和准确性。