Java-设计模式-单例模式

单例模式

从单例加载的时机区分,有懒汉模式/饥饿模式。

从实现方式区分有双重检查模式,内部类模式/Enum模式/Map模式等。在《Effective Java》中,作者提出利用Enum时实现单例模式的最佳实践。

内容概要

  1. 实现单例模式的几个关键点

  2. 利用Enum实现单例模式

  3. 结论

实现单例模式的几个关键点

为了实现单例模式,其核心就是确保单例对象的唯一性。需要充电关注几个关键点。

  1. 无法通过new来随意创建对象,构造函数为private
  2. 提供获取唯一实例对象的方法,通常是getInstance
  3. 多线程并发的情况下保证维一
  4. 避免反射创建单例对象(反射攻击)
  5. 避免通过序列化创建单例对象(如果单例类实现了Serializable)

利用Enum实现单例模式

利用Enum的天然属性,可以有效的保证上面的几个关键点。它属于饿汉模式的单例实现。

实现代码

Java代码

复制代码
/**
 * 使用枚举实现单例。
 */
public enum EnumSingleton {
    INSTANCE; // 唯一的实例对象

    public static EnumSingleton getInstance() {
        return INSTANCE;
    }
    
    // 单例对象的属性对象
    private Object obj = new Object();

    public Object getObj() {
        return obj;
    }

    /**
     * 单例提供的对外服务。
     */
    public Object getFactoryService() {
        return obj;
    }
}

反编译代码

复制代码
// 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.ws.pattern.singleton;


public final class EnumSingleton extends Enum
{

    public static EnumSingleton[] values()
    {
        return (EnumSingleton[])$VALUES.clone();
    }

    public static EnumSingleton valueOf(String name)
    {
        return (EnumSingleton)Enum.valueOf(com/ws/pattern/singleton/EnumSingleton, name);
    }
    
    // 无法通过new来随意创建对象,构造函数为private.
    private EnumSingleton(String s, int i)
    {
        super(s, i);
        obj = new Object();
    }

    // 提供获取唯一实例对象的方法,通常是getInstance
    public static EnumSingleton getInstance()
    {
        return INSTANCE;
    }

    public Object getObj()
    {
        return obj;
    }

    public Object getFactoryService()
    {
        return new Object();
    }

    // 提供获取唯一实例对象的方法,通常是getInstance
    // 也可以直接获取到INSTANCE,但是获取到的都是一个对象
    public static final EnumSingleton INSTANCE;
    private Object obj;
    private static final EnumSingleton $VALUES[];

    // 静态代码中实例化对象,多线程并发的情况下保证唯一,属于饿汉模式
    static 
    {
        INSTANCE = new EnumSingleton("INSTANCE", 0);
        $VALUES = (new EnumSingleton[] {
            INSTANCE
        });
    }
}

从反编译代码中我们可以看到Enum的本质:

  1. 枚举本质上是final类
  2. 定义的枚举值实际上就是一个枚举类的不可变对象(比如这里的INSTALL)
  3. 在Enum类加载的时候,就已经实例化了这个对象
  4. 无法通过new来创建枚举对象

Enum实现单例模式的几个关键点验证

在反编译代码中,对前三个关键点已经做了明确的保证,下面看看后面两个序列化攻击和反射攻击是如何保证的。

  1. 避免反射出创建单例对象(反射攻击)

    从反编译代码中,我们可以看到,枚举的私有构造函数如下所示

    java 复制代码
    private EnumSingleton(String s, int i);

    那我们来尝试利用反射创建对象,

    java 复制代码
    /**
     * 反射攻击。
     * 由于Enum天然的不允许反射创建实例,所以可以完美的防范反射攻击。
     */
    private static void reflectionAttack() {
        System.out.println("反射攻击单例对象-----------开始");
    
        try {
            Constructor con = EnumSingleton.class.getDeclaredConstructor(String.class, int.class);
            con.setAccessible(true);
            Object obj = con.newInstance("INSTANCE", 0); // 反射新建对象以破坏单例
            System.out.println(obj);
            System.out.println(EnumSingleton.getInstance());
        } catch (Exception e) {
            e.printStackTrace();
        }
    
        System.out.println("反射攻击单例对象-----------结束");
    }

    上看的代码运行后,会抛出异常java.lang.IllegalArgumentException: Cannot reflectively create enum objects

    复制代码
    Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
        at java.lang.reflect.Constructor.newInstance(Constructor.java:416)
        at com.ws.pattern.singleton.EnumSingletonAppMain.reflectionAttack(EnumSingletonAppMain.java:43)
        at com.ws.pattern.singleton.EnumSingletonAppMain.main(EnumSingletonAppMain.java:9)

    从异常可以看出来,newInstance抛出了异常。推测Java反射是不允许创建Enum对象的,看看源码Constructor.java中的newInstance方法,存在处理Enum类型实例化的一行判断代码if ((clazz.getModifiers() & Modifier.ENUM) != 0),满足这个条件就抛出异常。newInstance的JDK代码如下:

    java 复制代码
    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);
            }
        }
        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;
    }

    原来反射机制不允许实例化Enum类型的对象,自然挡住了反射攻击。

2.避免通过序列化创建单例对象(如果单例类实现了Serializable)(序列化攻击)

序列化对象后,如果执行反序列化,也可以创建一个对象。利用此机制来尝试创建一个新的Enum对象。

java 复制代码
/**
 * 序列化攻击
 * 需要在单例类中增加read
 */
private static void serializableAttack() {
    System.out.println("序列化攻击单例对象-----------开始");
    EnumSingleton singleton = EnumSingleton.getInstance();
    System.out.println(singleton);
    try {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("./EnumSingleton.out"));
        oos.writeObject(singleton);
        ObjectInputStream ois = new ObjectInputStream((new FileInputStream("./EnumSingleton.out")));
        Object obj = ois.readObject(); // 这里利用反序列化创建对象
        System.out.println(obj);
    } catch (IOException e) {
        e.printStackTrace();
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }
    System.out.println("序列化攻击单例对象-----------结束");
}

从执行结果来看,拿到了相同的对象。执行结果如下:

java 复制代码
序列化攻击单例对象-----------开始
INSTANCE
INSTANCE
序列化攻击单例对象-----------结束

来撸代码吧,跟踪进入ois.readObject(),会进入ObjectInputStream.readObject0方法。其中会解析class的二进制,根据class的文件定义,分别解析不同类型的字段。重点关注case TC_ENUM:如下所示:

java 复制代码
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);

    case TC_BLOCKDATA:
    case TC_BLOCKDATALONG:
        if (oldMode) {
            bin.setBlockDataMode(true);
            bin.peek();             // force header read
            throw new OptionalDataException(
                bin.currentBlockRemaining());
        } else {
            throw new StreamCorruptedException(
                "unexpected block data");
        }

    case TC_ENDBLOCKDATA:
        if (oldMode) {
            throw new OptionalDataException(true);
        } else {
            throw new StreamCorruptedException(
                "unexpected end of block data");
        }

    default:
        throw new StreamCorruptedException(
            String.format("invalid type code: %02X", tc));
}

进入readEnum方法,重点关注Enum.valueOf方法。如下所示:

java 复制代码
/**
 * Reads in and returns enum constant, or null if enum type is
 * unresolvable.  Sets passHandle to enum constant's assigned handle.
 */
private Enum<?> readEnum(boolean unshared) throws IOException {
    if (bin.readByte() != TC_ENUM) {
        throw new InternalError();
    }

    ObjectStreamClass desc = readClassDesc(false);
    if (!desc.isEnum()) {
        throw new InvalidClassException("non-enum class: " + desc);
    }

    int enumHandle = handles.assign(unshared ? unsharedMarker : null);
    ClassNotFoundException resolveEx = desc.getResolveException();
    if (resolveEx != null) {
        handles.markException(enumHandle, resolveEx);
    }

    String name = readString(false);
    Enum<?> result = null;
    Class<?> cl = desc.forClass();
    if (cl != null) {
        try {
            @SuppressWarnings("unchecked")
            Enum<?> en = Enum.valueOf((Class)cl, name); // 这里根据name和class拿到Enum实例。这里的name="INSTANCE"
            result = en;
        } catch (IllegalArgumentException ex) {
            throw (IOException) new InvalidObjectException(
                "enum constant " + name + " does not exist in " +
                cl).initCause(ex);
        }
        if (!unshared) {
            handles.setObject(enumHandle, result);
        }
    }

    handles.finish(enumHandle);
    passHandle = enumHandle;
    return result;
}

再跟进Enum.valueOf方法。代码如下:

java 复制代码
    /**
     * Returns the enum constant of the specified enum type with the
     * specified name.  The name must match exactly an identifier used
     * to declare an enum constant in this type.  (Extraneous whitespace
     * characters are not permitted.)
     *
     * <p>Note that for a particular enum type {@code T}, the
     * implicitly declared {@code public static T valueOf(String)}
     * method on that enum may be used instead of this method to map
     * from a name to the corresponding enum constant.  All the
     * constants of an enum type can be obtained by calling the
     * implicit {@code public static T[] values()} method of that
     * type.
     *
     * @param <T> The enum type whose constant is to be returned
     * @param enumType the {@code Class} object of the enum type from which
     *      to return a constant
     * @param name the name of the constant to return
     * @return the enum constant of the specified enum type with the
     *      specified name
     * @throws IllegalArgumentException if the specified enum type has
     *         no constant with the specified name, or the specified
     *         class object does not represent an enum type
     * @throws NullPointerException if {@code enumType} or {@code name}
     *         is null
     * @since 1.5
     */
    public static <T extends Enum<T>> T valueOf(Class<T> enumType,
                                                String name) {
        // 从enumConstantDirectory()中根据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);
    }

enumConstantDirectory()是Class的方法,其本质是从Class.java的enumConstantDirectory属性中获取。代码如下:

java 复制代码
private volatile transient Map<String, T> enumConstantDirectory = null;

也就是说,Enum中定义的Enum成员值都被缓存在了这个Map中,Key是成员名称(比如"INSTANCE"),Value就是Enum的成员对象。这样的机制天然保证了取到的Enum对象是唯一的。即使是反序列化,也是一样的。

结论

经过上面的分析,枚举的实现天然地支持了单例模式的特点,大大降低了单例的开发难度。

1. 双重检测锁的单例模式

Java 代码

java 复制代码
public class Singleton {
    //构造器私有化
    private Singleton() {
    }

    //Java多线程的happens-before原则,主要定义多线程可见性的问题
    //volatile 禁止指令重排
    private static volatile Singleton singleton = null;

    //所有的线程都可以不用争抢锁直接进入getSingleton
    public static Singleton getSingleton() {
    	//看当前对象有没有被构建,若是被构建了,直接跳出if返回,增强了性能
        if (singleton == null) {
        	//真正构建对象的时候才进行同步操作
            synchronized (Singleton.class) {
            	//防止对象重复构建。
            	//比如a线程已经进入了此代码,但是线程b也拿到了上面的锁,这样a和b都会new一个对象出来,做一次判空检测是不是已经构建了
                if (singleton == null) {
                	//在指令层面,这句话不是一个原子操作
                	//1.分配内存
                	//2.初始化对象
                	//3.对象指向内存地址
                	//真正执行的时候,虚拟机为了效率可能会进行指令重排,比如1、3、2
                	//这样多线程环境下会出现问题。比如线程a执行顺序1、3、2,到3的时候,线程b判断 singleton == null 为false,
                	//但是此时对象还未初始化,因此b线程返回的对象是个未初始化的对象
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

2. 单例模式之静态内部类

Java 代码

复制代码
public class Singleton{
	
	private Singleton(){}
	
	private static class SingletonHodler{
		public static Singleton instance = new Singleton();
	}
	
	public static Singleton getInstance(){
		return SingletonHodler.instance;
	}
}
  1. 饿汉式是只要Singleton类被加载就会实例化,没有懒加载

  2. 静态内部类方式是在需要实例化时,调用getInstance方法,才会加载SingletonHolder类,实例化Singleton由于类的静态属性只会在第一次加载类的时候初始化,所以在这里我们也保证了线程的安全性,所以通过这种静态内部类的方式解决了资源浪费和性能的问题

相关推荐
yaoxin52112316 分钟前
131. Java 泛型 - 目标类型与泛型推断
java
甜鲸鱼28 分钟前
在Maven多模块项目中进行跨模块的SpringBoot单元测试
spring boot·单元测试·maven
惊骇世俗王某人33 分钟前
1. 深入理解ArrayList源码
java·开发语言
用户403159863966342 分钟前
表达式并发计算
java·算法
SimonKing1 小时前
告别System.currentTimeMillis()!Java高精度计时最佳实践
java·后端·程序员
Dcs1 小时前
JUnit 5架构如何用模块化颠覆测试框架?
java
chanalbert1 小时前
Nacos 技术研究文档(基于 Nacos 3)
spring boot·分布式·spring cloud
肉肉不想干后端1 小时前
gRPC服务架构整合springboot部署实践指南
java
chanalbert1 小时前
Spring Cloud分布式配置中心:架构设计与技术实践
spring boot·spring·spring cloud
天天摸鱼的java工程师1 小时前
volatile关键字实战指南:八年Java开发者详解五大应用场景
java·后端