单例模式(Singleton Pattern)
介绍
意图: 保证一个类仅有一个实例。并提供一个访问它的全局访问点。
主要解决: 一个全局使用的类频繁的创建与销毁对象。
何时使用: 一个类只需要有一个对象时。
如何解决: 判断是否已有该类实例,有则返回,无则创建。
关键代码: 类自己创建自己的实例,构造器私有化,提供全局访问点。
应用实例: 任务管理器。
注意事项: 防止多线程环境下创建多个实例对象。
饿汉式单例模式
- 类一加载就创建类的实例对象,即饿汉式。
- 只在类加载时才会初始化实例对象,故只会有一个实例。
- 缺点:如果只是加载该类而不需要其实例,则会造成资源的浪费。
public class SingletonDemo01 {
//类加载的时候就自己new实例对象
private static SingletonDemo01 instance = new SingletonDemo01();
//构造器私有化,外部不可以new对象
private SingletonDemo01() {
}
//方法没有同步 调用效率高 且天然线程安全
public static SingletonDemo01 getInstance() {
return instance;
}
}
懒汉式单例模式
- 延迟加载,懒加载。用到类实例的时候才去加载资源。
- 资源的利用率提高了,但getInstance方法必须加同步,并发环境下的效率降低了。
public class SingletonDemo02 {
//类对象属性
private static SingletonDemo02 instance;
//构造器私有化
private SingletonDemo02() {
}
//方法加同步 防止多线程模式下产生多个实例对象
public static synchronized SingletonDemo02 getInstance() {
if(instance==null) {
instance = new SingletonDemo02();
}
return instance;
}
}
双重检测锁实现(Double Check Lock)
instance = new SingletonDemo03(); 实际上有3步:
在堆内为该对象开辟一块内存空间。
初始化该对象。
该对象(instance)指向开辟的内存空间。
而 JVM 可能会进行指令的重排,即乱序优化,从而优化内存。
可能会出现1,3,2这样的指令顺序,而在多线程环境下就有可能访问到一个未初始化的对象。
volatile 禁止乱序优化。
第一重检测:相比于懒汉式提高效率。
第二重检测:确保多线程环境下的单例保证。
public class SingletonDemo03 {
//volatile 标记的变量不会使用优化功能 即1,2,3步不会乱序优化 从而避免DCL失效
private static volatile SingletonDemo03 instance;
public static SingletonDemo03 getInstance() {
//第一重监测
//若该实例未创建,则加同步锁创建。若该实例存在,则直接返回
//如果不加第一次校验的话,那跟上面的懒汉模式没什么区别,每次都要去竞争锁。
if(instance==null) {
//加同步锁,
synchronized (SingletonDemo03.class) {
//第二重检测
if(instance==null) {
instance = new SingletonDemo03();
}
}
}
return instance;
}
//构造器的私有化
private SingletonDemo03() {
}
}
静态内部类实现
外部类加载时不需要立即加载内部类,就不会初始化实例 instance。
调用 getInstance 方法时才去加载内部类,进而初始化实例,类加载是线程安全的。
静态内部类实现的单例模式:兼具并发、高效调用和延迟加载的优势。
public class SingletonDemo04 {
//静态内部类实现
private static class SingletonInstance{
private static SingletonDemo04 instance = new SingletonDemo04();
}
//获取单例实例的方法
public static SingletonDemo04 getInstance() {
return SingletonInstance.instance;
}
//构造器私有化
private SingletonDemo04() {
}
}
枚举实现
- 枚举本身就是单例模式,由 JVM 提供保障。
- 避免了反射和反序列化的漏洞。由 JVM 提供保障。
- 缺点:没有懒加载的效果。
public enum SingletonDemo05 {
//这个枚举元素 本身就是单例模式
INSTANCE;
//添加自己需要的操作
public void singletonOperation() {
}
}
单例模式的破解
- 利用反射破解单例:直接用类的私有构造器创建实例对象。
- 利用反序列化破解:将实例对象序列化存储,再反序列化。
public class TestDemo06 {
public static void main(String[] args) throws Exception {
SingletonDemo06 instance1 = SingletonDemo06.getInstance();
SingletonDemo06 instance2 = SingletonDemo06.getInstance();
System.out.println(instance1);
System.out.println(instance2);
//通过反射的方式直接调用私有构造器
Class<?> clazz = SingletonDemo06.class;
Constructor<?> constructor = clazz.getDeclaredConstructor(null);
constructor.setAccessible(true);
SingletonDemo06 instance3 = (SingletonDemo06) constructor.newInstance(null);
SingletonDemo06 instance4 = (SingletonDemo06) constructor.newInstance(null);
System.out.println(instance3);
System.out.println(instance4);
//通过反序列化的方式破解单例模式
// FileOutputStream fos = new FileOutputStream("resources/temp.serializable"); //序列化存入内存
// ObjectOutputStream oos = new ObjectOutputStream(fos); //对象输出流
// oos.writeObject(instance2); //单例模式的第二个对象序列化
// oos.close();
// fos.close();
//
// ObjectInputStream ois = new ObjectInputStream(new FileInputStream("resources/temp.serializable"));
// SingletonDemo06 instance3 = (SingletonDemo06) ois.readObject(); //从内存中反序列化
// ois.close();
//
// //单例模式定义readResolve()方法
// System.out.println(instance3);
}
}
防止反射和反序列化的破解
- 防反射破解:私有构造器添加判断,第一次正常创建实例对象,而后抛异常。
- 防反序列化破解:定义 readResolve方法,反序列化的时候会调用这个方法。
public class SingletonDemo06 implements Serializable {
private static final long serialVersionUID = 1L; //序列化版本号 一旦修改则前后不兼容
//类对象属性
private static SingletonDemo06 instance;
//构造器私有化
private SingletonDemo06() {
//防止反射漏洞 第一次正常构造 再调用构造器的话抛异常
if(instance!=null) {
throw new RuntimeException();
}
}
//加同步 防止多线程模式下产生多个实例对象
public static synchronized SingletonDemo06 getInstance() {
if(instance==null) {
instance = new SingletonDemo06();
}
return instance;
}
//防止反序列化的漏洞
//基于回调 反序列化时自动调用
//反序列化的时候直接调用此方法把整个对象返回 而不需要反序列化一个新对象
private Object readResolve() {
return instance;
}
}
如何选择单例模式的实现
单例对象:占用资源小,不需要延时加载
枚举式 好于 饿汉式
单例对象:占用资源大,需要延时加载
静态内部类式 好于 懒汉式