单例模式(Singleton Pattern):确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。单例模式是一种对象创建型模式。
通常我们可以通过调用类的静态变量来使一个对象被访问,但它不能防止你实例化多个该对象。一个最好的办法就是让类自身负责保存它的唯一实例,这样就可以保证没有其他实例被创建,并且它可以提供一个访问该实例的方法。
Singleton(单例):在单例类的内部实现只生成一个实例,同时它提供一个静态的 getInstance() 工厂方法,让客户可以访问它的唯一实例;为了防止在外部对其实例化,将其构造函数设计为私有;在单例类内部定义了一个 Singleton 类型的静态对象,作为外部共享的唯一实例。
单例模式除了能保证唯一的实例外,还可以控制客户怎样访问它以及何时访问它。简单地说就是对唯一实例的受控访问。
通常有五种方式来实现单例模式,懒汉式、饿汉式、双重校验加锁式、静态内部类式、枚举类式。
需要使用时才实例化对象,节约内存。但多线程情况下可能会有多个线程同时判断singleton == null而实例化多个LazySingleton对象。
public class LazySingleton {
private static LazySingleton singleton = null;
private LazySingleton() {}
public static LazySingleton getInstance() {
if (singleton == null) {
singleton = new LazySingleton();
}
return singleton;
}
}
在类加载期间直接实例化一个单例,就算不使用它,也会占用内存。但线程安全。
public class HungrySingleton {
private static HungrySingleton singleton = new HungrySingleton();
private HungrySingleton() {}
public static HungrySingleton getInstance() {
return singleton;
}
}
懒汉式的线程安全形式,但获取锁和释放锁需耗费一定的CPU处理时间。
public class DoubleCheckSingleton {
private volatile static DoubleCheckSingleton singleton = null;//要加volatile,保证线程间的可见性
private DoubleCheckSingleton() {}
public static DoubleCheckSingleton getInstance() {
if (singleton == null) {//不让线程每次访问getInstance方法都加锁,只有在没有实例化时才加锁
synchronized (DoubleCheckSingleton.class) { //锁的是类对象,不是实例对象singleton,因为singleton不一定被创建出来了
if (singleton == null) { //如果有多个线程同时通过了第一层校验,在获得锁后如果不再校验一次,有可能实例已经被创建过了
singleton = new DoubleCheckSingleton();
}
}
}
return singleton;
}
}
静态内部类只有在被调用时才会被加载,加载时会初始化这个内部类的静态变量,由虚拟机保证只会被初始化一次,这样就同时实现了延迟加载和线程安全。
public class StaticInnerClassSingleton {
private StaticInnerClassSingleton() {}
private static class Singleton {
public static StaticInnerClassSingleton singleton = new StaticInnerClassSingleton();
}
public static StaticInnerClassSingleton getInstance() {
return Singleton.singleton;
}
}
枚举类实质上是功能齐全的类,因此可有自己的属性和方法,枚举是通过公有的静态final域为每个枚举常量导出实例的类。简洁高效安全,由虚拟机保障不会被实例化多次。
public enum EnumSingleton {
enumSingleton;
}
单例模式的目标是,任何时候该类都只有唯一的一个对象。但是上面我们写的大部分单例模式都存在漏洞,被攻击时会产生多个对象,破坏了单例模式。
通过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序列化机制的复习,我们可以自定义一个 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
注意:自己实现的单例模式都需要避免被序列化破坏
在单例模式中,构造器都是私有的,但反射可以通过构造器对象调用 setAccessible(true) 来获得权限,这样就可以创建多个对象,来破坏饿汉式单例模式了。
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 NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
HungrySingleton instance = HungrySingleton.getInstance();
Constructor constructor = HungrySingleton.class.getDeclaredConstructor();
constructor.setAccessible(true); // 获得权限
HungrySingleton newInstance = (HungrySingleton) constructor.newInstance();
System.out.println(instance);
System.out.println(newInstance);
System.out.println(instance == newInstance);
}
}
输出结果
com.singleton.HungrySingleton@3b192d32
com.singleton.HungrySingleton@16f65612
false
反射是通过它的Class对象来调用构造器创建新的对象,我们只需要在构造器中检测并抛出异常就可以达到目的了
private HungrySingleton() {
// instance 不为空,说明单例对象已经存在
if (instance != null) {
throw new RuntimeException("单例模式禁止反射调用!");
}
}
运行结果
Exception in thread "main" java.lang.reflect.InvocationTargetException
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
at com.singleton.HungrySingleton.main(HungrySingleton.java:32)
Caused by: java.lang.RuntimeException: 单例模式禁止反射调用!
at com.singleton.HungrySingleton.(HungrySingleton.java:20)
... 5 more
注意,上述解决方法针对饿汉式单例模式是有效的,但对懒汉式的单例模式是无效的,懒汉式的单例模式是无法避免反射攻击的!
为什么对饿汉有效,对懒汉无效?因为饿汉的初始化是在类加载的时候,反射一定是在饿汉初始化之后才能使用;而懒汉是在第一次调用 getInstance() 方法的时候才初始化,我们无法控制反射和懒汉初始化的先后顺序,如果反射在前,不管反射创建了多少对象,instance都将一直为null,直到调用 getInstance(),懒汉仍会判断此时的instance为null而重新创建对象。
事实上,实现单例模式的唯一推荐方法,是使用枚举类来实现。
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
public enum SerEnumSingleton implements Serializable {
INSTANCE; // 单例对象
private String content;
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
private SerEnumSingleton() {
}
public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
SerEnumSingleton singleton1 = SerEnumSingleton.INSTANCE;
singleton1.setContent("枚举单例序列化");
System.out.println("枚举序列化前读取其中的内容:" + singleton1.getContent());
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("SerEnumSingleton.obj"));
oos.writeObject(singleton1);
oos.flush();
oos.close();
FileInputStream fis = new FileInputStream("SerEnumSingleton.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
SerEnumSingleton singleton2 = (SerEnumSingleton) ois.readObject();
ois.close();
System.out.println(singleton1 + "\n" + singleton2);
System.out.println("枚举序列化后读取其中的内容:" + singleton2.getContent());
System.out.println("枚举序列化前后两个是否同一个:" + (singleton1 == singleton2));
Constructor<SerEnumSingleton> constructor = SerEnumSingleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
SerEnumSingleton singleton3 = constructor.newInstance(); // 通过反射创建对象
System.out.println("反射后读取其中的内容:" + singleton3.getContent());
System.out.println("反射前后两个是否同一个:" + (singleton1 == singleton3));
}
}
运行结果,序列化前后的对象是同一个对象,而反射的时候抛出了异常
枚举序列化前读取其中的内容:枚举单例序列化
INSTANCE
INSTANCE
枚举序列化后读取其中的内容:枚举单例序列化
枚举序列化前后两个是否同一个:true
Exception in thread "main" java.lang.NoSuchMethodException: com.singleton.SerEnumSingleton.()
at java.lang.Class.getConstructor0(Class.java:3082)
at java.lang.Class.getDeclaredConstructor(Class.java:2178)
at com.singleton.SerEnumSingleton.main(SerEnumSingleton.java:39)
将此枚举类编译后,再通过 JAD 进行反编译得到下面的代码
// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3)
// Source File Name: SerEnumSingleton.java
package com.singleton;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
public final class SerEnumSingleton extends Enum
implements Serializable
{
public static SerEnumSingleton[] values()
{
return (SerEnumSingleton[])$VALUES.clone();
}
public static SerEnumSingleton valueOf(String name)
{
return (SerEnumSingleton)Enum.valueOf(com/singleton/SerEnumSingleton, name);
}
public String getContent()
{
return content;
}
public void setContent(String content)
{
this.content = content;
}
private SerEnumSingleton(String s, int i)
{
super(s, i);
}
public static void main(String args[])
throws IOException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException
{
SerEnumSingleton singleton1 = INSTANCE;
singleton1.setContent("\u679A\u4E3E\u5355\u4F8B\u5E8F\u5217\u5316");
System.out.println((new StringBuilder()).append("\u679A\u4E3E\u5E8F\u5217\u5316\u524D\u8BFB\u53D6\u5176\u4E2D\u7684\u5185\u5BB9\uFF1A").append(singleton1.getContent()).toString());
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("SerEnumSingleton.obj"));
oos.writeObject(singleton1);
oos.flush();
oos.close();
FileInputStream fis = new FileInputStream("SerEnumSingleton.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
SerEnumSingleton singleton2 = (SerEnumSingleton)ois.readObject();
ois.close();
System.out.println((new StringBuilder()).append(singleton1).append("\n").append(singleton2).toString());
System.out.println((new StringBuilder()).append("\u679A\u4E3E\u5E8F\u5217\u5316\u540E\u8BFB\u53D6\u5176\u4E2D\u7684\u5185\u5BB9\uFF1A").append(singleton2.getContent()).toString());
System.out.println((new StringBuilder()).append("\u679A\u4E3E\u5E8F\u5217\u5316\u524D\u540E\u4E24\u4E2A\u662F\u5426\u540C\u4E00\u4E2A\uFF1A").append(singleton1 == singleton2).toString());
Constructor constructor = com/singleton/SerEnumSingleton.getDeclaredConstructor(new Class[0]);
constructor.setAccessible(true);
SerEnumSingleton singleton3 = (SerEnumSingleton)constructor.newInstance(new Object[0]);
System.out.println((new StringBuilder()).append("\u53CD\u5C04\u540E\u8BFB\u53D6\u5176\u4E2D\u7684\u5185\u5BB9\uFF1A").append(singleton3.getContent()).toString());
System.out.println((new StringBuilder()).append("\u53CD\u5C04\u524D\u540E\u4E24\u4E2A\u662F\u5426\u540C\u4E00\u4E2A\uFF1A").append(singleton1 == singleton3).toString());
}
public static final SerEnumSingleton INSTANCE;
private String content;
private static final SerEnumSingleton $VALUES[];
static
{
INSTANCE = new SerEnumSingleton("INSTANCE", 0);
$VALUES = (new SerEnumSingleton[] {
INSTANCE
});
}
}
通过反编译后代码我们可以看到,public final class T extends Enum,说明,当我们使用enmu来定义一个枚举类型的时候,编译器会自动帮我们创建一个final类型的类继承Enum类,所以枚举类型不能被继承。
public static > T valueOf(Class enumType, String name) {
T result = enumType.enumConstantDirectory().get(name);
if (result != null)
return result;
if (name == null)
throw new NullPointerException("Name is null");
throw new IllegalArgumentException(
"No enum constant " + enumType.getCanonicalName() + "." + name);
}
从代码中可以看到,代码会尝试从调用enumType这个Class对象的enumConstantDirectory()方法返回的map中获取名字为name的枚举对象,如果不存在就会抛出异常。再进一步跟到enumConstantDirectory()方法,就会发现到最后会以反射的方式调用enumType这个类型的values()静态方法,也就是上面我们看到的编译器为我们创建的那个方法,然后用返回结果填充enumType这个Class对象中的enumConstantDirectory属性。所以,JVM对序列化有保证。
Constructor类的 newInstance方法中会判断是否为 enum,若是会抛出异常
@CallerSensitive
public T newInstance(Object ... initargs) throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
if (!override) {
if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
Class> caller = Reflection.getCallerClass();
checkAccess(caller, clazz, null, modifiers);
}
}
// 不能为 ENUM,否则抛出异常:不能通过反射创建 enum 对象
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;
}
单例模式作为一种目标明确、结构简单、理解容易的设计模式,在软件开发中使用频率相当高,在很多应用软件和框架中都得以广泛应用。
由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。
单例类的职责过重,在一定程度上违背了 “单一职责原则”。
如果实例化的共享对象长时间不被利用,系统可能会认为它是垃圾,会自动销毁并回收资源,下次利用时又将重新实例化,这将导致共享的单例对象状态的丢失。
###6.3 适用场景
系统只需要一个实例对象,如系统要求提供一个唯一的序列号生成器或资源管理器,或者需要考虑资源消耗太大而只允许创建一个对象。
客户调用类的单个实例只允许使用一个公共访问点,除了该公共访问点,不能通过其他途径访问该实例。
JDK Runtime类代表着Java程序的运行时环境,每个Java程序都有一个Runtime实例,该类会被自动创建,我们可以通过 Runtime.getRuntime() 方法来获取当前程序的Runtime实例。一旦得到了一个当前的Runtime对象的引用,就可以调用Runtime对象的方法去控制Java虚拟机的状态和行为。
Runtime 应用了饿汉式单例模式:
public class Runtime {
private static Runtime currentRuntime = new Runtime();
public static Runtime getRuntime() {
return currentRuntime;
}
private Runtime() {
}
//....
}
addShutdownHook(Thread hook) 注册新的虚拟机来关闭挂钩。
availableProcessors() 向 Java 虚拟机返回可用处理器的数目。
exec(String command) 在单独的进程中执行指定的字符串命令。
exec(String[] cmdarray) 在单独的进程中执行指定命令和变量。
exec(String[] cmdarray, String[] envp) 在指定环境的独立进程中执行指定命令和变量。
exec(String[] cmdarray, String[] envp, File dir) 在指定环境和工作目录的独立进程中执行指定的命令和变量。
exec(String command, String[] envp) 在指定环境的单独进程中执行指定的字符串命令。
exec(String command, String[] envp, File dir) 在有指定环境和工作目录的独立进程中执行指定的字符串命令。
exit(int status) 通过启动虚拟机的关闭序列,终止当前正在运行的 Java 虚拟机。
freeMemory() 返回 Java 虚拟机中的空闲内存量。
gc() 运行垃圾回收器。
getRuntime() 返回与当前 Java 应用程序相关的运行时对象。
halt(int status) 强行终止目前正在运行的 Java 虚拟机。
load(String filename) 加载作为动态库的指定文件名。
loadLibrary(String libname) 加载具有指定库名的动态库。
maxMemory() 返回 Java 虚拟机试图使用的最大内存量。
removeShutdownHook(Thread hook) 取消注册某个先前已注册的虚拟机关闭挂钩。
runFinalization() 运行挂起 finalization 的所有对象的终止方法。
totalMemory() 返回 Java 虚拟机中的内存总量。
traceInstructions(on) 启用/禁用指令跟踪。
traceMethodCalls(on) 启用/禁用方法调用跟踪。
参考