设计模式(一):单例模式
单例模式是一种常用的软件设计模式,其定义是单例对象的类只能允许一个实例存在。
单例模式一般体现在类声明中,单例的类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
适用场合:
- 需要频繁的进行创建和销毁的对象;
- 创建对象时耗时过多或耗费资源过多,但又经常用到的对象;
- 工具类对象;
- 频繁访问数据库或文件的对象。
比如:许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息。这种方式简化了在复杂环境下的配置管理。
优点:
- 在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如网站首页页面缓存)。
- 避免对资源的多重占用(比如写文件操作)。
二、实现方式
1、普通饿汉式(线程安全,不能延时加载)
所谓饿汉。这是个比较形象的比喻。对于一个饿汉来说,他希望他想要用到这个实例的时候就能够立即拿到,而不需要任何等待时间。
public class Singleton { private final static Singleton INSTANCE = new Singleton(); private Singleton(){} public static Singleton getInstance(){ return INSTANCE; } }
优点:写法简单 线程安全
通过static
的静态初始化方式,在该类第一次被加载的时候,就有一个SimpleSingleton
的实例被创建出来了。这样就保证在第一次想要使用该对象时,他已经被初始化好了。
同时,由于该实例在类被加载的时候就创建出来了,所以也避免了线程安全问题。
JVM类加载机制中:
“ 并发:
虚拟机会保证一个类的类构造器<clinit>()在多线程环境中被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的类构造器<clinit>(),其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。
特别需要注意的是,在这种情形下,其他线程虽然会被阻塞,但如果执行<clinit>()方法的那条线程退出后,其他线程在唤醒之后不会再次进入/执行<clinit>()方法,因为在同一个类加载器下,一个类型只会被初始化一次。 ”
缺点:在类装载的时候就完成实例化,没有达到Lazy Loading的效果。
在类被加载的时候对象就会实例化。这也许会造成不必要的消耗,因为有可能这个实例根本就不会被用到。
想象一下,如果实例化instance
很消耗资源,我想让他延迟加载,另外一方面,我不希望在Singleton
类加载时就实例化,因为我不能确保Singleton
类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化instance
显然是不合适的。
解决不能Lazy Loading懒加载问题的办法:第一种是使用静态内部类的形式。第二种是使用懒汉式。下文会介绍。
2、静态代码块饿汉式(线程安全,不能延时加载)
public class Singleton { private static Singleton instance; static { instance = new Singleton(); } private Singleton() {} public static Singleton getInstance() { return instance; } }
和第一种一样,只不过将类实例化的过程放在了静态代码块中,也是在类装载的时候,就执行静态代码块中的代码,初始化类的实例。
3、静态内部类(线程安全,延迟加载,效率高)
public class Singleton { private Singleton() {} private static class SingletonInstance { private static final Singleton INSTANCE = new Singleton(); } public static Singleton getInstance() { return SingletonInstance.INSTANCE; } }
加载类 Singleton 时不会实例化对象,加载类 SingletonInstance 时才会实例化对象(也就是调用Singleton的getInstance方法时),实现了延迟加载。
关于类加载机制:JVM类加载机制
优点:线程安全,延迟加载,效率高。
4、枚举(线程安全,不能延时加载)
public enum Singleton { INSTANCE; public void whateverMethod() { } }
这种方式是Effective Java作者Josh Bloch
提倡的方式,它不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。
由于1.5中才加入enum
特性,用这种方式写不免让人感觉生疏,在实际工作中,我也很少看见有人这么写过,但是不代表他不好。
原理其实也是利用类加载机制实现线程安全。
反编译后:
public final class Singleton extends Enum<Singleton> { public static final Singleton INSTANCE = new Singleton("INSTANCE", 0); private static final Singleton[] $VALUES; public static Singleton[] values() { return (Singleton[])$VALUES.clone(); } public static Singleton valueOf(String string) { return Enum.valueOf(Singleton.class, string); } private Singleton(String string, int n) { super(string, n); } public void whateverMethod() { } static { $VALUES = new Singleton[]{INSTANCE}; } }
关于枚举原理:JDK源码学习笔记——Enum枚举使用及原理
优点:简单 线程安全
缺点:不能延迟加载 使用较少
5、普通懒汉式(线程不安全,可延时加载)
public class Singleton { private static Singleton singleton; private Singleton() {} public static Singleton getInstance() { if (singleton == null) { singleton = new Singleton(); } return singleton; } }
优点:可以实现延迟加载
缺点:线程不安全
多个线程可能同时进入if 中,创建出多个实例
6、synchronized 懒汉式(线程安全,可延时加载,效率低)
public class Singleton { private static Singleton singleton; private Singleton() {} public static synchronized Singleton getInstance() { if (singleton == null) { singleton = new Singleton(); } return singleton; } }
优点:可以实现延迟加载,线程安全
缺点:效率低
只有第一次创建实例的时候需要同步,其他情况都不需要。
我们知道synchronized是一个效率比较低的加锁方式,而每次获取实例都会同步加锁(本身不需要同步,直接返回 instance 即可),效率会很低。
7、双重校验锁懒汉式(线程安全,可延时加载,效率高)
详细可参考:Java并发(七):双重检验锁定DCL Java并发(二):Java内存模型
对于第六中方法进行优化,减小锁的粒度:
public class Singleton { private static Singleton singleton; Integer a; private Singleton(){} public static Singleton getInstance(){ if(singleton == null){ // 1 只有singleton==null时才加锁,性能好 synchronized (Singleton.class){ // 2 if(singleton == null){ // 3 singleton = new Singleton(); // 4 } } } return singleton; } }
会因为重排序出现问题:
线程A发现变量没有被初始化, 然后它获取锁并开始变量的初始化。
由于某些编程语言的语义,编译器生成的代码允许在线程A执行完变量的初始化之前,更新变量并将其指向部分初始化的对象。
线程B发现共享变量已经被初始化,并返回变量。由于线程B确信变量已被初始化,它没有获取锁。如果在A完成初始化之前共享变量对B可见(这是由于A没有完成初始化或者因为一些初始化的值还没有穿过B使用的内存(缓存一致性)),程序很可能会崩溃。
利用volatile限制重排序:
public class Singleton { private static volatile Singleton singleton; private Singleton() {} public static Singleton getInstance() { if (singleton == null) { synchronized (Singleton.class) { if (singleton == null) { singleton = new Singleton(); } } } return singleton; } }
三、单例与序列化
1、序列化对单例的破坏
双重检验锁实现单例:
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; } }
测试序列化对单例的影响:
public class SerializableDemo1 { //为了便于理解,忽略关闭流操作及删除文件操作。真正编码时千万不要忘记 //Exception直接抛出 public static void main(String[] args) throws IOException, ClassNotFoundException { //Write Obj to file ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("tempFile")); oos.writeObject(Singleton.getSingleton()); //Read Obj from file File file = new File("tempFile"); ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file)); Singleton newInstance = (Singleton) ois.readObject(); //判断是否是同一个对象 System.out.println(newInstance == Singleton.getSingleton()); } } //false
通过对Singleton的序列化与反序列化得到的对象是一个新的对象,这就破坏了Singleton的单例性。
2、分析
ois.readObject(); 调用的 readOrdinaryObject 方法
private Object readOrdinaryObject(boolean unshared) throws IOException { //此处省略部分代码 Object obj; try { obj = desc.isInstantiable() ? desc.newInstance() : null; } catch (Exception ex) { throw (IOException) new InvalidClassException( desc.forClass().getName(), "unable to create instance").initCause(ex); } //此处省略部分代码 if (obj != null && handles.lookupException(passHandle) == null && desc.hasReadResolveMethod()) { Object rep = desc.invokeReadResolve(obj); if (unshared && rep.getClass().isArray()) { rep = cloneArray(rep); } if (rep != obj) { handles.setObject(passHandle, obj = rep); } } return obj; }
isInstantiable
:如果一个serializable/externalizable的类可以在运行时被实例化,那么该方法就返回true。针对serializable和externalizable我会在其他文章中介绍。
desc.newInstance
:该方法通过反射的方式调用无参构造方法新建一个对象。
hasReadResolveMethod:
如果实现了serializable 或者 externalizable接口的类中包含readResolve
则返回true
invokeReadResolve:
通过反射的方式调用要被反序列化的类的readResolve方法。
原因:序列化会通过反射调用无参数的构造方法创建一个新的对象
解决:在Singleton中定义readResolve方法,并在该方法中指定要返回的对象的生成策略,就可以防止单例被破坏。
3、解决
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;
}
}
总结:一旦实现了Serializable接口之后,就不再是单例的了,因为,每次调用 readObject()方法返回的都是一个新创建出来的对象。解决办法就是使用readResolve()方法来避免此事发生。
四、关于枚举实现单例的序列化问题
为了保证枚举类型像Java规范中所说的那样,每一个枚举类型极其定义的枚举变量在JVM中都是唯一的,在枚举类型的序列化和反序列化上,Java做了特殊的规定:
在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.Enum的valueOf方法来根据名字查找枚举对象。同时,编译器是不允许任何对这种序列化机制的定制的,因此禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法。
所以,枚举实现的单例不会有序列化问题。