1 定义
单例模式:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
单例模式的使用场景:
- 只要一个实例,例如一个
Web页面上的计数器
,可以不用把每次刷新都记录到数据中,使用单例模式保持计数器的值
,并确保是线程安全的 - 创建一个对象需要消耗的资源过多,或者回收耗时久,如要访问IO和数据库等资源
- 想要可以简单的访问实例类
- 让类自己控制它的实例化
- 要求生成唯一序列化的环境
2 UML# 3 优点
- 严格控制客户端如何访问和何时访问它,即对唯一实例的受控访问。
4 单例类和实用类的区别
- 相同:实用类也采用私有的构造方法避免其实例化。
- 不同:
- 实用类不保存状态,仅提供静态方法或者静态属性给用户使用;而单例类是有状态的。
- 实用类不能用于继承多态;单例类可以有子类继承。
- 实用类只是一些方法和属性的集合;单例类却是有着唯一的对象实例。
5 单例模式的几种写法
参考
https://blog.csdn.net/itachi85/article/details/50510124
5.1 饿汉模式
这种方式在类加载时,将自己实例化,所以类加载较慢,但获取对象的速度快。这种方式基于类加载机制避免了多线程的同步问题,但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化instance显然没有达到懒加载的效果。
//饿汉模式
public class Singleton1 {
private static Singleton1 instance=new Singleton1();
private Singleton1(){}
public static Singleton1 getInstance(){
return instance;
}
}
5.2 懒汉模式(线程不安全)
懒汉模式在方法被调用后才创建对象,以时间换空间,虽然节约了资源,但第一次加载时需要实例化,反映稍慢一些,在多线程环境下存在风险。
//懒汉模式(线程不安全)
public class Singleton2 {
private static Singleton2 instance;
private Singleton2(){}
public static Singleton2 getInstance(){
if(instance==null)
instance=new Singleton2();
return instance;
}
}
5.3 懒汉模式(线程安全)
这种写法能够在多线程中很好的工作,但是每次调用getInstance方法时,都需要进行同步,造成不必要的同步开销,而且大部分时候我们是用不到同步的,所以不建议用这种模式。
//懒汉模式(线程安全)
public class Singleton3 {
private static Singleton3 instance;
private Singleton3(){}
public static synchronized Singleton3 getInstance(){
if(instance==null)
instance=new Singleton3();
return instance;
}
}
《java并发编程的艺术》中讲到该模式不是线程安全
,理由如下:
instance=new Singleton3();
可以分解成:
memory=allocate(); //1、分配对象的内存空间
ctorInstance(memory); //2、初始化对象
instance=memory; //3、设置instance指向刚分配的内存地址
注意:2和3可能会被重排序
(线程内安全,线程间不安全),B线程就会访问到未被初始化的对象
解决办法:
方案1:不允许2和3重排序
即:双重检查模式 (DCL, Double Check Locking),利用volatile修饰单例对象,在多线程环境下,禁止2和3的重排序
方案2:允许2和3重排序,但不允许其他线程“看到”这个重排序(基于类初始化的解决方案)
即:静态内部类单例模式,详细原理见下文
5.4 双重检查模式 (DCL, Double Check Locking)
这种写法在getSingleton方法中对singleton进行了两次判空,第一次是为了不必要的同步,第二次是在singleton等于null的情况下才创建实例。在这里用到了volatile关键字,不了解volatile关键字的可以查看Java多线程(三)volatile域这篇文章,在这篇文章也提到了双重检查模式是正确使用volatile关键字的场景之一。
在这里使用volatile会或多或少的影响性能,但考虑到程序的正确性,牺牲这点性能还是值得的。
-
优点:DCL优点是资源利用率高,第一次执行getInstance时,单例对象才被实例化。
-
缺点:
第一次加载时反应稍慢一些
,在高并发环境下也有一定的缺陷,虽然发生的概率很小。
DCL虽然在一定程度解决了资源的消耗
、多余的同步
和线程安全
等问题,但是他还是在某些情况会出现失效的问题,也就是DCL失效,在《java并发编程实践》一书建议用静态内部类单例模式
来替代DCL。
//双重检查模式 (DCL)
public class Singleton4 {
private static volatile Singleton4 instance;
private Singleton4(){}
public static Singleton4 getInstance(){
if(instance==null){
synchronized (Singleton4.class){
if(instance==null)
instance=new Singleton4();
}
}
return instance;
}
}
5.5 静态内部类单例模式
我们先来区分一下两个概念:类加载、加载
。
类加载的过程包括加载,初始化,验证,解析,准备,初始化等五个过程。加载是类加载的一部分
。
静态内部类的优点是:外部类加载时并不需要立即加载内部类
,内部类不被加载则不去初始化INSTANCE
,故而不占内存。即当SingleTon第一次被加载时,并不需要去加载SingleTonHoler,只有当getInstance()方法第一次被调用时,才会去初始化INSTANCE,第一次调用getInstance()方法会导致虚拟机加载SingleTonHoler类,这种方法不仅能确保线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化
。
那么,静态内部类又是如何实现线程安全的呢
?首先,我们先了解下类的加载时机。
类加载时机:JAVA虚拟机在有且仅有
的5种场景下会对类进行初始化
。
- 遇到new、getstatic、setstatic或者invokestatic这4个字节码指令时,对应的java代码场景为:
new
一个关键字或者一个实例化对象时、在这里插入代码片
读取或设置一个静态字段时(final修饰、已在编译期把结果放入常量池的除外)、调用一个类的静态方法
时。 - 使用java.lang.reflect包的方法对类进行
反射调用
的时候,如果类没进行初始化,需要先调用其初始化方法进行初始化
。 - 当初始化一个类时,如果其
父类还未进行初始化,会先触发其父类的初始化
。 - 当虚拟机启动时,用户需要指定一个
要执行的主类(包含main()方法的类)
,虚拟机会先初始化这个类。 - 当使用JDK 1.7等动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
这5种情况被称为是类的主动引用
,注意,这里《虚拟机规范》中使用的限定词是"有且仅有"
,那么,除此之外的所有引用类都不会对类进行初始化
,称为被动引用。静态内部类就属于被动引用的行列
。内部静态类不会自动初始化,只有调用静态内部类的方法,静态域,或者构造方法
的时候才会加载静态内部类。
当getInstance()方法被调用时,SingleTonHoler才在SingleTon的运行时常量池里,把符号引用替换为直接引用
,这时静态对象INSTANCE也真正被创建
,然后再被getInstance()方法返回出去,这点同饿汉模式。那么INSTANCE在创建过程中又是如何保证线程安全的呢?
在《深入理解JAVA虚拟机》中,有这么一句话:
虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁、同步
,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的()方法,其他线程都需要阻塞等待,直到活动线程执行()方法完毕。如果在一个类的()方法中有耗时很长的操作,就可能造成多个进程阻塞(需要注意的是,其他线程虽然会被阻塞,但如果执行()方法后,其他线程唤醒之后不会再次进入()方法。同一个加载器下,一个类型只会初始化一次
。),在实际应用中,这种阻塞往往是很隐蔽的。
故而,可以看出INSTANCE在创建过程中是线程安全的,所以说静态内部类形式的单例可保证线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化
。
那么,是不是可以说静态内部类单例就是最完美的单例模式了呢?其实不然,静态内部类也有着一个致命的缺点,就是传参
的问题,由于是静态内部类的形式去创建单例的,故外部无法传递参数进去,例如Context这种参数,所以,我们创建单例时,可以在静态内部类与DCL模式里自己斟酌
。
//静态内部类单例模式
public class Singleton5 {
private Singleton5(){}
public static Singleton5 getInstance(){
return SingletonHolder.instance;
}
private static class SingletonHolder{
private static final Singleton5 instance=new Singleton5();
}
}
5.6 枚举单例
默认枚举实例的创建是线程安全的,并且在任何情况下都是单例,上述讲的几种单例模式实现中,有一种情况下他们会重新创建对象,那就是反序列化
,将一个单例实例对象写到磁盘再读回来,从而获得了一个实例
。反序列化操作提供了readResolve
方法,这个方法可以让开发人员控制对象的反序列化。在上述的几个方法示例中如果要杜绝单例对象被反序列化是重新生成对象,就必须加入如下方法
:
private Object readResolve() throws ObjectStreamException{
return singleton;
}
//枚举单例
public enum Singleton6 {
INSTANCE;
public void doSomeThing() {
}
}
枚举单例的优点就是简单,但是大部分应用开发很少用枚举,可读性并不是很高,不建议用
。
5.7 使用容器实现单例模式
用SingletonManager 将多种的单例类统一管理,在使用时根据key获取对象对应类型的对象。这种方式使得我们可以管理多种类型的单例
,并且在使用时可以通过统一的接口进行获取操作,降低了用户的使用成本,也对用户隐藏了具体实现,降低了耦合度。
public class ContainerSingleton {
private ContainerSingleton() {
}
private static Map<String, Object> ioc = new ConcurrentHashMap<>();
public static Object getInstance(String className) {
Object instance = null;
if (!ioc.containsKey(className)) {
synchronized (ContainerSingleton.class) {
if (!ioc.containsKey(className)) {
try {
instance = Class.forName(className).newInstance();
ioc.put(className, instance);
} catch (Exception e) {
e.printStackTrace();
}
return instance;
}
}
}
return ioc.get(className);
}
}
6 对比 基于volatile的双重检查锁定方案 和 基于类初始化方案
基于类初始化方案
优点:
- 实现代码更简洁
缺点:
- 只能对静态字段实现初始化
基于volatile的双重检查锁定方案
优点:
- 除了可以对静态字段实现延迟初始化外,还可以对实例字段实现延迟初始化
字段延迟初始化
优点:
- 降低了初始化类或创建实例的开销
缺点:
- 增加了访问被延迟初始化的字段的开销。
总结:
正常的初始化要优于延迟初始化。如果确实需要对实例字段使用线程安全的延迟初始化,就使用基于volatile实现的延迟初始化方案;如果确实需要对静态字段使用线程安全的延迟初始化,就使用基于类初始化实现的延迟初始化方案。