单例模式

Posted mabaoqing

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了单例模式相关的知识,希望对你有一定的参考价值。

核心及优点

  • 保证只有一个实例存在,避免重复创建造成资源浪费,有时候也是为了避免多个不同的实例导致不一致的行为

技术图片

技术图片

饿汉式实现

  1. 构造器私有化,避免外部直接创建对象
  2. 声明一个私有的静态属性,同时创建该对象
  3. 创建一个对外的公共的静态方法访问该对象
public class SingletonHungry {
     // 1.构造器私有化,避免外部直接创建对象
     private SingletonHungry() {}
     // 2.创建一个静态实例,类初始化时立即加载这个对象(类加载的过程默认是线程安全的(JVM))
     private static SingletonHungry instance = new SingletonHungry();
     // 3.创建一个对外的公共的静态方法访问该变量,方法没有同步,效率较高
     public static SingletonHungry getInstance() {
           return instance;
     }
}

懒汉式实现

  1. 构造器私有化,避免外部直接创建对象
  2. 声明一个私有的静态引用
  3. 创建一个对外的公共的静态方法访问该引用变量,如果该变量没有对象,则创建一个
  4. 存在线程安全问题
  5. 可以实现延迟加载,又加懒加载,真正用到的时候才创建对象
public class Singleton {
  // 1.构造器私有化,避免外部直接创建对象
  private Singleton() {}
  // 2.创建一个静态引用

  private static Singleton instance = null;

  // 3.创建一个对外的公共的静态方法访问该变量,如果变量没有对象,则创建一个,需要同步,效率较低
  // 简单实现1
  public static synchronized Singleton getInstance() {
     if(null == instance) {
         instance = new Singleton();
     }
     return instance;
  }
 
  // 双重检测锁实现2(有问题,不建议):Thread, double checking
  public static Singleton getInstance2() {
     if(null == instance) {
        synchronized (Singleton.class) {
           if (null == instance) {
              instance = new Singleton();
           }
       }
     }
     return instance;
  }
}

静态内部类实现

兼备了高效并发调用与延迟加载的优点

public class Singleton {
     private Singleton() {}
     private static class Inner {
           private static final Singleton instance = new Singleton();
     }

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

技术图片

枚举实现

  • 实现简单
  • 枚举本身就是单例模式
  • 可以防止反射和反序列化操作
  • 没有延迟加载的功能
public enum SingletonEnum {
     // 定义一个枚举元素,代表一个实例
     INSTANCE;

     // 可以有自定义操作
     public void operate() {}
}

几种单例模式实现方式的比较

技术图片

常见应用场景

技术图片

利用反射测试单例模式漏洞

/** 
 * 测试使用反射构造对象 
 */
private static void testReflect() throws Exception {
     SingletonLazy s1 = SingletonLazy.getInstance();
     SingletonLazy s2 = SingletonLazy.getInstance();
     System.out.println(s1 == s2);
     // 利用反射获取Singleton的Class
     Class<SingletonLazy> clazz = (Class<SingletonLazy>) Class.forName("singleton.SingletonLazy");
     // 获取Singleton类中的构造方法
     Constructor<SingletonLazy> constructor = clazz.getDeclaredConstructor();
     // 设置访问权限,允许访问私有属性
     constructor.setAccessible(true);
     SingletonLazy s3 = constructor.newInstance();
     SingletonLazy s4 = constructor.newInstance();
     System.out.println(s3 == s4);
}

// 防止操作:在单例类的私有构造器中添加对象不为空的处理操作,如抛出异常等
private SingletonLazy() {
     if (instance != null) {
          throw new RuntimeException("构造器私有了,你哪来的对象?");
     }
}

利用反序列化测试单例模式漏洞

/**
 * 测试使用反序列化构造对象
 */
private static void testSerializ() throws Exception {
     SingletonLazy s1 = SingletonLazy.getInstance();
     // 输出流
     FileOutputStream fis = new FileOutputStream("src/singleton/a.txt");
     ObjectOutputStream oos = new ObjectOutputStream(fis);
     oos.writeObject(s1);
     oos.close();
     // 输入流
     ObjectInputStream ois = new ObjectInputStream(new FileInputStream("src/singleton/a.txt"));
     SingletonLazy s3 = (SingletonLazy) ois.readObject();
     ois.close();
     System.out.println(s1 == s3);
}

/**
 * 防止操作:反序列化时,在单例类中定义 Object readResolve() 方法并指定返回对象,
 * 则通过输入输出流获取对象时直接调用此方法以防止获取不同的对象(实际是回调)
 */
private Object readResolve() {
     return instance;
}

测试多线程环境下不同方式下创建对象的效率

private static void testEfficiency() throws InterruptedException {
     long start = System.currentTimeMillis();
     // 使用 CountDownLatch 监控线程的执行状态
     CountDownLatch countDownLatch = new CountDownLatch(10);
     // 多线程开始执行
     for (int i = 0; i < 10; i++) {
           new Thread(new Runnable() {
                public void run() {

                     for (int i = 0; i < 100000; i++) {

                           // Object o = SingletonHungry.getInstance();  //15ms

                           // Object o = SingletonLazy.getInstance();    //65ms

                           // Object o = SingletonInner.getInstance();   //47ms

                           Object o = SingletonEnum.INSTANCE;                //16ms
                     }
                     // 执行完一个线程,将总线程数量 -1
                     countDownLatch.countDown();
                }
           }).start();
     }
     // 阻塞当前线程(main),直到计数器变为0才会继续向下执行
     countDownLatch.await();
     long end = System.currentTimeMillis();
     System.out.println("总耗时:" + (end - start) + "ms");
}

以上是关于单例模式的主要内容,如果未能解决你的问题,请参考以下文章

常用代码片段

性能比较好的单例写法

片段作为 Android 中的单例

单例片段或保存网页视图状态

你熟悉的设计模式都有哪些?写出单例模式的实现代码

单例模式以及静态代码块