设计模式 | 单例模式及典型应用

2019-01-07  本文已影响0人  小旋锋的简书

微信原文:设计模式 | 单例模式及典型应用

单例是最常见的设计模式之一,实现的方式非常多,同时需要注意的问题也非常多。

本文主要内容:

单例模式

单例模式(Singleton Pattern):确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。单例模式是一种对象创建型模式。

单例模式有三个要点:

  1. 构造方法私有化;
  2. 实例化的变量引用私有化;
  3. 获取实例的方法共有

角色

Singleton(单例):在单例类的内部实现只生成一个实例,同时它提供一个静态的 getInstance() 工厂方法,让客户可以访问它的唯一实例;为了防止在外部对其实例化,将其构造函数设计为私有;在单例类内部定义了一个 Singleton 类型的静态对象,作为外部共享的唯一实例。

单例模式的七种写法

1、饿汉式

// 线程安全
public class Singleton {

    private final static Singleton INSTANCE = new Singleton();

    private Singleton(){}

    public static Singleton getInstance(){
        return INSTANCE;
    }
}

优点:简单,使用时没有延迟;在类装载时就完成实例化,天生的线程安全

缺点:没有懒加载,启动较慢;如果从始至终都没使用过这个实例,则会造成内存的浪费。

2、饿汉式变种

// 线程安全
public class Singleton {

    private static Singleton instance;

    static {
        instance = new Singleton();
    }

    private Singleton() {}

    public static Singleton getInstance() {
        return instance;
    }
}

将类实例化的过程放在了静态代码块中,在类装载的时执行静态代码块中的代码,初始化类的实例。优缺点同上。

3、懒汉式

// 线程不安全
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 语句,导致产生多个实例

4、懒汉式变种

// 线程安全,效率低
public class Singleton {

    private static Singleton singleton;

    private Singleton() {}

    public static synchronized Singleton getInstance() {
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }
}

优点:解决了上一种实现方式的线程不安全问题

缺点:synchronized 对整个 getInstance() 方法都进行了同步,每次只有一个线程能够进入该方法,并发性能极差

5、双重检查锁

// 线程安全
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 关键字的作用:

6、静态内部类

// 线程安全
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

7、枚举

// 线程安全
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 序列化机制

java.io.ObjectOutputStream 是Java实现序列化的关键类,它可以将一个对象转换成二进制流,然后可以通过 ObjectInputStream 将二进制流还原成对象。具体的序列化过程不是本文的重点,在此仅列出几个要点。

Java 序列化机制的要点

序列化破坏单例模式的解决方案

根据上面对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.<init>(HungrySingleton.java:20)
    ... 5 more

注意,上述方法针对饿汉式单例模式是有效的,但对懒汉式的单例模式是无效的,懒汉式的单例模式是无法避免反射攻击的!

为什么对饿汉有效,对懒汉无效?因为饿汉的初始化是在类加载的时候,反射一定是在饿汉初始化之后才能使用;而懒汉是在第一次调用 getInstance() 方法的时候才初始化,我们无法控制反射和懒汉初始化的先后顺序,如果反射在前,不管反射创建了多少对象,instance都将一直为null,直到调用 getInstance()

事实上,实现单例模式的唯一推荐方法,是使用枚举类来实现。

为什么推荐使用枚举单例

写下我们的枚举单例模式

package com.singleton;

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.<init>()
    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
        });
    }
}

通过反编译后代码我们可以看到,ublic final class T extends Enum,说明,当我们使用enmu来定义一个枚举类型的时候,编译器会自动帮我们创建一个final类型的类继承Enum类,所以枚举类型不能被继承。

那么,为什么推荐使用枚举单例呢?

1. 枚举单例写法简单

2. 线程安全&懒加载

代码中 INSTANCE 变量被 public static final 修饰,因为static类型的属性是在类加载之后初始化的,JVM可以保证线程安全;且Java类是在引用到的时候才进行类加载,所以枚举单例也有懒加载的效果。

3. 枚举自己能避免序列化攻击

为了保证枚举类型像Java规范中所说的那样,每一个枚举类型极其定义的枚举变量在JVM中都是唯一的,在枚举类型的序列化和反序列化上,Java做了特殊的规定。

在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.Enum的valueOf方法来根据名字查找枚举对象。同时,编译器是不允许任何对这种序列化机制的定制,因此禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法。 我们看一下Enum类的valueOf方法:

    public static <T extends Enum<T>> T valueOf(Class<T> 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对序列化有保证。

4. 枚举能够避免反射攻击,因为反射不支持创建枚举对象

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;
    }

单例模式总结

单例模式作为一种目标明确、结构简单、理解容易的设计模式,在软件开发中使用频率相当高,在很多应用软件和框架中都得以广泛应用。

单例模式的主要优点

单例模式的主要缺点

适用场景

单例模式的典型应用

JDK Runtime 饿汉单例

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() {
    }
    //....
}

API 介绍

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) 启用/禁用方法调用跟踪。

AWT Desktop 容器单例

Desktop 类允许 Java 应用程序启动已在本机桌面上注册的关联应用程序,以处理 URI 或文件。支持的操作包括:

Desktop 通过一个容器来管理单例对象

public class Desktop {
    // synchronized 同步方法
    public static synchronized Desktop getDesktop(){
        if (GraphicsEnvironment.isHeadless()) throw new HeadlessException();
        if (!Desktop.isDesktopSupported()) {
            throw new UnsupportedOperationException("Desktop API is not " + "supported on the current platform");
        }
        sun.awt.AppContext context = sun.awt.AppContext.getAppContext();
        Desktop desktop = (Desktop)context.get(Desktop.class); // 获取单例对象
        // 存在则返回,不存在则创建,创建后put进容器
        if (desktop == null) {
            desktop = new Desktop(); 
            context.put(Desktop.class, desktop);
        }
        return desktop;
    }

AppContext 中有一个 HashMap 对象table,是实际的容器对象

private final Map<Object, Object> table = new HashMap();

spring AbstractFactoryBean

AbstractFactoryBean 类

public final T getObject() throws Exception {
    if (this.isSingleton()) {
        return this.initialized ? this.singletonInstance : this.getEarlySingletonInstance();
    } else {
        return this.createInstance();
    }
}

private T getEarlySingletonInstance() throws Exception {
    Class<?>[] ifcs = this.getEarlySingletonInterfaces();
    if (ifcs == null) {
        throw new FactoryBeanNotInitializedException(this.getClass().getName() + " does not support circular references");
    } else {
        if (this.earlySingletonInstance == null) {
            // 通过代理创建对象
            this.earlySingletonInstance = Proxy.newProxyInstance(this.beanClassLoader, ifcs, new AbstractFactoryBean.EarlySingletonInvocationHandler());
        }
        return this.earlySingletonInstance;
    }
}

Mybatis ErrorContext ThreadLocal

ErrorContext 类,通过 ThreadLocal 管理单例对象,一个线程一个ErrorContext对象,ThreadLocal可以保证线程安全

public class ErrorContext {
    private static final ThreadLocal<ErrorContext> LOCAL = new ThreadLocal<ErrorContext>();
    private ErrorContext() {
    }
    
    public static ErrorContext instance() {
        ErrorContext context = LOCAL.get();
        if (context == null) {
          context = new ErrorContext();
          LOCAL.set(context);
        }
        return context;
    }
    //...
}

参考:
http://www.hollischuang.com/archives/197
https://www.cnblogs.com/chiclee/p/9097772.html
https://blog.csdn.net/abc123lzf/article/details/82318148

后记

欢迎评论、转发、分享,您的支持是我最大的动力

更多内容可访问我的个人博客:http://laijianfeng.org

关注【小旋锋】微信公众号,及时接收博文推送

关注_小旋锋_微信公众号
上一篇下一篇

猜你喜欢

热点阅读