深入探究单例模式
1. 概述
单例模式的作用
- 节省内存和计算。比如从数据库连接中获取连接,这个连接只需要一个。
- 保证结果正确。比如用多线程统计人数,则需要用单例,大家需要对同一个内容修改。
- 方便管理。比如工具类只需要一个实例,比如说日期工具类,字符串工具类等
单例模式的适用场景
- 无状态的工具类:比如日志工具类,不管是在哪里使用,我们需要的只是它帮我们记录日志信息,除此之外,并不需要在它的实例对象上存储任何状态,这时候我们就只需要一个实例对象即可。
- 全局信息类:比如我们在一个类上记录网站的访问次数,我们不希望有的访问被记录在对象A上,有的却记录在对象B上,这时候我们就让这个类成为单例。
2. 单例模式的8种写法
下面的用Java演示各种单例模式的书写
2.1 静态常量
- 静态常量只和类本身有关,和对象无关,因此静态常量只会存在一个。
// 饿汉式
public class Singleton {
private final static Singleton INSTANCE = new Singleton(); // 在链接阶段的准备环节显式赋值
private Singleton(){
}
public static Singleton getInstance(){
return INSTANCE; }
}
2.2 静态代码块
- 静态变量和静态代码块一般是在 初始化阶段 (类的加载一般分为:加载、链接(验证、准备、解析)、初始化;参考网址)在<clinit>函数中被执行。因为函数<clinit>() 带锁线程是安全的,只会在类的加载时执行一次,因此是线程安全的。
// 饿汉式
public class Singleton {
private final static Singleton INSTANCE;
static {
INSTANCE = new Singleton();
}
private Singleton(){
}
public static Singleton getInstance(){
return INSTANCE; }
}
2.3 synchronized加锁
- 这里在static方法上加锁,相当于加了类锁(详细内容可参照:参考网址),可以保证多线程下的安全,但是效率低下,比如:如果这个单例是个处理字符串工具类,如果多个进程想要得到该实例去处理字符串,则无法实现,效率低下。因此不推荐使用。
// 懒汉式
public class Singleton {
private static Singleton instance;
private Singleton(){
}
public synchronized static Singleton getInstance(){
if (instance == null) {
instance = new Singleton(); }
return instance;
}
}
2.4 双重检查
- 这种方式是最推荐的两种方式中的一种,十分重要,必须记住。
// 双重检查:DCL (Double Check-Loading)
public class Singleton {
private volatile static Singleton instance;
private Singleton(){
}
public static Singleton getInstance(){
if (instance == null)
synchronized (Singleton.class) {
// 类锁
if (instance == null)
instance = new Singleton();
}
return instance;
}
}
下面是关于这种写法的说明
-
这种写法的优点是什么?
线程安全;延迟加载,效率较高
-
为什么要double-check,单check行不行?
不行,如果去掉内部的 if 判断,会造成线程不安全;如果去掉外部的 if 判断,会造成效率低下,每次只能有一个线程运行。
-
为什么不把synchronized放在getInstance()这个方法前?
这样做事可以的,但这种方式不好,因为:比如这个单例是个处理字符串工具类,如果多个进程想要得到该实例去处理字符串,则无法实现,效率低下。
-
为什么需要用volatile修饰instance?
(1)因为新建对象不是原子操作,实际上包含三个步骤(详细内容可参考:参考网址,找到该网址中的
5 对象的创建与访问指令
小节查看即可):/** * 1. 新建一个空的对象; * 2. 调用构造函数赋值; * 3. 将对象引用赋值给instance。 */
(2)volatile保证了这三个步骤不会重排序,重排序会导致NPE。 如果没有volatile可能发生重排序,比如一种重排序方式为:132,如果此时线程A执行完13后切换到线程B,线程B发现instance非空,返回instance,会导致线程B使用成员变量时抛出空指针异常(NPE)。
(3)volatile同时也保证了可见性。(这种说法本身没有问题,但不应作为这个问题的回答,因为:synchronized具有happens-before原则,已经保证了可见性)
2.5 静态内部类
- 静态内部类和非静态内部类一样,都不会因为外部内的加载而加载,同时静态内部类的加载不需要依附外部类,在使用时才加载,不过在加载静态内部类的过程中也会加载外部类,参考网址。
- 这也是一种推荐使用的单例模式。
// 懒汉式
public class Singleton {
private Singleton(){
}
private static class SingletonInstance {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance(){
return SingletonInstance.INSTANCE;
}
}
2.6 枚举
- 这种方式是最推荐的两种方式中的另一种。
// 枚举
public enum SingletonByEnum {
INSTANCE;
public static SingletonByEnum getInstance() {
return INSTANCE;
}
}
2.7 两种常见错误写法
错误写法一
// 懒汉式
public class Singleton {
private static Singleton instance;
private Singleton(){
}
public static Singleton getInstance(){
if (instance == null) {
instance = new Singleton(); }
return instance;
}
}
- 错误原因:两个线程同时去 if 判断,均成立,会创建两个实例。
错误写法二
// 懒汉式
public class Singleton {
private static Singleton instance;
private Singleton(){
}
public static Singleton getInstance(){
if (instance == null)
synchronized (Singleton.class) {
instance = new Singleton();
}
return instance;
}
}
- 错误原因:两个线程同时去 if 判断,均成立,会创建两个实例。
3. 单例模式的安全性探究
3.1 使用序列化破坏单例模式
- 这里演示破坏DCL
- 如下是破坏程序
public class DestroyWithSerializable {
public static void main(String[] args) throws Exception {
// 使用序列化破坏 DCL
Singleton instance = Singleton.getInstance();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton"));
oos.writeObject(instance);
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("singleton"));
Singleton instance2 = (Singleton) ois.readObject();
System.out.println(instance);
System.out.println(instance2);
System.out.println(instance == instance2);
}
}
我们发现Singleton
无法被序列化,输出结果如下:
- 为了演示序列化破坏单例,这里让
Singleton
实现序列化接口,更改DCL程序如下:
public class Singleton implements Serializable {
// 实现序列化接口
private volatile static Singleton instance;
private Singleton(){
}
public static Singleton getInstance(){
if (instance == null)
synchronized (Singleton.class) {
// 类锁
if (instance == null)
instance = new Singleton();
}
return instance;
}
}
- 此时再执行上面的
DestroyWithSerializable
程序,结果如下,发现已经破坏了单例
-
那么如果一个单例类实现了
Serializable
接口,序列化就一定可以破坏单例吗,有没有什么方法防止序列化破坏单例?存在防止序列化破坏单例的方式,我们可以更改
Singleton
类如下,这样就可以解决了序列化破坏单例的问题。
public class Singleton implements Serializable {
private volatile static Singleton instance;
private Singleton(){
}
public static Singleton getInstance(){
if (instance == null)
synchronized (Singleton.class) {
// 类锁
if (instance == null)
instance = new Singleton();
}
return instance;
}
private Object readResolve() {
return instance; }
}
- 此时再执行上面的
DestroyWithSerializable
程序,结果如下,发现已经无法破坏单例:
此时,我们脑海中就会存在这样一个疑问:为什么加上这样一个函数就可以防止序列化破坏单例? 下面我们就从源码的角度探究这个问题。
(1)我们要从Singleton instance2 = (Singleton) ois.readObject();
这句话开始看起,下面是readObject()
的源码
public final Object readObject() throws IOException, ClassNotFoundException {
if (enableOverride) {
return readObjectOverride(); }
// if nested read, passHandle contains handle of enclosing object
int outerHandle = passHandle;
try {
Object obj = readObject0(false); // 关键点
handles.markDependency(outerHandle, passHandle);
ClassNotFoundException ex = handles.lookupException(passHandle);
if (ex != null) {
throw ex; }
if (depth == 0) {
vlist.doCallbacks(); }
return obj;
} finally {
passHandle = outerHandle;
if (closed && depth == 0) {
clear(); }
}
}
(2)我们看到这段源码调用了Object obj = readObject0(false);
这个函数,接着分析这个函数,如下(无关代码已被省略):
private Object readObject0(boolean unshared) throws IOException {
// ...
try {
switch (tc) {
case TC_NULL: return readNull();
case TC_REFERENCE: return readHandle(unshared);
case TC_CLASS: return readClass(unshared);
case TC_CLASSDESC:
case TC_PROXYCLASSDESC: return readClassDesc(unshared);
case TC_STRING:
case TC_LONGSTRING: return checkResolve(readString(unshared));
case TC_ARRAY: return checkResolve(readArray(unshared));
case TC_ENUM: return checkResolve(readEnum(unshared));
case TC_OBJECT: return checkResolve(readOrdinaryObject(unshared)); // 关键
case TC_EXCEPTION:
IOException ex = readFatalException();
throw new WriteAbortedException("writing aborted", ex);
// ......
}
} finally {
depth--;
bin.setBlockDataMode(oldMode);
}
}
(3)因为我们最开始调用的是readObject()
方法,因此这里会进入这个分支,接着分析这个函数,如下(无关代码已被省略):
private Object readOrdinaryObject(boolean unshared) throws IOException {
// ...
ObjectStreamClass desc = readClassDesc(false);
desc.checkDeserialize();
// ...
Object obj;
try {
// 关键点,新的Singleton一定会被创建,只不过这个新对象不一定被返回
// 如果被序列化的对象定义readResolve()方法,则返回该方法返回的对象
obj = desc.isInstantiable() ? desc.newInstance() : null;
} catch (Exception ex) {
// ...
}
// ...
if (obj != null &&
handles.lookupException(passHandle) == null &&
desc.hasReadResolveMethod()) // 关键点
{
// 关键点,通过反射执行我们定义的readResolve()方法
Object rep = desc.invokeReadResolve(obj);
if (unshared && rep.getClass().isArray()) {
rep = cloneArray(rep); }
if (rep != obj) {
// Filter the replacement object
if (rep != null) {
if (rep.getClass().isArray()) filterCheck(rep.getClass(), Array.getLength(rep));
else {
filterCheck(rep.getClass(), -1); }
}
handles.setObject(passHandle, obj = rep); // 关键点:obj被重新赋值
}
}
return obj;
}
(4)我们可以看到上面的代码调用了desc.hasReadResolveMethod()
,下面进入这个方法查看,这个方法位于另外一个类ObjectStreamClass
。
/**
* Returns true if represented class is serializable or externalizable and
* defines a conformant readResolve method. Otherwise, returns false.
*/
// 如果被表示的类是可序列化的或可外部化的,并且定义了一个一致的readResolve方法,则返回true。否则,返回false
boolean hasReadResolveMethod() {
requireInitialized();
return (readResolveMethod != null);
}
(5)我们在Singleton
中定义了readResolve
方法,因此该方法返回true,if判断成立,我们可以进入执行Object rep = desc.invokeReadResolve(obj);
这句话,之后就会执行我们定义的readResolve
方法,然后返回我们自己的单例,因此防止了序列化破坏单例。
3.2 使用反射破坏单例模式
补充:反射相关知识
/*
反射:任何一个类都是Class的实例对象,这个实例对象有三种表示方式,称为类类型,比如有个类Foo,里面有个函数print(int a, int b)
Foo foo = new Foo();
Class c1 = Foo.class; // 第一种创建方式
Class c2 = foo.getClass(); // 第二种创建方式
Class c3 = Class.forName("Foo"); // 第三种创建方式
我们完全可以通过类的类类型创建该类的实例对象
Foo newFoo = (Foo)c1.newInstance();
获取方法信息
Method[] ms = c1.getMethods(); // 所有的public的函数,包括父类继承而来的
Method[] ms = c1.getDeclaredMethods(); // 获取所有该类自己声明的方法,不问访问权限
Class returnType = ms[0].getReturnType(); // 得到方法返回类型
String methodName = ms[0].getName(); // 得到方法名称
Class[] paramTypes = ms[0].getParameterTypes(); // 得到参数列表的类型的类类型
获取成员变量信息
Field[] fs = c1.getFields(); // 获取所有public的成员变量的信息
Field[] fs = c1.getDeclareFields(); // 获取所有该类自己声明的成员变量的信息
Class fieldType = fs[0].getType(); // 得到成员变量的类类型
String typeName = fieldType.getName(); // 得到成员变量的类型
String fieldName = fs[0].getName(); // 得到成员变量的名称
获取构造函数
Constructor[] cs = c1.getConstructors(); // 获取所有的public构造函数
Constructor[] cs = c1.getDeclareConstructors(); // 获取所有的构造函数
String constructorName = cs[0].getName(); // 获取构造函数名称
Class[] paramTypes = cs[0].getParameterTypes(); // 得到参数列表的类型的类类型
String paramName = paramTypes[0].getName(); // 获取参数名称
通过反射调用函数
Method m = c1.getMethod("print", new Class[]{int.class, int.class}); // 获取该方法
Method m = c1.getMethod("print", int.class, int.class); // 获取该方法
Object o = m.invoke(foo, new Object[]{10, 20}); // 用print方法这个对象m来操作foo,如果没有返回值就返回null
Object o = m.invoke(foo, 10, 20); // 和上句话效果一致
*/
- 既然序列化无法破坏单例了,那么我们就使用更加厉害的工具来破坏单例,即 反射。
- 此时的需要被破坏的代码如下:
public class Singleton implements Serializable {
private volatile static Singleton instance;
private Singleton(){
}
public static Singleton getInstance(){
if (instance == null)
synchronized (Singleton.class) {
// 类锁
if (instance == null)
instance = new Singleton();
}
return instance;
}
private Object readResolve() {
return instance; }
}
- 如下是破坏程序X
public class DestroyWithReflection {
public static void main(String[] args) throws Exception {
// 通过反射破坏 DCL
Class objectClass = Singleton.class;
Constructor c = objectClass.getDeclaredConstructor();
c.setAccessible(true); // 打开访问private权限
Singleton instance = Singleton.getInstance();
Singleton instance2 = (Singleton) c.newInstance(); // 会调用空参构造器
System.out.println(instance);
System.out.println(instance2);
System.out.println(instance == instance2);
}
}
单例模式被成功破坏,结果如下:
刚才我们辛辛苦苦防止序列化破坏单例,在这里就失效了吗?对,确实失效了,刚才的方式只能解决序列化破坏单例模式。
- 我们需要进一步升级
Singleton
类,既然反射会调用空参构造器,那么我们可以在空参构造器中加入判断,以防止单例被破坏,代码如下:
public class Singleton implements Serializable {
private volatile static Singleton instance;
private Singleton() {
if (instance != null) {
throw new RuntimeException("单例构造器禁止反射调用");
}
}
public static Singleton getInstance() {
if (instance == null)
synchronized (Singleton.class) {
// 类锁
if (instance == null)
instance = new Singleton();
}
return instance;
}
private Object readResolve() {
return instance; }
}
- 此时执行上面的破坏程序,结果如下:
可也看到,抛出了我们预期的异常。**但你以为这样就结束了吗?**事实上面这种方式并不能解决单例模式被破坏,我们只需要更改破坏程序中的代码,就可以让这种方法失效,即先用反射创建对象,此时Singleton
中的instance未被赋值,然后再调用getInstance()
就可以得到另一个不同的对象,升级为破坏程序Y:
public class DestroyWithReflection {
public static void main(String[] args) throws Exception {
// 通过反射破坏 DCL
Class objectClass = Singleton.class;
Constructor c = objectClass.getDeclaredConstructor();
c.setAccessible(true); // 打开访问private权限
Singleton instance2 = (Singleton) c.newInstance(); // 会调用空参构造器
Singleton instance = Singleton.getInstance();
System.out.println(instance);
System.out.println(instance2);
System.out.println(instance == instance2);
}
}
单例模式又被成功破坏,结果如下:
- 此时我们又该怎么办呢?别急,我们还有办法,继续升级我们的
Singleton
,我们可以在Singleton
中设置标志位,开始为false,一旦创建对象(无论是怎么创建的),就将flag置为true,下次如果再创建对象,发现flag为true直接抛出异常,升级后的Singleton
如下:
public class Singleton implements Serializable {
private volatile static Singleton instance;
private static boolean flag = false;
private Singleton() {
if (!flag) flag = true;
else throw new RuntimeException("单例构造器禁止反射调用");
}
public static Singleton getInstance() {
if (instance == null)
synchronized (Singleton.class) {
// 类锁
if (instance == null)
instance = new Singleton();
}
return instance;
}
private Object readResolve() {
return instance; }
}
执行刚刚升级过得破坏程序Y,结果如下:
同样可以看到,抛出了我们预期的异常。**但你以为这样总可以结束了吗?**事实上面这种方式并不能解决单例模式被破坏,我们只需要更改破坏程序中的代码,通过反射可以获取flag(我们如何知道该类中有个标志位叫做flag呢,既然我们想去破坏单例模式,一定有办法得到的,比如javap反编译),然后将其值强行置为true,此时单例又被破坏,升级为破坏程序Z:
public class DestroyWithReflection {
public static void main(String[] args) throws Exception {
// 通过反射破坏 DCL
Class objectClass = Singleton.class;
Constructor c = objectClass.getDeclaredConstructor();
c.setAccessible(true); // 打开访问private权限
Singleton instance2 = (Singleton) c.newInstance(); // 会调用空参构造器
Field flag = objectClass.getDeclaredField("flag"); // 获取objectClass的flag字段
flag.setAccessible(true); // 打开访问private权限
flag.set(objectClass, false); // 将objectClass的flag字段设为false
Singleton instance = Singleton.getInstance();
System.out.println(instance);
System.out.println(instance2);
System.out.println(instance == instance2);
}
}
单例模式又被成功破坏,结果如下:
- 那么,我们怎样修改代码才能避免DCL被反射攻击呢?最后的答案是:没办法!!!
反射攻击单例得到的最终的结论
对于懒汉式单例,我们无法避免发射攻击;对于饿汉式单例,我们可以通过在所有构造函数加入判断避免单例被破坏。
3.3 最安全的单例模式:枚举单例
-
Joshua Bloch大神在《Effctive Java》中明确表达过的观点:“使用枚举实现单例的方法虽然还没有广泛采用,但是单元素的枚举类型已经成为实现Singleton的最佳方法。”
(1)枚举简单,相当于饿汉式;
(2)线程安全有保证,枚举反编译为final class,继承枚举父类,并且各个变量以及方法都是用static定义的,所以枚举的本质就是一个静态的对象。
(3)可以避免反序列化和反射破坏单例。
-
如下测试使用的枚举单例如下:
public enum EnumSingleton {
INSTANCE {
@Override
protected void printTest() {
System.out.println("Print Test");
}
};
protected abstract void printTest(); // 保证外部可以调用
public static EnumSingleton getInstance() {
return INSTANCE;
}
}
- 尝试使用序列化破话枚举单例,代码如下:
public class DestroyWithSerializable {
public static void main(String[] args) throws Exception {
// 使用序列化破坏 枚举单例:无法破坏
EnumSingleton instance = EnumSingleton.getInstance();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton"));
oos.writeObject(instance);
instance.printTest(); // 调用INSTANCE中的printTest方法
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("singleton"));
EnumSingleton instance2 = (EnumSingleton) ois.readObject();
System.out.println(instance);
System.out.println(instance2);
System.out.println(instance == instance2);
}
}
无法破坏单例,结果如下:
- 尝试使用反射破坏单例,枚举类中不存在空参构造器,虽然IDEA反编译出来的.class文件中显示存在空参构造器,再虽然javap反编译出来内容显示有空参构造器,这些显示内容可以理解为不正确,没错,就可以理解为工具错了(参考视频),也可以查看
Enum
的源码,发现里面没有空参构造器。此时就需要使用更加专业的工具反编译.class文件了,那就是jad:下载网址,如下是枚举单例对应的.class文件反编译出来的结果:
// 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: EnumSingleton.java
package com.wxx;
import java.io.PrintStream;
public abstract class EnumSingleton extends Enum
{
public static EnumSingleton[] values()
{
return (EnumSingleton[])$VALUES.clone();
}
public static EnumSingleton valueOf(String name)
{
return (EnumSingleton)Enum.valueOf(com/wxx/EnumSingleton, name);
}
private EnumSingleton(String s, int i)
{
super(s, i);
}
protected abstract void printTest();
public static EnumSingleton getInstance()
{
return INSTANCE;
}
public static final EnumSingleton INSTANCE;
private static final EnumSingleton $VALUES[];
static
{
INSTANCE = new EnumSingleton("INSTANCE", 0) {
protected void printTest()
{
System.out.println("Print Test");
}
};
$VALUES = (new EnumSingleton[] {
INSTANCE
});
}
}
尝试使用反射破坏单例:
public class DestroyWithReflection {
public static void main(String[] args) throws Exception {
// 通过反射破坏 枚举单例:无法破坏
Class objectClass = EnumSingleton.class;
Constructor c = objectClass.getDeclaredConstructor(String.class, int.class);
c.setAccessible(true); // 打开访问private权限
EnumSingleton instance2 = (EnumSingleton) c.newInstance();
EnumSingleton instance = EnumSingleton.getInstance();
System.out.println(instance);
System.out.println(instance2);
System.out.println(instance == instance2);
}
}
无法破坏单例,结果如下:
4. 单例模式举例
- 例如JDK中的
Runtime
类:静态常量
5. 单例模式常见面试题
哪种单例模式最好?
-
枚举单例最好。原因如下:(1)简单;(2)安全(可以避免反序列化和反射攻击)
-
其次是DCL,但饿汉式单例无法避免反射攻击。
饿汉式和懒汉式的优缺点
-
饿汉式
优点:写法简单,线程安全
缺点:上来就加载实例,造成一定的浪费
-
懒汉式
优点:不需要一上来就加载实例,解决了浪费问题
缺点:写法稍微复杂,稍不注意可能造成线程不安全
单例模式各种写法的使用场合
-
最好的方法时利用枚举,因为还可以防止反序列化和反射重新创建新的对象;
-
如果程序一开始要加载的资源太多,那么就应该使用懒加载;
-
饿汉式如果是对象的创建需要配置文件就不合适,因为可能需要调用单例的一些方法做准备工作;
-
懒加载虽然好,但是静态内部类这种方式会引入编程复杂性
写出DCL,为什么是这样的?volatile去掉可以吗?
- 请参照2.4