单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一
实现方式
一个类的对象的产生是由类构造函数来完成的。如果一个类对外提供了public的构造方法,那么外界就可以任意创建该类的对象。所以,如果想限制对象的产生,一个办法就是将构造函数变为私有的(至少是受保护的),使外面的类不能通过引用来产生对象。同时为了保证类的可用性,就必须提供一个自己的对象以及访问这个对象的静态方法。(至于为什么是静态方法,我想了一下构造器是私有的,如果getInstance方法不是static,那么我们就没有办法在程序中调用getInstance方法了)
- 饿汉式
public class Singleton {
//在类内部实例化一个实例
private static Singleton instance = new Singleton();
//私有的构造函数,外部无法访问
private Singleton() {
}
//对外提供获取实例的静态方法
public static Singleton getInstance() {
return instance;
}
}
饿汉式的问题:,在类被加载的时候对象就会实例化。这也许会造成不必要的消耗,因为有可能这个实例根本就不会被用到。而且,如果这个类被多次加载的话也会造成多次实例化。其实解决这个问题的方式有很多,下面提供两种解决方式,第一种是使用静态内部类的形式。第二种是使用懒汉式。
- 静态内部类式
public class StaticInnerClassSingleton {
//在静态内部类中初始化实例对象
private static class SingletonHolder {
private static final StaticInnerClassSingleton INSTANCE = new StaticInnerClassSingleton();
}
//私有的构造方法
private StaticInnerClassSingleton() {
}
//对外提供获取实例的静态方法
public static final StaticInnerClassSingleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
该种方式:这种方式同样利用了classloder的机制来保证初始化instance时只有一个线程,它跟饿汉式不同的是(很细微的差别):饿汉式是只要Singleton类被装载了,那么instance就会被实例化(没有达到lazy loading效果),而这种方式是Singleton类被装载了,instance不一定被初始化。因为SingletonHolder类没有被主动使用(主动使用是一个关键点),只有显示通过调用getInstance方法时,才会显示装载SingletonHolder类,从而实例化instance。
- 懒汉式
//线程不安全的
public class Singleton {
//定义实例
private static Singleton instance;
//私有构造方法
private Singleton(){}
//对外提供获取实例的静态方法
public static Singleton getInstance() {
//在对象被使用的时候才实例化
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
//线程安全版本1
public class SynchronizedSingleton {
//定义实例
private static SynchronizedSingleton instance;
//私有构造方法
private SynchronizedSingleton(){}
//对外提供获取实例的静态方法,对该方法加锁
public static synchronized SynchronizedSingleton getInstance() {
//在对象被使用的时候才实例化
if (instance == null) {
instance = new SynchronizedSingleton();
}
return instance;
}
}
//synchronized对整个方法同步,导致效率不是很高!,有的时候不会进入if判断,锁的范围太大
//线程安全版本2,减小了锁的范围
public class Singleton {
private static Singleton singleton;
private Singleton() {
}
public static Singleton getSingleton() {
if (singleton == null) { //s2
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton(); //s1
}
}
}
return singleton;
}
}
线程安全版本2,也存在一定问题,就是与java内存模型有关!
。考虑下面的事件序列:线程A发现变量没有被初始化, 然后它获取锁并开始变量的初始化。
由于某些编程语言的语义,编译器生成的代码允许在线程A执行完变量的初始化之前,更新变量并将其指向部分初始化的对象。线程B发现共享变量已经被初始化,并返回变量。由于线程B确信变量已被初始化,它没有获取锁。如果在A完成初始化之前共享变量对B可见(这是由于A没有完成初始化或者因为一些初始化的值还没有穿过B使用的内存(缓存一致性)),程序很可能会崩溃。
总结成一句话,就是如果线程A在S1处初始化的时候,发生了线程切换,B线程在S2处看到单例已经初始化了,就直接返回单例,但是这个时候线程B看到的是不完成的单例,因为A线程还没有完成初始化
在JSR303内存模型升级后,加强了volatile的语义,保证写在读之前的可见性,之后这样的代码比较好
public class VolatileSingleton {
private static volatile VolatileSingleton singleton;
private VolatileSingleton() {
}
public static VolatileSingleton getSingleton() {
if (singleton == null) {
synchronized (VolatileSingleton.class) {
if (singleton == null) {
singleton = new VolatileSingleton();
}
}
}
return singleton;
}
}
//但存在序列化破坏单例模式,下面的代码比较好,自定义了序列化的过程
public class Singleton implements Serializable{
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
private Object readResolve() {
return singleton;
}
}
- 枚举类
public enum T {
SPRING,SUMMER,AUTUMN,WINTER;
}
这个貌似是最令人惊讶的,枚举不用线程安全和序列化的问题
- 线程安全:
反编译后代码如下:当一个Java类第一次被真正使用到的时候静态资源被初始化、Java类的加载和初始化过程都是线程安全的(因为虚拟机在加载枚举的类的时候,会使用ClassLoader的loadClass方法,而这个方法使用同步代码块保证了线程安全)。所以,创建一个enum类型是线程安全的。
public final class T extends Enum
{
//省略部分内容
public static final T SPRING;
public static final T SUMMER;
public static final T AUTUMN;
public static final T WINTER;
private static final T ENUM$VALUES[];
static
{
SPRING = new T("SPRING", 0);
SUMMER = new T("SUMMER", 1);
AUTUMN = new T("AUTUMN", 2);
WINTER = new T("WINTER", 3);
ENUM$VALUES = (new T[] {
SPRING, SUMMER, AUTUMN, WINTER
});
}
}
- 序列化问题:
在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.Enum的valueOf方法来根据名字查找枚举对象。同时,编译器是不允许任何对这种序列化机制的定制的,因此禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法。
普通的Java类的反序列化过程中,会通过反射调用类的默认构造函数来初始化对象。所以,即使单例中构造函数是私有的,也会被反射给破坏掉。由于反序列化后的对象是重新new出来的,所以这就破坏了单例。
参考博文:Hoils大神的单例