深入解析单例模式

单例模式

一、 单例的定义

确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。

二 、实现的通用基本步骤

- 定义一个静态成员变量
- 私有化构造器(防反射和防反序列化下面会讲)
- 提供一个静态方法,获取实例对象

三、各种方式,代码跑起来,gogo

描述:提供饿汉式、懒汉式、枚举、私有内部类(推荐)、双重检测锁实现代码。

  • 饿汉式
package com.tmq.designmodeldemo.singleton;

/**
 * 饿汉式
 * @Author tangmqb DCITS
 * @DATE 2020/7/23 10:10
 * @Version 1.0.0
 */
public class Demo1 {
    //类初始化时立即加载 天然线程安全
    private static Demo1 demo1 = new Demo1();

    private Demo1() {

    }
    //方法没有同步 效率高
    public static Demo1 getInstance() {
        return  demo1;
    }

}

  • 懒汉式
package com.tmq.designmodeldemo.singleton;

/**
 * 测试懒汉式
 *
 * @Author tangmqb DCITS
 * @DATE 2020/7/23 10:15
 * @Version 1.0.0
 */
public class Demo2 {

    private static Demo2 instance = null;

    private Demo2() {

    }
    //懒加载 同步方法 效率低 类初始化时较快 不加载对象 用的时候才加载
    public static synchronized Demo2 getInstance() {
        if (instance == null) {
            instance = new Demo2();
        }
        return instance;
    }
}
  • 私有内部静态类
package com.tmq.designmodeldemo.singleton;

import java.io.Serializable;

/**
 * 私有静态内部类 推荐使用
 * 这种方式:线程安全,调用效率高,并且实现了延时加载!
 * @Author tangmqb DCITS
 * @DATE 2020/7/23 10:24
 * @Version 1.0.0
 */
public class Demo3 implements Serializable {

    private Demo3() {

    }

    private static class innerDemo3 {
        public static final Demo3 instance = new Demo3();
    }

    //方法没有同步
    public static Demo3 getInstance() {
        return innerDemo3.instance;
    }
}
  • 双重检测锁 了解
package com.tmq.designmodeldemo.singleton;

/**
 * 双重检测锁 了解
 *
 * @Author tangmqb DCITS
 * @DATE 2020/7/23 10:33
 * @Version 1.0.0
 */
public class Demo4 {
    private static Demo4 instance = null;

    private Demo4() {

    }

    public static Demo4 getInstance() {
        if (instance == null) {
            Demo4 demo4;
            synchronized (Demo4.class) {
                demo4=instance;
                if (demo4 == null) {
                    synchronized (Demo4.class) {
                        if (demo4 == null) {
                            demo4=new Demo4();
                        }
                    }
                     instance=demo4;
                }
            }
        }
        return instance;
    }
}
  • 枚举
package com.tmq.designmodeldemo.singleton;

/**
 * 测试枚举式实现单例模式(没有延时加载)
 * @Author tangmqb
 * @DATE 2020/7/23 10:49
 */
public enum Demo5 {
    //这个枚举 本身就是单例
    INSTANCE;

    //添加自己需要的操作!
    public void singletonOperation(){
    }
}

四、效率测试

  • 我们来测试下各模式的效率,测试多线程环境下五种创建单例模式的效率
package com.tmq.designmodeldemo.singleton;

import java.util.concurrent.CountDownLatch;

/**
 * 测试各种单例模式的效率
 * @Author tangmqb DCITS
 * @DATE 2020/7/23 11:25
 * @Version 1.0.0
 */
public class ClientDemo3 {

    public static void main(String[] args) throws InterruptedException {
        long start = System.currentTimeMillis();
        int cutDownNum = 10;
      final CountDownLatch countDownLatch = new CountDownLatch(cutDownNum);
        for (int i = 0; i < cutDownNum; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {

                    for(int i=0;i<1000000;i++){
//                      Object o =  Demo1.getInstance();
                        Object o =  Demo4.getInstance();
                    }
                    countDownLatch.countDown();
                }
            }).start();
        }
        countDownLatch.await();	//main线程阻塞,直到计数器变为0,才会继续往下执行!
        long end = System.currentTimeMillis();
        System.out.println("总耗时:"+(end-start));
    }
}
运行结果:
双重检测锁:总耗时:17
内部静态类:总耗时:86
懒汉式:总耗时:204
饿汉式:总耗时:46
其他自行运行比较

五 反序列化与反射破解单例

描述:通过反序列化和反射可以破解单例模式,获取到多个对象,测试代码:

  • 测试
package com.tmq.designmodeldemo.singleton;

import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

/**
 * 测试反射及反序列化(类实现了Serializable接口)破解单例模式
 * 防止 见Demo6
 * @Author tangmqb DCITS
 * @DATE 2020/7/23 10:57
 * @Version 1.0.0
 */
public class ClientDemo2 {
    public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Demo3 instance1 = Demo3.getInstance();
        Demo3 instance2 = Demo3.getInstance();
        System.out.println("***********正常的单例获取**************");
        System.out.println("instance1==instance2:" + (instance1 == instance2));

        //通过反序列化破解 获取多个对象
        System.out.println("***********测试反序列化破解**************");
        FileOutputStream fos = new FileOutputStream("d:/a.txt");
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(instance1);
        oos.close();
        fos.close();
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("d:/a.txt"));
        Demo3 instance3 =  (Demo3) ois.readObject();
        System.out.println("instance3==instance2:" + (instance3 == instance2));

        //通过反射破解 获取多个对象
        System.out.println("***********测试反射破解**************");
        Class<Demo3> clazz =(Class<Demo3>) Class.forName("com.tmq.designmodeldemo.singleton.Demo3");
        Constructor<Demo3> c = clazz.getDeclaredConstructor(null);
        c.setAccessible(true);
        Demo3 instance4 = c.newInstance();
        Demo3 instance5 = c.newInstance();
        System.out.println("instance4==instance5:" + (instance4 == instance5));
        //防止反射及反序列化破解
//        System.out.println("***********测试防止反射及反序列化破解**************");
//        Class<Demo6> clazz =(Class<Demo6>) Class.forName("com.tmq.designmodeldemo.singleton.Demo6");
//        Constructor<Demo6> c = clazz.getDeclaredConstructor(null);
//        c.setAccessible(true);
//        Demo6 instance6 = c.newInstance();
//        Demo6 instance7 = c.newInstance();
//        System.out.println("instance6==instance7:" + (instance6 == instance7));


    }
}

运行结果:

 com.tmq.designmodeldemo.singleton.ClientDemo2
***********正常的单例获取**************
instance1==instance2:true
***********测试反序列化破解**************
instance3==instance2:false
***********测试反射破解**************
instance4==instance5:false
  • 防止反序列化和反射
package com.tmq.designmodeldemo.singleton;

import java.io.ObjectStreamException;
import java.io.Serializable;

/**
 * 防止反射和反序列化破解单例模式
 * @Author tangmqb DCITS
 * @DATE 2020/7/23 10:24
 * @Version 1.0.0
 */
public class Demo6 implements Serializable {

    private static Demo6 instance = null;

    private Demo6() {
        //防止通过反射构造器获取对象
        if (instance == null) {
            System.out.println("想通过反射破解,没门!");
            throw new RuntimeException();
        }
    }

    public static synchronized Demo6 getInstance() {
        if (instance == null) {
            instance = new Demo6();
        }
        return instance;
    }
    //反序列化时,如果定义了readResolve()则直接返回此方法指定的对象。而不需要单独再创建新对象!
    private Object readResolve() throws ObjectStreamException {
        return instance;
    }

}

六、总结

常见的五种单例模式实现方式

主要

饿汉式(线程安全,调用效率高。 但是,不能延时加载。)
懒汉式(线程安全,调用效率不高。 但是,可以延时加载。)

其他

双重检测锁式(由于JVM底层内部模型原因,偶尔会出问题。不建议使用)
静态内部类式(线程安全,调用效率高。 但是,可以延时加载)
枚举式(线程安全,调用效率高,不能延时加载。并且可以天然的防止反射和反序列化漏洞!)

如何选用?

单例对象 占用 资源 少,不需要 延时加载:
枚举式 好于 饿汉式
单例对象 占用 资源 大,需要 延时加载:
静态内部类式 好于 懒汉式

七、其他

需要考虑对象的复制情况。在Java中,对象默认是不可以被复制的,若实现了 Cloneable接口,并实现了clone方法,则可以直接通过对象复制方式创建一个新对象,对象复制是不用调用类的构造函数,因此即使是私有的构造函数,对象仍然可以被复制。在一般情况下,类复制的情况不需要考虑,很少会出现一个单例类会主动要求被复制的情况,解决该问题的最好方法就是单例类不要实现Cloneable接口。

end

评论