单例是最常见的设计模式之一,实现的方式非常多,同时需要注意的问题也非常多。
本文主要内容:
- 介绍单例模式
- 介绍单例模式的N中写法
- 单例模式的安全性
- 单例模式总结
- 介绍单例模式的典型应用
单例模式(Singleton Pattern):确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。单例模式是一种对象创建型模式。
单例模式有三个要点:
- 构造方法私有化;
- 实例化的变量引用私有化;
- 获取实例的方法共有
Singleton(单例):在单例类的内部实现只生成一个实例,同时它提供一个静态的 getInstance() 工厂方法,让客户可以访问它的唯一实例;为了防止在外部对其实例化,将其构造函数设计为私有;在单例类内部定义了一个 Singleton 类型的静态对象,作为外部共享的唯一实例。
// 线程安全
public class Singleton {
private final static Singleton INSTANCE = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return INSTANCE;
}
}
优点:简单,使用时没有延迟;在类装载时就完成实例化,天生的线程安全
缺点:没有懒加载,启动较慢;如果从始至终都没使用过这个实例,则会造成内存的浪费。
// 线程安全
public class Singleton {
private static Singleton instance;
static {
instance = new Singleton();
}
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
将类实例化的过程放在了静态代码块中,在类装载的时执行静态代码块中的代码,初始化类的实例。优缺点同上。
// 线程不安全
public class Singleton {
private static Singleton singleton;
private Singleton() {}
public static Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
优点:懒加载,启动速度快、如果从始至终都没使用过这个实例,则不会初始化该实力,可节约资源
缺点:多线程环境下线程不安全。if (singleton == null) 存在竞态条件,可能会有多个线程同时进入 if 语句 ,导致产生多个实例
// 线程安全,效率低
public class Singleton {
private static Singleton singleton;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
优点:解决了上一种实现方式的线程不安全问题
缺点:synchronized 对整个 getInstance() 方法都进行了同步,每次只有一个线程能够进入该方法,并发性能极差
// 线程安全
public class Singleton {
// 注意:这里有 volatile 关键字修饰
private static volatile Singleton singleton;
private Singleton() {}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
优点:线程安全;延迟加载;效率较高。
由于 JVM 具有指令重排的特性,在多线程环境下可能出现 singleton 已经赋值但还没初始化的情况,导致一个线程获得还没有初始化的实例。volatile 关键字的作用:
- 保证了不同线程对这个变量进行操作时的可见性
- 禁止进行指令重排序
// 线程安全
public class Singleton {
private Singleton() {}
private static class SingletonInstance {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonInstance.INSTANCE;
}
}
优点:避免了线程不安全,延迟加载,效率高。
静态内部类的方式利用了类装载机制来保证线程安全,只有在第一次调用getInstance方法时,才会装载SingletonInstance内部类,完成Singleton的实例化,所以也有懒加载的效果。
加入参数 -verbose:class 可以查看类加载顺序
$ javac Singleton.java
$ java -verbose:class Singleton
// 线程安全
public enum Singleton {
INSTANCE;
public void whateverMethod() {
}
}
优点:通过JDK1.5中添加的枚举来实现单例模式,写法简单,且不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。
单例模式的目标是,任何时候该类都只有唯一的一个对象。但是上面我们写的大部分单例模式都存在漏洞,被攻击时会产生多个对象,破坏了单例模式。
通过Java的序列化机制来攻击单例模式
public class HungrySingleton {
private static final HungrySingleton instance = new HungrySingleton();
private HungrySingleton() {
}
public static HungrySingleton getInstance() {
return instance;
}
public static void main(String[] args) throws IOException,ClassNotFoundException {
HungrySingleton singleton = HungrySingleton.getInstance();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_file"));
oos.writeObject(singleton); // 序列化
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("singleton_file"));
HungrySingleton newSingleton = (HungrySingleton) ois.readObject(); // 反序列化
System.out.println(singleton);
System.out.println(newSingleton);
System.out.println(singleton == newSingleton);
}
}
结果
com.singleton.HungrySingleton@ed17bee
com.singleton.HungrySingleton@46f5f779
false
Java 序列化是如何攻击单例模式的呢?我们需要先复习一下Java的序列化机制
java.io.ObjectOutputStream 是Java实现序列化的关键类,它可以将一个对象转换成二进制流,然后可以通过 ObjectInputStream 将二进制流还原成对象。具体的序列化过程不是本文的重点,在此仅列出几个要点。
Java 序列化机制的要点:
- 需要序列化的类必须实现
java.io.Serializable 接口,否则会抛出NotSerializableException 异常
- 若没有显示地声明一个
serialVersionUID 变量,Java序列化机制会根据编译时的class自动生成一个serialVersionUID 作为序列化版本比较(验证一致性),如果检测到反序列化后的类的serialVersionUID 和对象二进制流的serialVersionUID 不同,则会抛出异常
- Java的序列化会将一个类包含的引用中所有的成员变量保存下来(深度复制),所以里面的引用类型必须也要实现
java.io.Serializable 接口
- 当某个字段被声明为
transient 后,默认序列化机制就会忽略该字段,反序列化后自动获得0或者null值
- 静态成员不参与序列化
- 每个类可以实现
readObject 、writeObject 方法实现自己的序列化策略,即使是transient 修饰的成员变量也可以手动调用ObjectOutputStream 的writeInt 等方法将这个成员变量序列化。
- 任何一个readObject方法,不管是显式的还是默认的,它都会返回一个新建的实例,这个新建的实例不同于该类初始化时创建的实例
- 每个类可以实现
private Object readResolve() 方法,在调用readObject 方法之后,如果存在readResolve 方法则自动调用该方法,readResolve 将对readObject 的结果进行处理,而最终readResolve 的处理结果将作为readObject 的结果返回。readResolve 的目的是保护性恢复对象,其最重要的应用就是保护性恢复单例、枚举类型的对象
-
Serializable 接口是一个标记接口,可自动实现序列化,而Externalizable 继承自Serializable ,它强制必须手动实现序列化和反序列化算法,相对来说更加高效
根据上面对Java序列化机制的复习,我们可以自定义一个 readResolve ,在其中返回类的单例对象,替换掉 readObject 方法反序列化生成的对象,让我们自己写的单例模式实现保护性恢复对象
public class HungrySingleton implements Serializable {
private static final HungrySingleton instance = new HungrySingleton();
private HungrySingleton() {
}
public static HungrySingleton getInstance() {
return instance;
}
private Object readResolve() {
return instance;
}
public static void main(String[] args) throws IOException,ClassNotFoundException {
HungrySingleton singleton = HungrySingleton.getInstance();
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("singleton_file"));
HungrySingleton newSingleton = (HungrySingleton) ois.readObject();
System.out.println(singleton);
System.out.println(newSingleton);
System.out.println(singleton == newSingleton);
}
}
再次运行
com.singleton.HungrySingleton@24273305
com.singleton.HungrySingleton@24273305
true
注意:自己实现的单例模式都需要避免被序列化破坏
(编辑:安卓应用网)
【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!
|