单例模式(单例模式作用、常见形式、代码实现)
一、单例模式能干啥?
所谓单例,就是整个程序有且仅有一个实例。
某个类全局只有一个实例对象有什么好处?一方面,由于单例模式只生成一个实例,减少了系统性能开销;另一方面,单例模式存在全局访问点,所以可以优化共享资源访问。比如:网站的计数器,一般也是采用单例模式实现,如果存在多个计数器对象,每一个用户的访问都刷新不同的计数器对象的值,统计总数的时候就很麻烦。如果采用单例模式实现就不会存在这样的问题,而且还可以避免线程安全问题。还有很多这样的场景:
- Windows的任务管理器
- Windows的回收站,也是一个单例应用
- 项目中的读取配置文件的对象(如Mybatis中的Configuration类的对象,保存配置信息)
- 数据库的连接池
- Servlet中的Application Servlet
- Spring中的Bean默认也是单例的
- SpringMVC Struts中的控制器
总的来说适用于:
- 1.需要生成唯一序列的环境
- 2.需要频繁实例化然后销毁的对象。
- 3.创建对象时耗时过多或者耗资源过多,但又经常用到的对象。
- 4.方便资源相互通信的环境
二、单例模式常见形式
总的按对象的创建时机,可以分为懒汉式和饿汉式:
1.饿汉式:线程安全 调用率高 但是不能延迟加载
2.懒汉式:线程安全 调用率不高 但是可以延迟加载
3.双重检测(double check )-----懒汉式
4.静态内部类(线程安全 可以延迟加载)--------懒汉式
5.枚举单例 线程安全 不可以延迟加载
看着形式很多,实际上单例模式都有以下特点:
- 构造器私有:这个很好理解,要是构造器不私有,那就可以在外部随意创建不同的对象了,违背了单例思想
- 持有自己类型的属性
- 对外提供获取实例的静态方法
三、懒汉式
懒汉式顾名思义,比较懒,只在你需要的时候才创建这个唯一的对象。它分为两种:
懒汉式(1)
public class Singleton1 {
// 自己持有自己
private static Singleton1 instance;
// 构造器私有化,不让外部通过构造器产生对象,从而保证对象全局唯一
private Singleton1() {}
// 对外提供获取唯一实例的静态方法
public static Singleton1 getInstance() {
// 先判断实例对象是否已经存在
if(instance == null){
// 创建实例
instance = new Singleton1();
}
return instance;
}
}
上面的如果是单线程的情况下是没问题的,但是在多线程时就有可能产生多个不同的对象,即是线程不安全的。
懒汉式(2)
上面懒汉(1)线程不安全,因此在创建实例方法上加上锁就有了下面的
public class Singleton2 {
// 自己持有自己
private static Singleton2 instance;
// 构造器私有化,不让外部通过构造器产生对象,从而保证对象全局唯一
private Singleton2() {}
// 对外提供获取唯一实例的静态方法
public static synchronized Singleton2 getInstance() {
// 先判断实例对象是否已经存在
if(instance == null){
// 创建实例
instance = new Singleton2();
}
return instance;
}
}
因为锁的原因,效率自然不高。
四、饿汉式
饿汉式顾名思义,比较饥渴,不管你要不要这个对象都先给你创建出来。
public class Singleton3 {
// 自己持有自己并直接创建对象
private static Singleton3 instance = new Singleton3();
// 构造器私有化,不让外部通过构造器产生对象,从而保证对象全局唯一
private Singleton3() {}
// 对外提供获取唯一实例的静态方法
public static Singleton3 getInstance() {
return instance;
}
}
和懒汉式的延时创建相比,饿汉式在加载类的时候对象就已经创建了,所以加载类的速度比较慢,但是获取对象的速度比较快,调用效率高,且是线程安全的。
五、双检锁式(DCL)
上面的懒汉式(2)加了锁虽然线程安全了,但是效率也降低了,而双检锁式可以兼顾线程安全和效率。实际上,双检锁也可以看成是懒汉式的一种特殊形式,也是延时创建唯一的对象。
public class Singleton4 {
// 自己持有自己并直接创建对象(使用volatile关键字防止重排序,new Instance()是一个非原子操作,可能创建一个不完整的实例)
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;
}
}
这里有两次对象的判空处理,锁是在两次判空中间的,这个锁只会在第一次创建实例的时候执行一次,一旦该实例被创建后面的线程就不会再需要拿到这个锁,因此不会因为锁而降低效率。
注意:
这里还有个关键字volatile,它是来防止某个线程获取不完整对象的。new Instance()是一个非原子操作,jvm存在乱序执行功能,因为重排序的原因,可能创建出来的就是一个不完整的实例。比如演示一下new Instance()过程:
- 分配实例所需的内存
- 创建引用,指向这个内存
- 初始化实例对象
这是一种顺序,但这个顺序不是固定的,实际过程中也可能是下面的顺序
- 分配实例所需的内存
- 初始化实例对象
- 创建引用,指向这个内存
每次执行顺序是重排序的,那就可能发生下面的现象:
- 线程A先进入Singleton4()方法
- 此时instance == null,线程A进入synchronized 块
- 再次判断,此时instance == null,线程A在执行 instance = new Singleton4();时,先执行“分配实例所需的内存”和“创建引用指向这个内存”,但还没有初始化这个对象
- 此时线程B进来,因为instance虽然未被初始化,但已经非null,不会再进行下面的创建语句,直接返回这个未初始化的instance
- 线程A等到资源后,继续完成对象初始化操作,线程A获得完成的instance对象
上面的线程B就拿到了非完整对象,这应该是个重大bug。
volatile包含以下语义:
(1)Java 存储模型不会对valatile指令的操作进行重排序:这个保证对volatile变量的操作时按照指令的出现顺序执行的。
(2)volatile变量不会被缓存在寄存器中(只有拥有线程可见)或者其他对CPU不可见的地方,每次总是从主存中读取volatile变量的结果。也就是说对于volatile变量的修改,其它线程总是可见的,并且不是使用自己线程栈内部的变量。也就是在happens-before法则中,对一个valatile变量的写操作后,其后的任何读操作理解可见此写操作的结果。
volatile 关键字修饰这个对象可以避免以上重排序可能带来的问题(volatile 需要在JDK1.5之后的版本才能确保安全)。
六、静态内部类式
/**
* Feng, Ge 2020/2/29 14:14
*/
public class Singleton5 {
// 构造器私有化,不让外部通过构造器产生对象,从而保证对象全局唯一
private Singleton5() {}
// 静态内部类
private static class SingleInnerHolder{
private static Singleton5 instance = new Singleton5();
}
// 对外提供获取唯一实例的静态方法
public static Singleton5 getInstance() {
return SingleInnerHolder.instance;
}
}
你肯定觉得这不是饿汉式么,对象直接就创建了,实际这个对象也是延时创建的,因此也可以理解成是懒汉式的一种特殊形式。
外部类加载时并不需要立即加载内部类,内部类不被加载则不去初始化INSTANCE,故而不占内存。
这里当Singleton5类被加载时,其静态内部类SingletonHolder没有被主动使用,只有当调用getInstance方法时, 才会装载SingletonHolder类,从而实例化单例对象。
这样的方式既能实现懒汉式对象的延时创建,也保证了线程的安全。
那么,静态内部类又是如何实现线程安全的呢?首先,我们先了解下类的加载时机。
类加载时机: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.INSTANCE,取的是SingleTonHoler里的INSTANCE对象,跟上面那个DCL方法不同的是,getInstance()方法并没有多次去new对象,故不管多少个线程去调用getInstance()方法,取的都是同一个INSTANCE对象,而不用去重新创建。当getInstance()方法被调用时,SingleTonHoler才在SingleTon的运行时常量池里,把符号引用替换为直接引用,这时静态对象INSTANCE也真正被创建,然后再被getInstance()方法返回出去,这点同饿汉模式。那么INSTANCE在创建过程中又是如何保证线程安全的呢?在《深入理解JAVA虚拟机》中,有这么一句话:
虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。如果在一个类的<clinit>()方法中有耗时很长的操作,就可能造成多个进程阻塞(需要注意的是,其他线程虽然会被阻塞,但如果执行<clinit>()方法后,其他线程唤醒之后不会再次进入<clinit>()方法。同一个加载器下,一个类型只会初始化一次。),在实际应用中,这种阻塞往往是很隐蔽的。
故而,可以看出INSTANCE在创建过程中是线程安全的,所以说静态内部类形式的单例可保证线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。
那么,是不是可以说静态内部类单例就是最完美的单例模式了呢?其实不然,静态内部类也有着一个致命的缺点,就是传参的问题,由于是静态内部类的形式去创建单例的,故外部无法传递参数进去,例如Context这种参数,所以,我们创建单例时,可以在静态内部类与DCL模式里自己斟酌。
七、枚举式
public enum Singleton6 {
// 定义1个枚举的元素,即为该类的唯一实例
INSTANCE;
public void anyMethod(){
System.out.println("任何一个方法!");
}
}
/**
* Feng, Ge 2020/2/29 15:34
*/
public class TestEnum {
public static void main(String[] args) {
Singleton6 singleton6 = Singleton6.INSTANCE;
singleton6.anyMethod();
}
}
枚举在java中与普通类一样,都能拥有字段与方法,防止反序列化生成多个实例,在任何情况下,它都是一个单例,因此线程安全。
八、打破单例模式
1.克隆(clone)
需要做到平时锁死,但是关键时刻我们能够撬开,克隆就是这么一种良性的单例模式破坏方法,具体做法如下:
public class DoubleLockSingleton implements Cloneable {
private static volatile DoubleLockSingleton doubleLockSingleton;
public DoubleLockSingleton() {
// constructed by Velociraptors
}
public static DoubleLockSingleton getInstance() {
if (doubleLockSingleton == null) {
synchronized (DoubleLockSingleton.class) {
if (doubleLockSingleton == null) {
doubleLockSingleton = new DoubleLockSingleton();
}
}
}
return doubleLockSingleton;
}
// Override by Velociraptors
@Override
protected Object clone() throws CloneNotSupportedException {
// auto created by Velociraptors
return super.clone();
}
public void method() {
System.out.println("Hello DoubleLockSingleton!");
}
}
public class SingletonLauncher {
public static void main(String[] args) throws CloneNotSupportedException {
// auto created by Velociraptors
DoubleLockSingleton doubleLockSingleton = DoubleLockSingleton.getInstance();
DoubleLockSingleton doubleLockSingleton2 = (DoubleLockSingleton)doubleLockSingleton.clone();
if(doubleLockSingleton == doubleLockSingleton2) {
System.out.println("Singleton break failed");
} else {
doubleLockSingleton.method();
doubleLockSingleton2.method();
}
}
}
//Hello DoubleLockSingleton!
//Hello DoubleLockSingleton!
2.反射(reflect)
通过java的反射机制,何止是创建一个实例,就连映射整个java类本身的结构都可以:
public class SingletonLauncher {
public static void main(String[] args) throws Throwable {
// auto created by Velociraptors
IdlerSingleton idlerSingleton = IdlerSingleton.getInstance();
Class<?> clazz = Class.forName("com.singleton.d1213.IdlerSingleton");
Constructor<?> constructor = clazz.getDeclaredConstructor();
constructor.setAccessible(true);
IdlerSingleton idlerSingleton2 = (IdlerSingleton) constructor.newInstance();
if(idlerSingleton == idlerSingleton2) {
System.out.println("Singleton break failed!");
}else {
System.out.println("Singleton break succeed!");
}
}
}
// Singleton break succeed!
3.序列化(serializable)
其与克隆性质有些相似,需要类实现序列化接口,相比于克隆,实现序列化在实际操作中更加不可避免,有些类,它就是一定要序列化。通过对Singleton的序列化与反序列化得到的对象是一个新的对象,这就破坏了Singleton的单例性。
例如下面就有一个序列化的类,并且实现了双重锁单例模式:
public class DoubleLockSingletonSerializable implements Serializable {
private static final long serialVersionUID = 972132622841L;
private static volatile DoubleLockSingletonSerializable doubleLockSingleton;
public DoubleLockSingletonSerializable() {
// constructed by Velociraptors
}
public static DoubleLockSingletonSerializable getInstance() {
if (doubleLockSingleton == null) {
synchronized (DoubleLockSingletonSerializable.class) {
if (doubleLockSingleton == null) {
doubleLockSingleton = new DoubleLockSingletonSerializable();
}
}
}
return doubleLockSingleton;
}
public void method() {
System.out.println("Hello DoubleLockSingleton!");
}
}
public class SingletonLauncher {
@SuppressWarnings("resource")
public static void main(String[] args) throws Throwable {
// auto created by Velociraptors
DoubleLockSingletonSerializable doubleLockSingletonSerializable = DoubleLockSingletonSerializable.getInstance();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("tempFile"));
objectOutputStream.writeObject(doubleLockSingletonSerializable);
File file = new File("tempFile");
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(file));
DoubleLockSingletonSerializable doubleLockSingletonSerializable2 = (DoubleLockSingletonSerializable) objectInputStream.readObject();
if (doubleLockSingletonSerializable == doubleLockSingletonSerializable2) {
System.out.println("Singleton break failed!");
} else {
System.out.println("Singleton break succeed!");
}
}
}
想要阻止序列化破坏单例模式,就只需要声明一个readResolve方法就好了。
public class DoubleLockSingletonSerializable implements Serializable {
private static final long serialVersionUID = 972132622841L;
private static volatile DoubleLockSingletonSerializable doubleLockSingleton;
public DoubleLockSingletonSerializable() {
// constructed by Velociraptors
}
public static DoubleLockSingletonSerializable getInstance() {
if (doubleLockSingleton == null) {
synchronized (DoubleLockSingletonSerializable.class) {
if (doubleLockSingleton == null) {
doubleLockSingleton = new DoubleLockSingletonSerializable();
}
}
}
return doubleLockSingleton;
}
public void method() {
System.out.println("Hello DoubleLockSingleton!");
}
private Object readResolve() {
return doubleLockSingleton;
}
}