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

时间:2022-05-02 10:44:55

设计模式中的单例模式可以有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提倡使用枚举的方式去实现单例模式。因为它不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象,同时写法简单。对于枚举方式创建单例,为何可以避免多线程的同步以及防止反序列化重新创建新的对象这个原因,详见相关博文:K:枚举的线程安全性及其序列化问题

七、双重校验锁

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件事情:

  1. 给 instance 分配内存
  2. 调用 Singleton的构造函数来初始化成员变量
  3. 将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然后取到值的情况。从「先行发生原则」(即happen-before)的角度理解的话,就是对于一个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();
}
}

原因详见博文:K:java中序列化的两种方式—Serializable或Externalizable

回到目录|·(工)·)