5. 单例模式(Singleton Pattern)

定义

  • 保证一个类仅有一个实例,并提供一个全局访问点

类型

  • 创建型

适用场景

  • 想确保任何情况下都绝对只有一个实例

优点

  • 在内存里只有一个实例,减少了内存的开销。特别是一个对象需要频繁的创建和销毁时,而且创建销毁时的性能又无法优化
  • 可以避免对资源的多重占用。例如我们对一个文件进行写操作,由于只有一个实例在内存中存在,可以避免对同一个文件进行写操作
  • 设置全局访问点,严格控制访问。对外不让new出来,只能通过我们的方法创建单例对象

缺点

  • 没有接口,扩展困难。如果想要扩展就得修改代码,基本上没有其他途径可以实现

重点

  • 私有构造器。为了禁止从单例类外部调用构造函数,来创建对象。为了达到这个目的,必须设置构造函数的权限为private。
  • 线程安全。必须保证
  • 延迟加载(lazy load)。我们想使用的时候再创建,就需要延迟加载了
  • 序列化和反序列化安全。如果需要对对象进行序列化反序列化,就必须保证其安全,否则就会对单例造成破坏
  • 反射。防止反射攻击

代码实例

懒汉式

  1. 说明:字面意思就是比较懒。在初始化的时候没有被创建的,而是做一个延迟加载,并且私有化构造器。
  2. LazySingleton code
/**
 * 懒汉式单例
 */
public class LazySingleton {
    //声明一个静态的要被单例的对象
    private static LazySingleton lazySingleton = null;

    //私有化构造器
    private LazySingleton() {
    }

    //获取单例对象的方法
    public static LazySingleton getInstance(){
        //做一个空判断如果为null创建对象,反之返回lazySingleton对象
        if (lazySingleton == null){
            lazySingleton = new LazySingleton();
        }
        return lazySingleton;
    }
}

这种懒汉式单例在单线程的时候是ok的,但是多线程的情况下是线程不安全的。

  1. Test
  • 模拟单线程情况
public class Test {
    public static void main(String[] args) {
        LazySingleton lazySingleton = LazySingleton.getInstance();
        System.out.println(lazySingleton);
    }
}

执行结果:

LazySingleton@45ee12a7
  • 模拟多线程情况
    为了更好的看出效果我们修改一下getInstance()
    //获取单例对象的方法
    public static LazySingleton getInstance(){
        //做一个空判断如果为null创建对象,反之返回lazySingleton对象
        if (lazySingleton == null){
            try {
            	//问题放大
                Thread.sleep(2);
            }catch (Exception e){
                e.printStackTrace();
            }
            lazySingleton = new LazySingleton();
        }
        return lazySingleton;
    }

执行测试代码

public class Test {
    public static void main(String[] args) {
        ExecutorService threadPool = Executors.newFixedThreadPool(2);
        for (int i = 0; i< 2; i++) {
            threadPool.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName()+":"+LazySingleton.getInstance());
                }
            });
        }
        threadPool.shutdown();
    }
}

执行结果

pool-1-thread-2:com.test.principle.pattern.creational.singleton.LazySingleton@23eff874
pool-1-thread-1:com.test.principle.pattern.creational.singleton.LazySingleton@a49a412

从结果来看,创建两个不同的实例。这就是所谓的线程安全问题。
原因我们也说过了。线程1执行到if (lazySingleton == null),读取了lazySingleton 为null,然后cpu就被线程2抢去了,此时,线程1还没有对lazySingleton 进行实例化。因此,线程2读取lazySingleton 时仍然为null,于是,它对lazySingleton 进行实例化了。然后,cpu就被线程1抢去了。此时,线程1由于已经读取了lazySingleton 的值并且认为它为null,所以,再次对lazySingleton 进行实例化。所以,线程1和线程2返回的不是同一个实例。

改进方法

对于懒汉式线程安全的问题我们有几种改进方案

synchronized改进方式

在我们的getInstance()上 添加 synchronized关键字修饰,使这个方法变成同步方法
简单提一下synchronized关键字:synchronized添加到静态方法上,相当于锁的是这个方法所在的类的class文件,如果不是static修饰的方法,则锁的是在堆内存中生成的对象。
代码

/**
 * 懒汉式单例(synchronized修饰getInstance())
 */
public class LazySingleton {
    //声明一个静态的要被单例的对象
    private static LazySingleton lazySingleton = null;

    //私有化构造器
    private LazySingleton() {
    }

    public synchronized static LazySingleton getInstance(){
        if (lazySingleton == null){
            lazySingleton = new LazySingleton();
        }
        return lazySingleton;
    }

}

重新执行我们模拟多线程代码,执行结果

pool-1-thread-2:com.test.principle.pattern.creational.singleton.LazySingleton@f710c85
pool-1-thread-1:com.test.principle.pattern.creational.singleton.LazySingleton@f710c85

该方法虽然解决了线程安全问题。但是同步锁呢 比较消耗资源,有一个加锁解锁的开销,而且这里synchronized修饰static方法的时候,锁的是这个class,锁的范围非常大,对性能也会有影响。还有就是,每个线程去执行getInstance()时都要先获得锁再去执行方法体,没有锁线程就是一个阻塞状态的,这样变得有点像串行了,所以弊端还是非常大的。因此我们还需要改进。

Double Check (双重检查)方式

代码

/**
 *懒汉式单例(Double Check双重检查方式)
 */
public class LazyDoubleCheckSingleton {
    //声明一个静态的要被单例的对象
    private static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;

    //私有化构造器
    private LazyDoubleCheckSingleton() {
    }

    public static LazyDoubleCheckSingleton getInstance(){
        if (lazyDoubleCheckSingleton == null){
            synchronized (LazyDoubleCheckSingleton.class){
                if (lazyDoubleCheckSingleton == null) {
                    lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
                }
            }
        }
        return lazyDoubleCheckSingleton;
    }
}

这样做呢,就是所有的线程都会进到getInstance()的方法体中,如果不为null就直接返回实例,如果为null那也会只有一个线程进入到同步代码块中,这样可以大幅的降低synchronized修饰在getInstance()带来的性能开销。

在同步代码块中lazyDoubleCheckSingleton 实例化之前进行判断是否为空 ,是为了返回同一个实例对象。如果不加,例如有线程1和线程2,线程1读取lazyDoubleCheckSingleton 值为null,此时cpu被线程2抢去了,线程2再来判断lazyDoubleCheckSingleton 值为null,于是,它开始执行同步代码块中的代码,对lazyDoubleCheckSingleton 进行实例化。此时,线程2获得cpu,由于线程1之前已经判断过lazyDoubleCheckSingleton 值为null了,于是开始执行它后面的同步代码块代码。它也会去对lazyDoubleCheckSingleton 进行实例化。

看上去这个实现方式是非常完美的,当多个线程的时候我们通过方法体内的加锁,来保证只有一个线程,能创建对象,当创建好后,再调用getInstance()的时候都不会在需要加锁,直接返回已创建好的实例对象。但是这个过程中还是存在安全隐患。因为,这里会涉及到一个指令重排序问题

  • 首先我们先分析一下 lazyDoubleCheckSingleton = new lazyDoubleCheckSingleton();的步骤
  1. 申请一块内存空间

  2. 在这块空间里实例化对象

  3. 设置lazyDoubleCheckSingleton 指向刚分配的内存地址

    这是我们理想的步骤。但是在java 语言规范中说所有的线程在执行java程序时必须遵从,intra-thread semantics 保证重排序不会改变单线程内的程序执行结果。在我们的这个程序中步骤2和3互换,并没有改变单线程内的程序结果,所以符合规范,在真正执行的时候步骤就有可能变成1->3->2
    假设按1->3->2执行,这样存在的问题就是,假设线程1 执行完分配内存地址还没有在这块空间里实例化对象的时候,线程2判断lazyDoubleCheckSingleton为null的条件就出问题了,因为已经不是空了,因此,线程2也就不会实例化对象了。

  • 解决这个问题的方式有两种方式
  1. 不允许步骤2和3重排序
    我们在声明lazyDoubleCheckSingleton 的时候 添加volatile关键字 来禁止重排序
    简单提一下volatile 关键字,在多线程的时候是有共享内存的,在添加volatile关键字后所有的线程就都可以看到共享内存的最新状态,在进行写操作的时候,将当前缓存行的数据写回到内存,这时候,会使得其他内存里缓存了该内存地址的数据无效,重新从共享内存同步数据这样就保证了内存的可见性。这里主要是用的java的缓存一致性协议。当处理器发现我的缓存无效了,所以在进行操作的时候,会重新从系统内存中,把数据读到处理器的缓存里
public class LazyDoubleCheckSingleton {
    //声明一个静态的要被单例的对象
    private volatile static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;

    //私有化构造器
    private LazyDoubleCheckSingleton() {
    }

    public static LazyDoubleCheckSingleton getInstance(){
        if (lazyDoubleCheckSingleton == null){
            synchronized (LazyDoubleCheckSingleton.class){
                if (lazyDoubleCheckSingleton == null) {
                    lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
                }
            }
        }
        return lazyDoubleCheckSingleton;
    }
}
  1. 运行当前线程重排序,但是运行其他线程看到这个重排序
    通过静态内部类解决
静态内部类方式
/**
 * 静态内部类-单例 写法1
 */
public class StaticInnerClassSingleton {
    private StaticInnerClassSingleton(){
    }
    private static class InnerClass{
        private static StaticInnerClassSingleton staticInnerClassSingleton = new StaticInnerClassSingleton();
    }

    public static StaticInnerClassSingleton getInstance(){
        return InnerClass.staticInnerClassSingleton;
    }
}

原理:JVM在类的初始化阶段,也就是class加载后,并且在线程使用之前,都是类的初始化阶段,在执行类的初始化期间,JVM会获取一个Class对象的初始化锁,这个锁会同步,多个线程对一个类的初始化,基于这个特性,我们可以实现基于静态内部类,线程安全并且延迟加载的初始化方案。
那他是如何保证线程安全的呢,这里我们就要说一下类加载时机。
类加载时机根据java语言规范主要分为5中情况

  1. 有一个A类型的实例被创建
  2. A类中有一个声明的静态方法被调用
  3. A类中声明的一个静态成员被赋值
  4. A类中声明的一个静态成员被使用,并且该成员不是一个常量成员
  5. A类如果是一个顶级类,并且在这个类中有嵌套的断言语句

举个栗子:
假设有线程A和线程B。他们两个线程在首次试图获取Class对象初始化锁的时候,这个时候必然只有一个线程能获取到,假设线程A拿到了,并且执行静态内部类的初始化,对于静态内部类中staticInnerClassSingleton = new StaticInnerClassSingleton()的分配内存地址和在这块空间里实例化对象存在重排序, 但是线程1是无法感知的,因为线程1还处于一个等待状态。

静态内部类核心在于,InnerClass这个静态内部类的初始化锁,看哪个线程拿到,哪个线程就去初始化它

饿汉式

在类加载的时候就完成实例化
代码

/**
 * 饿汉式-单例写法1 直接初始化
 */
public class HungrySingleton {
    private final static HungrySingleton HUNGRY_SINGLETON = new HungrySingleton();
    private HungrySingleton(){
    }

    public static HungrySingleton getInstance(){
        return HUNGRY_SINGLETON;
    }
}
/**
 * 饿汉式-单例写法2 放到静态代码块中初始化
 */
public class HungrySingleton {
    private final static HungrySingleton HUNGRY_SINGLETON;
    static {
        HUNGRY_SINGLETON = new HungrySingleton();
    }
    private HungrySingleton(){
    }

    public static HungrySingleton getInstance(){
        return HUNGRY_SINGLETON;
    }
}

这个就是非常简单的饿汉式单例,优点呢就是写法简单,类加载的时候就完成了初始化,避免了线程同步问题,缺点就是,因为在类加载的时候就完成了初始化,没有延迟加载的效果。如果这个类从始至终我们的系统都没用过,还会造成内存的浪费。

反序列化破坏单例模式

验证

我们以饿汉式单例为例。

  1. 首先我们要让我们的HungrySingleton 实现Serializable接口
/**
 * 饿汉式-单例
 */
public class HungrySingleton implements Serializable{
    private final static HungrySingleton HUNGRY_SINGLETON = new HungrySingleton();
    private HungrySingleton(){
    }

    public static HungrySingleton getInstance(){
        return HUNGRY_SINGLETON;
    }
}
  1. 编写测试类
public class Test {
    public static void main(String[] args) throws Exception{
        //获取饿汉式单例对象
        HungrySingleton instance = HungrySingleton.getInstance();
        //假设我们把instance这个对象 序列化到一个文件中,再从文件中把这个对象取出来。取出来的对象还是原来的那个对象吗?
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_file"));
        oos.writeObject(instance);

        File file = new File("singleton_file");
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
        HungrySingleton newInstance = (HungrySingleton)ois.readObject();
        
        System.out.println(instance);
        System.out.println(newInstance);
        System.out.println(instance == newInstance);
    }
}

执行结果

com.test.principle.pattern.creational.singleton.HungrySingleton@12a3a380
com.test.principle.pattern.creational.singleton.HungrySingleton@27d6c5e0
false

从结果可以看出,我们已经违背了单例模式的一个初衷,通过序列化和反序列化拿到了不一样的对象。而我们希望的是同一个对象

解决方法

修改我们的单例类

/**
 * 饿汉式-单例
 */
public class HungrySingleton implements Serializable{
    private final static HungrySingleton HUNGRY_SINGLETON = new HungrySingleton();
    private HungrySingleton(){
    }

    public static HungrySingleton getInstance(){
        return HUNGRY_SINGLETON;
    }
   //添加readResolve方法(强制,换成其他方法名不生效)
    public Object readResolve(){
        return HUNGRY_SINGLETON;
    }
}

再执行我们的测试类,得出的执行结果

com.test.principle.pattern.creational.singleton.HungrySingleton@12a3a380
com.test.principle.pattern.creational.singleton.HungrySingleton@12a3a380
true

就此反序列化破坏单例模式的问题解决。其他单例的模式也是相同的。

原因

我们很好奇为什么定义一个**readResolve()**就解决的了这个问题。方法名必须是readResolve,别的就不生效。
首先我们得知道在反序列化的过程中到底发生了什么。
根据我们的测试代码对象的序列化过程通过ObjectOutputStream和ObjectInputputStream来实现的,那么带着刚刚的问题,分析一下ObjectInputputStream 的readObject 方法执行情况到底是怎样的。
首先我们DEBUG看一下readObject调用栈:
readObject—>readObject0—>readOrdinaryObject—>checkResolve、
接下来划重点
主要看readOrdinaryObject(boolean unshared)这个方法

   public class ObjectInputStream
        extends InputStream implements ObjectInput, ObjectStreamConstants {
 	//省略部分代码...
    private Object readOrdinaryObject(boolean unshared)
            throws IOException {
      	//省略部分代码.....
        Object obj;
        try {
        	//1.部分 
        	//首先isInstantiable()判断是否可以初始化
    		//如果为true,则调用newInstance()方法创建对象,这时创建的对象是不走构造函数的,是一个新的对象
            obj = desc.isInstantiable() ? desc.newInstance() : null;
        } catch (Exception ex) {
            throw (IOException) new InvalidClassException(
                    desc.forClass().getName(),
                    "unable to create instance").initCause(ex);
        }
		//省略部分代码....
		//2.部分
		//hasReadResolveMethod()会去判断,我们的InnerClassSingleton对象中是否有readResolve()方法
        if (obj != null &&
                handles.lookupException(passHandle) == null &&
                desc.hasReadResolveMethod()){
                //3.部分
            	//如果为true,则执行readResolve()方法,而我们在自己的readResolve()方法中 直接retrun InnerClassHelper.INSTANCE,所以还是返回的同一个对象,保证了单例
            	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);
            }
        }
        return obj;
    }
    //省略部分代码...
}
  • 第一部分代码
 obj = desc.isInstantiable() ? desc.newInstance() : null;

这里创建的这个obj对象,就是readOrdinaryObject()要返回的对象,也可以暂时理解为是ObjectInputStream的readObject返回的对象。
而调用的两个方法。

	desc.isInstantiable():如果一个serializable/externalizable的类可以在运行时被实例化,那么该方法就返回true。
	desc.newInstance():该方法通过反射的方式调用无参构造方法新建一个对象。

至此我们找到了,反序列化会破坏单例的原因 序列化会通过反射调用无参数的构造方法创建一个新的对象

  • 第二部分代码和第三部分代码
  desc.hasReadResolveMethod()
  Object rep = desc.invokeReadResolve(obj);
hasReadResolveMethod:如果实现了serializable 或者 externalizable接口的类中包含readResolve方法名则返回true、
invokeReadResolve:通过反射的方式调用要被反序列化的类的readResolve方法。

至此我们了解了为什么在单例类中中定义readResolve方法,并在该方法中指定要返回的对象的生成策略,就可以防止单例被破坏的原因。

在这个过程中 第一部分的代码一定会走的。只是在单例类中定义readResolve方法后没有返回第一部分实例化的对象。这个还是要注意一下的。
在我们的业务场景中如果有需要序列化反序列的单例,一定考虑一下反序列化对单例的影响。

反射攻击

验证

我们还是以饿汉式单例为例。
编写测试类

    public static void main(String[] args) throws Exception{
        //已正常的方式获取饿汉式单例对象
        HungrySingleton instance = HungrySingleton.getInstance();
        System.out.println(instance);

        //已反射的方式获取饿汉式单例对象
        Constructor constructor = HungrySingleton.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        HungrySingleton newInstance = (HungrySingleton)constructor.newInstance();
        System.out.println(newInstance);
        
        System.out.println(instance == newInstance);
    }

执行结果

com.test.principle.pattern.creational.singleton.HungrySingleton@45ee12a7
com.test.principle.pattern.creational.singleton.HungrySingleton@330bedb4
false

从结果来看我们用反射,也破坏了单例的初衷

解决方法

在我们的构造器中添加反射防御的代码

/**
 * 饿汉式-单例
 */
public class HungrySingleton implements Serializable{
    private final static HungrySingleton HUNGRY_SINGLETON = new HungrySingleton();
    private HungrySingleton(){
        if (HUNGRY_SINGLETON != null){
            throw new RuntimeException("duplicate instance create error!" + HungrySingleton.class.getName());
        }
    }

    public static HungrySingleton getInstance(){
        return HUNGRY_SINGLETON;
    }

    public Object readResolve(){
        return HUNGRY_SINGLETON;
    }
}

再执行测试代码,执行结果

com.test.principle.pattern.creational.singleton.HungrySingleton@45ee12a7
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.test.principle.pattern.creational.singleton.Test.main(Test.java:20)
Caused by: java.lang.RuntimeException: duplicate instance create error!com.test.principle.pattern.creational.singleton.HungrySingleton
	at com.test.principle.pattern.creational.singleton.HungrySingleton.<init>(HungrySingleton.java:12)
	... 5 more

我们发现 这样解决反射问题。这种方法只能解决类加载时初始化实例的单例实现方式(静态内部类的方式),延迟加载的实现方式并不能防止反射破坏

在我们真正的业务中,尽量不要用反射来创建实例

Enum 枚举单例

枚举类天然的可序列化机制,能够强有力的保证不会出现多种实例化的情况,即使在复杂的序列化情况下或者反射的攻击下,枚举类型的单例模式都没有问题。

code

/**
 * 枚举类单例模式
 */
public enum EnumInstance {
    INSTANCE;

    private Object data;

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }

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

测试序列化反序列化

测试代码

public class Test {
    public static void main(String[] args) throws Exception{
        //获取枚举单例对象
        EnumInstance instance = EnumInstance.getInstance();
        //设置枚举单例里的data
        instance.setData(new Object());
        //序列化
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_file"));
        oos.writeObject(instance);
        //反序列化
        File file = new File("singleton_file");
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
        EnumInstance newInstance = (EnumInstance)ois.readObject();

        //data是否是同一个对象
        System.out.println(instance.getData());
        System.out.println(newInstance.getData());
        System.out.println(instance.getData() == newInstance.getData());
    }
}

执行结果

java.lang.Object@4f3f5b24
java.lang.Object@4f3f5b24
true

通过结果我们可以看出,序列化反序列化后还是同一个对象。单例没有遭到破坏
跟之前的方式一样我们先看一下ObjectInputputStream 的readObject 方法的调用栈

readObject —> readObject0 —> readEnum —> checkResolve

接下来我们划重点,主要在readEnum(boolean unshared) 这个方法中,上代码

 public class ObjectInputStream
            extends InputStream implements ObjectInput, ObjectStreamConstants {
        //省略部分代码...
        private Enum<?> readEnum(boolean unshared) throws IOException {
  			//省略部分代码...
  			
			//1. 通过readString这个方法获取到枚举对象的名称
            String name = readString(false);
          	//2.声明一个为null的Enum
            Enum<?> result = null;
            Class<?> cl = desc.forClass();
            if (cl != null) {
                try {
                    //3.通过valueOf方法获取Enum,参数为class和name
                    @SuppressWarnings("unchecked")
                    Enum<?> en = Enum.valueOf((Class) cl, name);
                    //4. 赋值
                    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单例模式 序列化的时候只将 INSTANCE 这个名称输出,反序列化的时候再通过这个名称,查找对应的枚举类型,因此反序列化后的实例也会和之前被序列化的对象实例相同。

测试反射

测试代码

public class Test {
    public static void main(String[] args) throws Exception {
        Constructor constructor = EnumInstance.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        EnumInstance newInstance = (EnumInstance)constructor.newInstance();
        System.out.println(newInstance);
    }
}

执行结果

Exception in thread "main" java.lang.NoSuchMethodException: com.test.principle.pattern.creational.singleton.EnumInstance.<init>()
	at java.lang.Class.getConstructor0(Class.java:3082)
	at java.lang.Class.getDeclaredConstructor(Class.java:2178)
	at com.test.principle.pattern.creational.singleton.Test.main(Test.java:16)

通过执行结果我们可以看出,当我们用想要反射初始化的时候,EnumInstance.class.getDeclaredConstructor();报了一个NoSuchMethodException,也就是说在过获取构造器的时候并没有获得无参的构造器。为什么会这样呢。
我们首先看一下java.lang.Enum

//首先这是一个抽象类
public abstract class Enum<E extends Enum<E>>
        implements Comparable<E>, Serializable {
   	//省略部分代码...
   	//重点,Enum只有一个构造方法并没有无参构造器
    protected Enum(String name, int ordinal) {
        this.name = name;
        this.ordinal = ordinal;
    }
    //省略部分代码...
}

既然他没有无参构造器,我们就尝试用它有参的构造器反射试一下

public class Test {
    public static void main(String[] args) throws Exception {
        Constructor constructor = EnumInstance.class.getDeclaredConstructor(String.class,int.class);
        constructor.setAccessible(true);
        EnumInstance newInstance = (EnumInstance)constructor.newInstance("oiobee",666);
        System.out.println(newInstance);
    }
}

执行结果

Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
	at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
	at com.test.principle.pattern.creational.singleton.Test.main(Test.java:15)

我们发现虽然我们获取到了他的构造器,但是我们真正创建对象的时候,还是抛了一个IllegalArgumentException的异常出来,并且提示很明确的说不能反射创建枚举对象。我们可以看到这个异常是从Constructor这个类里抛出来的。那我们就进去看一下

public final class Constructor<T> extends Executable {
//省略部分代码....
    @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);
            }
        }
        //重点:这行判断就是看一下我们使用newInstance的时候目标类型是不是一个枚举类,是就直接抛异常
        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把枚举类当儿子不让它受到一点点的伤害
我们在看看java.lang.Enum的一部分代码,你就知道啥叫亲儿子了,你想克隆或者序列化Enum对象不存在的,人家直接给你抛异常

    protected final Object clone() throws CloneNotSupportedException {
        throw new CloneNotSupportedException();
    }
    //
    private void readObject(ObjectInputStream in) throws IOException,
        ClassNotFoundException {
        throw new InvalidObjectException("can't deserialize enum");
    }

    private void readObjectNoData() throws ObjectStreamException {
        throw new InvalidObjectException("can't deserialize enum");
    }

反编译

我们来反编译EnumInstance.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:   EnumInstance.java

package com.test.principle.pattern.creational.singleton;

//final代表这个类不可被继承
public final class EnumInstance extends Enum
{

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

    public static EnumInstance valueOf(String name)
    {
        return (EnumInstance)Enum.valueOf(com/test/principle/pattern/creational/singleton/EnumInstance, name);
    }

    //符合我们单例模式的要求私有构造器,不允许外部实例化
    private EnumInstance(String s, int i)
    {
        super(s, i);
    }

    public Object getData()
    {
        return data;
    }

    public void setData(Object data)
    {
        this.data = data;
    }

    public static EnumInstance getInstance()
    {
        return INSTANCE;
    }

    //类变量是静态
    public static final EnumInstance INSTANCE;
    private Object data;
    private static final EnumInstance $VALUES[];

    //通过静态代码块的方式来实例化INSTANCE,所以这个类在加载的时候就把INSTANCE给初始化了,所以它是线程安全的。这点跟我的饿汉式单例是很像的
    static 
    {
        INSTANCE = new EnumInstance("INSTANCE", 0);
        $VALUES = (new EnumInstance[] {
            INSTANCE
        });
    }
}

容器单例

和享元模式类型。我们可以使用容器来管理多个单例模式对象

/**
 * 容器单例类
 */
public class ContainerSingleton {

   	private ContainerSingleton(){
    }
    
    private static Map<String,Object> containerMap = new HashMap<String, Object>();

    public static void putInstance(String key, Object instance){
        if (StringUtils.isNotBlank(key) && instance != null){
            if (!containerMap.containsKey(key)){
                containerMap.put(key,instance);
            }
        }
    }

    public static Object getInstance(String key){
        return containerMap.get(key);
    }
}

这种写法很简单。但是这个单例模式有缺点,是一个平衡根据业务场景判断,假设,单例对象非常多,也可以考虑用一个容器来统一管理
优点:统一管理节省资源,map就相当于一个缓存
缺点:线程不安全
为什么是线程不安全的:本身hashmap就是线程不安全的,虽然用Hashtable,他就是线程安全的了,但是不建议使用,因为会影响性能,频繁去获取实例的时候,都会有同步锁。因为我们这个容器是静态的,而是会直接操作容器,所以用ConcurrentHashMap也并不是觉得的线程安全。

ThreadLocal"线程"单例

这个单例不是那么纯粹,它无法保证全局唯一,但是他可以线程唯一
code

/**
 * ThreadLocal线程单例
 */
public class ThreadLocalInstance {
    private static ThreadLocal<ThreadLocalInstance> threadLocalinstanceThreadLocal
            = new ThreadLocal<ThreadLocalInstance>(){
        //重写ThreadLocal的初始化方法
        @Override
        protected ThreadLocalInstance initialValue() {
            return new ThreadLocalInstance();
        }
    };

    //私有化构造器
    private ThreadLocalInstance(){
    }

    //获取实例
    public static ThreadLocalInstance getInstance(){
        return threadLocalinstanceThreadLocal.get();
    }
}

测试代码

public class Test {
    public static void main(String[] args) throws Exception {
        System.out.println(Thread.currentThread().getName()+":"+ ThreadLocalInstance.getInstance());
        System.out.println(Thread.currentThread().getName()+":"+ ThreadLocalInstance.getInstance());
        System.out.println(Thread.currentThread().getName()+":"+ ThreadLocalInstance.getInstance());
        ExecutorService threadPool = Executors.newFixedThreadPool(2);
        for (int i = 0; i< 2; i++) {
            threadPool.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName()+":"+ ThreadLocalInstance.getInstance());
                }
            });
        }
        threadPool.shutdown();
    }
}

执行结果

main:com.test.principle.pattern.creational.singleton.ThreadLocalInstance@45ee12a7
main:com.test.principle.pattern.creational.singleton.ThreadLocalInstance@45ee12a7
main:com.test.principle.pattern.creational.singleton.ThreadLocalInstance@45ee12a7
pool-1-thread-1:com.test.principle.pattern.creational.singleton.ThreadLocalInstance@1a6270ed
pool-1-thread-2:com.test.principle.pattern.creational.singleton.ThreadLocalInstance@2988d273

从结果来看,main线程获取的是同一个实例,而pool-1-thread-1和pool-1-thread-2获取的则是两个完全不同的实例对象
讲一下原因:
ThreadLocal会为每个线程创建一个独立的变量副本。 ThreadLocal是基于它里面一个静态内部类ThreadLocalMap来实现的所有我们调用get()的时候默认走的就是ThreadLocalMap不用指定key,他维持了线程间的隔离,ThreadLocal隔离了多个线程对数据的访问冲突,
对于多线程资源共享的问题,如果使用同步锁就是拿时间换空间的方式,因为要排队,如果使用ThreadLocal就是以空间换时间的方式,他会创建很多对象。至少在一个线程里会创建一个,但是对于这个线程他获取这个对象是唯一的,正如我们的main线程一样。里面拿到的对象都是同一个。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值