单例模式
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
注意:
- 单例模式只能有一个实例
- 单例类必须自己创建自己的唯一实例
- 单例类必须给所有其他对象提供这一实例
介绍
意图: 保证一个类仅有一个实例,并提供一个访问它的全局访问点。
主要解决: 一个全局使用的类频繁地创建与销毁。
何时使用: 当您想控制实例数目,节省系统资源的时候。
如何解决: 判断系统是否已经有这个单例,如果有则返回,如果没有则创建。
关键代码: 构造函数是私有的。
应用实例:
1、一个班级只有一个班主任。
2、Windows 是多进程多线程的,在操作一个文件的时候,就不可避免地出现多个进程或线程同时操作一个文件的现象,所以所有文件的处理必须通过唯一的实例来进行。
3、一些设备管理器常常设计为单例模式,比如一个电脑有两台打印机,在输出的时候就要处理不能两台打印机打印同一个文件。
4、JDK源码中,Runtime(饿汉模式),Currency(序列化)
优点:
1、在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存)。
2、避免对资源的多重占用(比如写文件操作)。
缺点:
没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。
1. 懒汉模式 线程不安全
1、线程安全问题
2、double check 加锁优化
3、编译器(JIT), CPU有可能对指令进行重排序,导致使用到尚未初始化的实例, 可以添加volatile
关键词进行修饰。对于volatile修饰的字段,可以防止指令重排序(字节码)
实例
/**
* @author hz
* @date 2020/8/2 15:22
*/
// 单例模式
public class LazySingletonTest {
public static void main(String[] args) {
new Thread( () -> {
LazySingleton instance = LazySingleton.getInstance();
System.out.println(instance);
}).start();
new Thread( () -> {
LazySingleton instance = LazySingleton.getInstance();
System.out.println(instance);
}).start();
}
}
class LazySingleton{
private static LazySingleton instance;
private LazySingleton() {};
public static LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
// 运行结果
singleton.LazySingleton@70b2a7f5
singleton.LazySingleton@4965b721
在多线程下,两次拿到的实例对象不是同一个。解决办法:在获取实例方法上添加关键词:synchronized
public static LazySingleton getInstance() {
if (instance == null) {
synchronized (LazySingleton.class) {
if (instance == null) {
instance = new LazySingleton();
}
}
}
return instance;
}
字节码层面
从字节码的层面来说:
instance = new LazySingleton(); 的执行顺序
JIT CPU
1. 分配空间
2. 初始化
3. 引用赋值 2,3 在单线程下可颠倒
多线程模式下:可能会出现空指针异常
解决办法:
添加关键词 volatile 防止重排序
private volatile static LazySingleton instance;
2. 饿汉模式
优点: 没有加锁,执行效率会提高。
缺点: 类加载时就初始化,容易产生垃圾对象,浪费内存。
类加载的初始化阶段完成了实例的初始化。 本质上就是借助JVM
类加载机,制保证实例的唯一性。
类加载过程:
1. 加载二进制数据到内存中,生成对应的Class数据结构;
2. 连接:a.验证 b.准备(给类的静态成员变量赋默认值
) c. 解析
3. 初始化:给类的静态变量赋初值
只有在真正使用对应的类时,才会触发初始化,如(当前类是启动类即main函数所在类,直接进行new操作,访问静态属性,访问静态方法,用反射访问类,初始化一个子类等)
public class HangrySingletonTest {
public static void main(String[] args) {
HangrySingleton instance = HangrySingleton.getInstance();
HangrySingleton instance1 = HangrySingleton.getInstance();
System.out.println(instance == instance1); // true
}
}
// 饿汉模式
class HangrySingleton{
private static HangrySingleton instance = new HangrySingleton();
private HangrySingleton() {};
public static HangrySingleton getInstance() {
return instance;
}
}
3. 静态内部类
1、本质上是利用类的加载机制来保证线程安全
2、只有在实际使用的时候,才会触发类的初始化,所以也是一种懒加载形式
public class InnerClassSingletonTest {
public static void main(String[] args) {
InnerClassSingleton instance = InnerClassSingleton.getInstance();
InnerClassSingleton instance1 = InnerClassSingleton.getInstance();
System.out.println(instance == instance1); // true
new Thread(() -> {
InnerClassSingleton instance3 = InnerClassSingleton.getInstance();
System.out.println(instance3);
}).start();
new Thread(() -> {
InnerClassSingleton instance4 = InnerClassSingleton.getInstance();
System.out.println(instance4);
}).start();
}
}
// 静态内部类 基于JVM的类加载机制,懒加载
class InnerClassSingleton{
private static class InnerClassHolder {
private static InnerClassSingleton instance = new InnerClassSingleton();
}
private InnerClassSingleton() {}
// 只有当调用这个方法时,并返回值,才会导致静态内部类的初始化
public static InnerClassSingleton getInstance() {
return InnerClassHolder.instance;
}
}
4. 反射攻击实例
以上方式创建单例对象时,通过反射创建出来的对象不是单例。
/**
* @author hz
* @date 2020/8/2 16:46
*/
// 通过反射创建实例,测试
public class ReflectionTest {
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Constructor<InnerClassSingleton> constructor = InnerClassSingleton.class.getDeclaredConstructor();
// 设置访问权限,使用private修饰的也会被获取
constructor.setAccessible(true);
// 通过反射获取到的实例
InnerClassSingleton innerClassSingleton = constructor.newInstance();
// 通过静态方法获取到的实例
InnerClassSingleton instance = InnerClassSingleton.getInstance();
System.out.println(innerClassSingleton == instance); // false
}
}
如何预防反射攻击?
在使用饿汉模式和静态内部类的方式,可以通过以下方式实现反射攻击的防护,懒汉模式不能使用这种方式。
private InnerClassSingleton() {
if (InnerClassHolder.instance != null) {
throw new RuntimeException("单例对象不允许多个实例对象");
}
}
5.枚举
1、天然不支持反射创建对应的实例,且有自己的反序列化机制
2、利用类加载机制保证线程安全
在通过反射获取实例时InnerClassSingleton innerClassSingleton = constructor.newInstance();
,
由JDK的源码可以看出是通过枚举的方式实现
public T newInstance(Object ... initargs)
throws InstantiationException, IllegalAccessException,
IllegalArgumentException, InvocationTargetException
{
if (!override) {
Class<?> caller = Reflection.getCallerClass();
checkAccess(caller, clazz, clazz, modifiers);
}
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
ConstructorAccessor ca = constructorAccessor; // read volatile
if (ca == null) {
ca = acquireConstructorAccessor();
}
@SuppressWarnings("unchecked")
T inst = (T) ca.newInstance(initargs);
return inst;
}
描述: 这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。
这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。不过,由于 JDK1.5 之后才加入 enum 特性,用这种方式写不免让人感觉生疏,在实际工作中,也很少用。
不能通过反射攻击(reflection attack) 来调用私有构造方法。
// 单例模式 枚举
public enum EnumSingleton {
INSTANCE;
public void print() {
System.out.println(this.hashCode());
}
}
class EnumTest {
public static void main(String[] args) {
EnumSingleton instance = EnumSingleton.INSTANCE;
EnumSingleton instance1 = EnumSingleton.INSTANCE;
System.out.println(instance == instance1); // true
}
}
通过反射的方式创建,是不支持的
// 测试enum能否被反射实例化
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Constructor<EnumSingleton> constructor = EnumSingleton.class.getDeclaredConstructor(String.class, int.class);
constructor.setAccessible(true);
EnumSingleton instance = constructor.newInstance("INSTANCE", 0);
}
//Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
反序列化机制
1、实现接口Serializable
2、通过流的方式序列化到文件
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("testSerializable"));
oos.writeObject(instance);
oos.close();
3、从磁盘拿出数据
// 从磁盘读出
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("testSerializable"));
InnerClassSingleton o = (InnerClassSingleton) ois.readObject();
通过反序列化的机制创建对象,他不会调用我们自己的构造函数。而是使用自己的序列化机制,直接从数据流中获取数据。
// 测试 反序列化
public static void main(String[] args) throws IOException, ClassNotFoundException {
InnerClassSingleton instance = InnerClassSingleton.getInstance();
// 序列化到文件
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("testSerializable"));
oos.writeObject(instance);
oos.close();
// 从磁盘读出
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("testSerializable"));
InnerClassSingleton o = (InnerClassSingleton) ois.readObject();
System.out.println(o == instance); // false
}
解决方案
查看Serializable
文档说明,添加方法readResolve
* Classes that need to designate a replacement when an instance of it
* is read from the stream should implement this special method with the
* exact signature.
*
* <PRE>
* ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException;
* </PRE><p>
添加版本号
若不添加版本号,序列化时JVM会根据字段名,方法名生成一个版本号存到序列化文件中,下次反序列化时,会根据class生成系列号进行比对,如果一致,可以进行反序列化,不一致,说明文件修改过。
static final long serialVersionUID = 42L;
// 替换序列化机制
Object readResolve() throws ObjectStreamException {
return InnerClassHolder.instance;
}
完整
public class InnerClassSingletonTest {
// 测试 反序列化
public static void main(String[] args) throws IOException, ClassNotFoundException {
InnerClassSingleton instance = InnerClassSingleton.getInstance();
// 序列化到文件
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("testSerializable"));
oos.writeObject(instance);
oos.close();
// 从磁盘读出
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("testSerializable"));
InnerClassSingleton o = (InnerClassSingleton) ois.readObject();
System.out.println(o == instance); // false
}
}
// 静态内部类 基于JVM的类加载机制,懒加载
class InnerClassSingleton implements Serializable {
static final long serialVersionUID = 42L;
private static class InnerClassHolder {
private static InnerClassSingleton instance = new InnerClassSingleton();
}
private InnerClassSingleton() {
if (InnerClassHolder.instance != null) {
throw new RuntimeException("单例对象不允许多个实例对象");
}
}
// 只有当调用这个方法时,并返回值,才会导致静态内部类的初始化
public static InnerClassSingleton getInstance() {
return InnerClassHolder.instance;
}
// 替换序列化机制
Object readResolve() throws ObjectStreamException {
return InnerClassHolder.instance;
}
}
枚举序列化
// 序列化测试
public static void main(String[] args) throws IOException, ClassNotFoundException {
EnumSingleton instance = EnumSingleton.INSTANCE;
// 序列化到文件
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("enumSingleton"));
oos.writeObject(instance);
oos.close();
// 从磁盘读出
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("enumSingleton"));
EnumSingleton o = (EnumSingleton) ois.readObject();
System.out.println(o == instance); // false
}