长处
- 提供了对惟一实例的受控拜访。
- 因为在零碎内存中只存在一个对象,因而能够节约系统资源,对于一些须要频繁创立和销毁的对象单例模式无疑能够进步零碎的性能。
- 容许可变数目标实例。
毛病
- 因为单例模式中没有形象层,因而单例类的扩大有很大的艰难。
- 单例类的职责过重,在肯定水平上违反了“繁多职责准则”。
- 滥用单例将带来一些负面问题,如为了节俭资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而呈现连接池溢出;如果实例化的对象长时间不被利用,零碎会认为是垃圾而被回收,这将导致对象状态的失落。
特点
- 构造方法公有。
- 外部对象公有。
- 提供返回对象的函数私有。
Java 中单例模式的实现形式
利用公有的外部工厂类(线程平安,外部类也能够换成外部接口,不过工厂类变量的作用于要改为 public)
public class Singleton {private Singleton(){System.out.println("Singleton:" + System.nanoTime());
}
public static Singleton getInstance(){return SingletonFactory.singletonInstance;}
private static class SingletonFactory{private static Singleton singletonInstance = new Singleton();
}
}
为什么应用动态外部类实现单例模式,能够保障线程平安?
- 加载一个类时,其内部类不会同时被加载。一个类被加载,当且仅当其某个动态成员(动态域、结构器、静态方法等)被调用时产生。
- 类的加载的过程是单线程执行的。它的并发平安是由 JVM 保障的。所以,这样写的益处是在 instance 初始化的过程中,由 JVM 的类加载机制保障了线程平安,而在初始化实现当前,不论前面多少次调用 getInstance 办法都不会再遇到锁的问题了。
饿汉式和懒汉式
饿汉式和懒汉式的区别?
在程序启动或单件模式类被加载的时候,单件模式实例就曾经被创立。
- 饿汉式:在程序启动或单件模式类被加载的时候,单件模式实例就曾经被创立。
- 懒汉式:当程序第一次拜访单件模式实例时才进行创立。
饿汉式(线程平安)
在程序启动或单件模式类被加载的时候,单件模式实例就曾经被创立。
- 不让外界调用构造方法创建对象,构造方法使私有化,应用
private
润饰。 - 怎么让内部获取本类的实例对象?通过本类提供一个办法,供内部调用获取实例。因为没有对象调用,所以此办法为类办法,用
static
润饰。 - 通过办法返回实例对象,因为类办法 (静态方法) 只能调用静态方法,所以寄存该实例的变量改为类变量,用
static
润饰。 - 类变量,类办法是在类加载时初始化的,只加载一次。因为内部不能创建对象,并且实例只在类加载时创立一次,饿汉式单例模式实现。
public class Single2 {private static Single2 instance = new Single2();
private Single2(){System.out.println("Single2:" + System.nanoTime());
}
public static Single2 getInstance(){return instance;}
}
懒汉式(如果办法没有 synchronized,则线程不平安)
public class Single3 {
private static Single3 instance = null;
private Single3(){System.out.println("Single3:" + System.nanoTime());
}
public static synchronized Single3 getInstance(){if(instance == null){instance = new Single3();
}
return instance;
}
}
懒汉模式改良版(线程平安,应用了 double-check,即 check- 加锁 -check,目标是为了缩小同步的开销)
public class Single4 {
// volatile 关键字必须加,保障可见性
private volatile static Single4 instance = null;
private Single4(){System.out.println("Single4:" + System.nanoTime());
}
public static Single4 getInstance(){if(instance == null){synchronized (Single4.class) {if(instance == null){instance = new Single4();
}
}
}
return instance;
}
}
指令重排序是怎么回事?
在给 instance 对象初始化的过程中,jvm 做了上面 3 件事:
- 给 instance 对象分配内存
- 调用构造函数
- 将 instance 对象指向调配的内存空间
因为 jvm 的 ” 优化 ”, 指令 2 和指令 3 的执行程序是不肯定的,当执行完指定 3 后,此时的 instance 对象就曾经不在是 null 的了, 但此时指令 2 不肯定曾经被执行。
假如线程 1 和线程 2 同时调用 getInstance()办法,此时线程 1 执行完指令 1 和指令 3,线程 2 抢到了执行权,此时 instance 对象是非空的。
所以线程 2 拿到了一个尚未初始化的 instance 对象,此时线程 2 调用这个 instance 就会抛出异样。
为什么 volatile 关键字能够保障双检锁不会呈现指令重排序的问题?
- volatile 关键字能够保障 jvm 执行的肯定的“有序性”,在指令 1 和指令 2 执行完之前,指定 3 肯定不会被执行。为什么说是肯定的 ” 有序性 ” 呢,因为对于非易失的读写,jvm 依然容许对 volatile 变量进行乱序读写
- 保障了 volatile 变量被批改后立即刷新到 CPU 的缓存中。
枚举类型实现单例模式
在 Java 引入了 enum 关键字当前,能够应用枚举来实现单例类:
public class Single5 {private Single5(){ }
/**
* 枚举类型是线程平安的,并且只会装载一次
*/
private enum Singleton{
INSTANCE;
private final Single5 instance;
Singleton(){instance = new Single5();
}
private Single5 getInstance(){return instance;}
}
public static Single5 getInstance(){return Singleton.INSTANCE.getInstance();
}
}
枚举类实现单例模式是 effective java 作者极力推荐的单例实现模式,因为枚举类型是线程平安的,并且只会装载一次,设计者充沛的利用了枚举的这个个性来实现单例模式,枚举的写法非常简单,而且枚举类型是所用单例实现中惟一一种不会被毁坏的单例实现模式。
反射如何毁坏单例模式
演示
一个单例类:
public class Singleton {private static Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {return instance;}
}
通过反射毁坏单例模式:
public class Test {public static void main(String[] args) throws Exception{Singleton s1 = Singleton.getInstance();
Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton s2 = constructor.newInstance();
System.out.println(s1.hashCode());
System.out.println(s2.hashCode());
}
}
输入后果:
671631440
935563443
结果表明 s1 和 s2 是两个不同的实例了。
剖析
通过反射取得单例类的构造函数,因为该构造函数是 private 的,通过 setAccessible(true)批示反射的对象在应用时应该勾销 Java 语言拜访查看, 使得公有的构造函数可能被拜访,这样使得单例模式生效。
正文
publicConstructor<T> getDeclaredConstructor(Class<?>... parameterTypes)
获取单个构造方法(能获取公有的,但要用 Constructor 类的 setAccessible(true)
办法设置拜访权限),参数示意的是:你要获取的构造方法的结构参数个数及数据类型的 class 字节码文件对象。
毁坏单例模式的办法及解决办法
除枚举形式外, 其余办法都会通过反射的形式毁坏单例, 反射是通过调用构造方法生成新的对象,所以如果咱们想要阻止单例毁坏。
-
能够在构造方法中进行判断,若已有实例, 则阻止生成新的实例:
private SingletonObject1(){if (instance !=null){throw new RuntimeException("实例曾经存在,请通过 getInstance()办法获取"); } }
-
如果单例类实现了序列化接口 Serializable, 就能够通过反序列化毁坏单例,所以咱们能够不实现序列化接口, 如果非得实现序列化接口,能够重写反序列化办法 readResolve(), 反序列化时间接返回相干单例对象:
public Object readResolve() throws ObjectStreamException {return instance;}
-
避免构造函数被胜利调用两次,在构造函数中对实例化次数进行统计,大于一次就抛出异样。
public class Singleton { private static int count = 0; private static Singleton instance = null; private Singleton(){synchronized (Singleton.class) {if(count > 0){throw new RuntimeException("创立了两个实例"); } count++; } } public static Singleton getInstance() {if(instance == null) {instance = new Singleton(); } return instance; } public static void main(String[] args) throws Exception {Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor(); constructor.setAccessible(true); Singleton s1 = constructor.newInstance(); Singleton s2 = constructor.newInstance();} }
执行后果
Exception in thread "main" java.lang.reflect.InvocationTargetException at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) at sun.reflect.NativeConstructorAccessorImpl.newInstance(Unknown Source) at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(Unknown Source) at java.lang.reflect.Constructor.newInstance(Unknown Source) at com.yzz.reflect.Singleton.main(Singleton.java:33) Caused by: java.lang.RuntimeException: 创立了两个实例 at com.yzz.reflect.Singleton.<init>(Singleton.java:14) ... 5 more
剖析
在通过反射创立第二个实例时抛出异样,避免实例化多个对象。构造函数中的 synchronized 是为了避免多线程状况下实例化多个对象。
援用 / 参考
设计模式:懒汉式和饿汉式 – 北京小辉 – CSDN
“ 泡泡 201908061058789″ 的答复 – 牛客
外部类加载程序及动态外部类单例模式 – CSDN
java 中双检锁为什么要加上 volatile 关键字 – CSDN
反射如何毁坏单例模式 – Everglow 的博客