单例模式
一、单例概念
单例模式是一种对象创建模式,它用于产生一个对象的具体实例,确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。
二、使用场景
-
创建一个对象会消耗过多的资源,如要访问IO和数据库等资源等,并且这个对象会被多次使用;
-
频繁使用的对象
-
某种类型的对象只应该有且只有一个。
由于new操作的次数减少,因而对系统内存的使用频率也会降低,这将减轻GC压力,缩短
GC停顿时间
三、UML类图
imageSingleton:单例类
实现单例模式主要有以下几个关键点:
-
构造函数不对外开发,一般为private;
-
通过一个静态方法或枚举返回单例类对象;
-
确保单例类的对象有且只有一个,尤其是在多线程环境下;
-
确保单例类对象在反序列化是不会重新构建对象。
四、单例的六种写法和各自特点
目前单例的写法有饿汉、懒汉、DCL、静态内部类、枚举、容器这六种方式。
1.饿汉模式
在JVM虚拟机加载这个类时,对象就会被实例化,不管是否使用到。(适用于创建对象不会消耗过多资源的情况)
public class HungrySingleton{ private static final HungrySingleton instance = new HungrySingleton(); private HungrySingleton(){} public static HungrySingleton getInstance(){ return instance; }}复制代码
不足之处:无法对instance实例做延迟加载。
2.懒汉模式
懒汉模式是声明一个静态对象,在用户第一次调用getInstance时进行初始化。
public class LazySingleton { private static LazySingleton instance = null; private LazySingleton(){} //使用synchronized关键字,保证此方法时线程安全的 public static synchronized LazySingleton getInstance(){ if(instance ==null){ instance = new LazySingleton(); } return instance; }}复制代码
优点:单例只有在使用时才会被实例化,在一定程度上节约了资源。
缺点:第一次加载时需要及时进行实例化,反应稍慢。
不足之处:每次调用getInstance都进行同步,造成不必要的同步开销。
3.Double Check Lock(DCL)方式
DCL方式既能够在需要时才能初始化单例,又能保证线程安全,且单例对象初始化后调用getInstance不进行同步锁。
public class DclSingleton { private static DclSingleton instance = null; private DclSingleton() { } public static DclSingleton getInstance() { if (instance == null) { //同步 synchronized (DclSingleton.class) { if (instance == null) { instance = new DclSingleton(); } } } return instance; }}复制代码
不足之处:JVM的即时编译器中存在指令重排序的优化。下面分析一下:
假设线程A执行到instance = new DclSingleton();语句,这句代码最终会被编译成多条汇编指令,它大致做了三件事情:
1)给DclSingleton的实例分配内存;
2)调用DclSingleton的构造函数,初始化成员字段;
3)将instance对象指向分配的内存空间(此时instance就不是null)。
由于Java编译器允许处理器乱序执行,上面2、3的顺序是无法保证的。在3执行完毕,2未执行之前,切换到B线程上,这是instance已经非空;所以线程B直接取走instance,再使用时会出错;这就是DCL失效问题,而且这种错误难以跟踪难以重现,很可能会隐藏很久。
在JDK1.5之后,具体化了volatile关键字,1.5之后的版本,只需将instance的定义改成
private volatile static DclSingleton instance = null;复制代码
就可以保证instance对象每次都是从主内存中读取,就可以用DCL的方式来完成单例模式。
4.静态内部类方式
DCL虽然在一定程度上解决了资源消耗、多余的同步、线程安全等问题,但是它还是在某些情况下出现失效的问题。
public class StaticInnerSingleton { private StaticInnerSingleton(){} public static StaticInnerSingleton getInstance(){ return SingletonHolder.instance; } private static class SingletonHolder{ private static final StaticInnerSingleton instance= new StaticInnerSingleton(); }}复制代码
这种方式不经能够确保线程安全,也能保证单例对象的唯一性,同时也延迟了单例的实例化,推荐使用这种方式实现单例模式。
5.枚举单例
public enum EnumSingleton { INTANCE;//定义一个枚举类的元素,它就是EnumSingleton的实例 public void doSomething(){ System.out.print("do sth."); }}复制代码
优点:写法简单、线程安全。
枚举在Java中与普通的类一样,不仅能够有字段,还能够有自己的方法。最重要的是默认枚举实例的创建时线程安全的,并且在任何情况下它都是一个单例。
6.使用容器实现单例模式
public class SingletonManager { private static Map<String,Object> objMap = new HashMap<>(); public static void registerService(String key,Object instance){ if(!objMap.containsKey(key)){ objMap.put(key,instance); } } public static Object getService(String key){ return objMap.get(key); }}复制代码
在程序的初始,将多种单例类型注入到一个统一的管理勒种,在使用时根据key获取对象。
此方式可以管理多种类型的单例,通过统一的接口进行获取操作,降低了使用成本及耦合度。
五、总结
实现单例模式的核心原理是将构造函数私有化,通过静态方法获取唯一的实例;在获取的过程中必须保证线程安全、防止反序列化导致重新生成实例对象等问题。
选择哪种实现方式取决于项目本身,如是否是复杂的并发环境、JDK版本是否过低、单例对象的资源消耗等。
参考文献:Android源码设计模式(第二版)
网友评论