KandQ:单例模式的七种写法及其相关问题解析

@改进自:http://cantellow.iteye.com/blog/838473

设计模式中的单例模式可以有7种写法,这7种写法有各自的优点和缺点。

代码示例(java)及其分析如下:

一、懒汉式

public class Singleton

{

private static Singleton singleton;

private Singleton(){}

public static Singleton getInstance()

{

if(singleton==null)

singleton=new Singleton();

return singleton;

}

}

}

优点不是马上就初始化的,当需要使用的时候才进行初始化(即是lazy loading)

缺点在并发情况下是线程不安全

二、懒汉式线程安全版

public class Singleton

{
private static Singleton singleton;

private Singleton(){}

public synchronized static Singleton getInstance()

{

if(singleton==null)

singleton=new Singleton();

return singleton;

}
}

 

优点:不是类加载之后就进行初始化的,当需要使用的时候才进行初始化(即是lazy loading),且为线程安全的

缺点:效率低,加了synchronized进行同步之后,效率上有所降低

三、饿汉式

public class Singleton

{

private static Singleton singleton=new Singleton();

private Singleton(){}

public static Singleton getInstance()

{

return singleton;

}

}

 

这种方式基于classloder机制避免了多线程的同步问题,不过,instance在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用getInstance方法,但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化instance显然没有达到lazy loading的效果。其一个明显的好处就是是线程安全的

四、饿汉式的变种写法

public class Singleton

{

private static Singleton singleton;

static

{

singleton=new Singleton();

}

private Singleton(){}

public static Singleton getInstance()

{

return singleton;

}

}

其会在类加载的时候就进行加载。和上面的饿汉式的写法优缺点相同

五、静态内部类方式

public class Singleton

{

private Singleton(){}

private static class SingletonHolder

{

private static final Singleton singleton=new Singleton();

}

public static Singleton getInstance()

{

return SingletonHolder.singleton;

}

}

这种方式同样利用了classloder的机制来保证初始化instance时只有一个线程,它跟第三种和第四种方式不同的是(很细微的差别): 第三种和第四种方式是只要Singleton类被装载了,那么instance就会被实例化(没有达到lazy loading效果),而这种方式是Singleton类被装载了,instance不一定被初始化。因为SingletonHolder类没有被主动使用,只有显示通过调用getInstance方法时,才会显示装载SingletonHolder类,从而实例化instance。想象一下,如果实例化instance很消耗资源,我想让他延迟加载,另外一方面,我不希望在Singleton类加载时就实例化,因为我不能确保Singleton类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化instance显然是不合适的。这个时候,这种方式相比第三和第四种方式就显得很合理。

六、采用枚举方式

public enum Singletons

{

INSTANCE;

//此处表示单例对象里面的各种方法

public void Method()

{

}

}

Effective Java作者Josh Bloch提倡使用枚举的方式去实现单例模式。因为它不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象,同时写法简单。对于枚举方式创建单例,为何可以避免多线程的同步以及防止反序列化重新创建新的对象这个原因,详见相关博文:枚举的线程安全性及序列化问题

七、双重校验锁

public class Singleton

{

private volatile static Singleton singleton;

private Singleton(){}

public static Singleton getInstance()

{

if (singleton == null)

{

synchronized (Singleton.class)

{

if (singleton == null)

{

singleton = new Singleton();

}

}

}

return singleton;

}

}

对于双重校验锁,其是对懒汉式线程安全版的改进,其目的在于减少同步所用的开销。对singleton进行两次null检查,一次是在同步块外,一次是在同步块内。为什么在同步块内还要再检验一次?因为可能会有多个线程一起进入同步块外的 if,如果在同步块内不进行二次检验的话就会生成多个实例。

singleton变量使用volatile关键字的原因是,instance = new Singleton()这句,并非是一个原子操作,事实上在 JVM中这句话大概做了下面 3件事情:
instance 分配内存
调用 Singleton 的构造函数来初始化成员变量
instance对象指向分配的内存空间(执行完这步instance 就为非null 了)
但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是1-2-3 也可能是1-3-2。如果是后者,则在3 执行完毕、2未执行之前,被线程二抢占了,这时 instance已经是非 null了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地jvm就会报错。有些人认为使用volatile 的原因是可见性,也就是可以保证线程在本地不会存有instance 的副本,每次都是去主内存中读取。但其实是不对的。使用volatile 的主要原因是其另一个特性:禁止指令重排序优化。也就是说,在volatile 变量的赋值操作后面会有一个内存屏障(生成的汇编代码上),读操作不会被重排序到内存屏障之前。比如上面的例子,取操作必须在执行完1-2-3 之后或者1-3-2 之后,不存在执行到1-3 然后取到值的情况。从「先行发生原则」的角度理解的话,就是对于一个volatile 变量的写操作都先行发生于后面对这个变量的读操作(这里的“后面”是时间上的先后顺序)。

对于第一种和第二种写法,实际上其可以归类为懒汉式这一种写法,对于第三种和第四种,其也可以归为饿汉式这一种写法。为此,一般单例都是五种写法。懒汉,饿汉,双重校验锁,枚举和静态内部类

对于单例模式,其有两个问题需要注意:

1.如果单例由不同的类装载器装入,那便有可能存在多个单例类的实例。假定不是远端存取,例如一些servlet容器对每个servlet使用完全不同的类装载器,这样的话如果有两个servlet访问一个单例类,它们就都会有各自的实例。

2.如果Singleton实现了java.io.Serializable接口,那么这个类的实例就可能被序列化和复原。不管怎样,如果你序列化一个单例类的对象,接下来复原多个那个对象,那你就会有多个单例类的实例。

对第一个问题修复的办法是:

private static Class getClass(String classname)throws ClassNotFoundException

{

//获取当前执行线程的上下文类加载器

ClassLoader classLoader = Thread.currentThread().getContextClassLoader();

if (classLoader == null)

classLoader = Singleton.class.getClassLoader();

return (classLoader.loadClass(classname));

}

对第二个问题修复的办法是:

class Singletones implements java.io.Serializable

{     

   private static Singletones INSTANCE = new Singletones();  

   private Singletones()

   {          

   }

   public static  Singletones getInstance()

   {

   return INSTANCE;

   }

/*

我们反序列化后获得的并不是原来的对象,而是经过重构的新的对象实例。

    ObjectInputStream对象在反序列化的时候,会在从I/O流中读取对象时,调用readResolve()方法。实际上就是用readResolve()中返回的对象直接替换在反序列化过程中重构的对象。

*/

   private Object readResolve()

   {

        return Singletones.getInstance();     

   }

}   

原因详见博文:Serializable或者Externalizable对象的序列化

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值