单例模式 单例设计模式(Singleton Design Pattern)就是一个类只允许创建一个对象。
为什么要使用单例?
单例可以表示全局唯一,一些数据在系统中应该且只能保存一份,那就应该设计为单例类,例:配置类,全局计数器
处理资源访问冲突(如日志写文件如果多线程同时写就会导致覆盖问题)
如何获取一个单例 常见的单例获取方式大致可以分为懒汉式和饿汉式,除此外还有其他获取的方式,但是无论是哪种方式都要注意以下几点
构造器需要私有化
暴露公共接口获取单例对象
是否支持懒加载
是否线程安全
下面讲讲单例模式的几种实现方式
饿汉式 饿汉式的实现方法比较简单,在类加载的时候已经创建完毕。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class EagerSingleton implements Serializable { private static EagerSingleton instance = new EagerSingleton(); /** * 私有构造函数 */ private EagerSingleton() { synchronized (EagerSingleton.class){ if (instance != null){ throw new RuntimeException("单例构造器禁止反射调用!!!"); } } } public static EagerSingleton getInstance() { return instance; } }
饿汉式对象在类加载的时候被创建,所以没有线程不安全的问题,但是如果对象过大而且没有被使用,会占用较多内存资源,也会增加程序初始化开销(但是如果对象过大等到使用的时候再创建就会等待时间过长问题,还是要根据具体业务决定)
懒汉式-线程不安全 懒汉式就是等到真正需要对象的时候才创建,而不是一开始就创建好,实现代码如下
1 2 3 4 5 6 7 8 9 10 public class LazySingleton { private static Singleton instance; private Singleton (){} public static Singleton getInstance() { if (instance null) { instance = new Singleton(); } return instance; } }
但是上面的代码有一个很大的问题,就是在并发量大的情况下,可能回同时创建多个单例对象,无法满足单例特点
懒汉式-线程安全 要解决懒汉式线程不安全问题也很简单,加锁就可以了
1 2 3 4 5 6 7 8 9 10 11 12 13 public class LazySingleton { private static LazySingleton instance = null; private LazySingleton() { } public synchronized static LazySingleton getInstance() { if (instance null) { instance = new LazySingleton(); } return instance; } }
但是整个方法加锁后会影响性能,线程懒汉式中线程不安全的情况只有在创建对象的时候才会出现,在对象已经被创建之后锁不仅不能解决并发问题,反而会阻塞线程获取单例对象
懒汉式——双重检查锁 不加锁会有线程问题,加了锁会导致对象创建后获取对象进程阻塞,影响性能,那么我们可以用双重检查锁来解决这个问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class DclSingleton { private static volatile DclSingleton instance; private DclSingleton() { } public static DclSingleton getInstance() { if (instance null) { synchronized (DclSingleton.class) { if (instance null) { instance = new DclSingleton(); } } } return instance; } }
单例对象加入volatile是为了防止重排序导致对象半初始化情况(在高JDK已解决这个问题)
静态内部类 静态内部类实现单例既能够满足延迟加载 ,内部对象在类加载的时候不会被创建,只有在调用获取单例对象方法的时候才会加载内部类,又能保证线程安全,主要是由JVM内部保证单例
1 2 3 4 5 6 7 8 9 10 11 12 13 public class InnerSingleton { private InnerSingleton(){ } private static class SingletonHolder{ private static final InnerSingleton INSTANCE = new InnerSingleton(); } public static InnerSingleton getInstance(){ return SingletonHolder.INSTANCE; } }
枚举单例 1 2 3 public enum EnumSingleton { INSTANCE; }
这种实现方式是通过Java枚举的特性来保证单例的
以上单例模式方法真的能够保证单例吗 答案显然是否定的,我们可以通过反射和序列化继续创建对象
反射 除了枚举外,其他实现方式均能通过反射获取构造器并创建新的对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class Main { public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException { // 获取类对象 Class eagerSingletonClass = EagerSingleton.class; //获取构造器 Constructor declaredConstructor = eagerSingletonClass.getDeclaredConstructor(); //取消访问权限限制 declaredConstructor.setAccessible(true); //创建对象 EagerSingleton eagerSingleton = declaredConstructor.newInstance(); EagerSingleton eagerSingleton2 = declaredConstructor.newInstance(); //判断是否是同一个对象 System.out.println(eagerSingleton eagerSingleton2); } }
怎么解决这个问题呢,既然反射获取构造器不能阻止,那就只能在构造器做一些处理了,如果对象已经创建还调用构造方法,那就直接抛出异常
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public class EagerSingleton implements Serializable { private static EagerSingleton instance = new EagerSingleton(); /** * 私有构造函数 */ private EagerSingleton() { synchronized (EagerSingleton.class){ if (instance != null){ throw new RuntimeException("单例构造器禁止反射调用!!!"); } } } public static EagerSingleton getInstance() { return instance; } private Object readResolve() { return instance; } }
序列化和反序列化 解决了反射构造对象难道就能保证单例了吗,其实我们通过对象的序列化和反序列化还是可以创建新的对象
1 2 3 4 5 6 7 8 9 10 11 public static void main(String[] args) throws IOException, ClassNotFoundException { EagerSingleton singleton = EagerSingleton.getInstance(); FileOutputStream fout = new FileOutputStream("D://singleton.txt"); ObjectOutputStream out = new ObjectOutputStream(fout); out.writeObject(singleton); // 将实例反序列化出来 FileInputStream fin = new FileInputStream("D://singleton.txt"); ObjectInputStream in = new ObjectInputStream(fin); Object o = in.readObject(); System.out.println(osingleton); }
要解决这个问题,就只能在反序列化的时候做一些改动,在反序列化的过程中,会执行readResolve方法,并将返回值作为反序列化的结果,所以我们可以重写这个方法,返回单例对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class EagerSingleton implements Serializable { private static EagerSingleton instance = new EagerSingleton(); /** * 私有构造函数 */ private EagerSingleton() { synchronized (EagerSingleton.class){ if (instance != null){ throw new RuntimeException("单例构造器禁止反射调用!!!"); } } } public static EagerSingleton getInstance() { return instance; } // 重写方法 private Object readResolve() { return instance; } }
单例模式的应用 在Java中严格的单例模式其实很少,jdk中的Runtime类和mybatis的vfs(查找指定路径的资源)就是单例
实际的开发中我们很少严格的用到单例模式 因为他有一些弊端
无法面向对象编程:单例导致构造器私有化让类无法成为其他类的父类,等于放弃了继承和多态性,难以扩展需求
难以横向扩展:当单例对象无法满足需求的时候没办法扩展
但是我们可以借鉴这种思想,例如线程级别的单例(LocalThread)和容器级别的单例(IOC)