Matrix42

不饱食以终日,不弃功于寸阴

GOF23设计模式之 : 单例模式(二)

核心作用

保证一个类只有一个实例,并且提供一个访问该实例的全局访问点

常见应用场景

  • Windows的Task Manager(任务管理器)就是很典型的单例模式

  • Windows的Recycle Bin(回收站)也是典型的单例应用.在整个系统运行过程中,回收站一直维护着仅有的一个实例

  • 项目中,读取配置文件的类,一般也只有一个对象.没有必要每次使用配置文件数据都new一个对象去读取

  • 网站的计数器,一般也是采用单例模式实现,否则难以同步

  • 应用程序的日志应用,一般都何用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加

  • 数据库连接池的设计一般也是采用单例模式,因为数据库连接是一种数据库资源.

  • 操作系统的文件系统,也是大的单例模式实现的具体例子,一个操作系统只能有一个文件系统

  • Application也是单例的典犁府用(Servlet中会涉及到)

  • 在Spring中,每个Bean默认就是单例的,这样做的优点是Spring容器可以管理

  • 在Servlet编程中,每个Servlet也是单例

  • 在spring MVC架构/struts1框架中,控制器对象也是单例

单例模式的优点

  • 由于单例模式只生产一个实例,减少了系统性能开销,当一个对象的生产需要比较多的资源时,如读取配置,产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后永久驻留内存来解决

  • 单例模式可以在系统设置全局的访问点,优化共享资源访问,例如可以设计一个单例,负责所有数据表的映射处理

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

  • 主要

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

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

  • 其他

    • 双重检测锁式(由于JVM底层内部模型原因,偶尔会出现问题,不建议使用)

    • 静态内部类式(线程安全,调用效率高,可以延时加载)

    • 枚举单例(线程安全,调用效率高,不能延时加载)

  • 如何选用

    • 单例对象 占用资源少 不需要延时加载

      枚举式 好于 饿汉式

    • 单例对象 占用资源多 需要延时加载

      静态内部类 好于 懒汉式

饿汉式实现(单例对象立即加载)

/**
 * 饿汉式
 * @author Matrix42
 *
 */
public class SingletonDemo01 {

    //类初始化时立即加载
    private static SingletonDemo01 instance = new SingletonDemo01();

    private SingletonDemo01(){

    }

    //方法没有同步,调用效率高
    public static SingletonDemo01 getInstance(){
        return instance;
    }

}
  • 饿汉式单例模式代码中,static变量会在类装载时初始化,此时也不会涉及多个线程对象访问该对象的问题.虚拟机保证只会装载一次该类,肯定不会发生并发访问的问题.因此,可以省略synchronized关键字

  • 问题:如果只加载本类,而不调用getInstance(),甚至永远没有调用,则会造成资源浪费!

懒汉式实现(单例对象延迟加载)

public class LazayLoad {

    private static LazayLoad instance;

    private LazayLoad(){

    }

    public static synchronized LazayLoad getInstance(){
        if (instance==null) {
            instance = new LazayLoad();
        }
        return instance;
    }
}
  • 问题:资源利用率高了,但是每次调用getInstance()方法都要同步,并发效率低了

双重检测锁实现

  • 这个模式将同步内容放到if内部,提高了执行的效率,不比每次获取对象时都进行同步,只有第一次才同步,创建了以后就没必要了

  • 由于JVM底层内部模型原因,偶尔会出现问题,不建议使用

public class SingletonDemo3 {

  private static SingletonDemo3 instance = null;

  public static SingletonDemo3 getInstance() {
    if (instance == null) {
      SingletonDemo3 sc;
      synchronized (SingletonDemo3.class) {
        sc = instance;
        if (sc == null) {
          synchronized (SingletonDemo3.class) {
            if(sc == null) {
              sc = new SingletonDemo3();
            }
          }
          instance = sc;
        }
      }
    }
    return instance;
  }

  private SingletonDemo3() {

  }

}

静态内部类实现方式(也是一种懒加载方式)

  • 外部类没有static属性,不会像饿汉式那样立即加载对象

  • 只有真正调用getInstance(),才会加载静态内部类.加载时是线程安全的.instance是static final类型,保证了内存中只有一个实例存在,而且只能被赋值一次,从而保证了线程安全性

    • 兼备了并发高效调用和延迟加载的优势
public class SingletonDemo04 {

    private static class SingketonClassInstance{
        private static final SingletonDemo04 instance = new SingletonDemo04();
    }

    public static SingletonDemo04 getInstance(){
        return SingketonClassInstance.instance;
    }

    private SingletonDemo04(){

    }
}

使用枚举实现单例模式

  • 优点
    • 实现简单

    • 枚举本身就是单例模式.由JVM从根本上提供保障!避免通过反射和反序列化的漏洞!

  • 缺点

    • 无延迟加载
public enum SingletonDemo05 {

    //这个枚举元素本身就是单例对象
    INSTANCE;

    //添加自己需要的操作
    public void singtonOperation(){

    }
}

问题

  • 反射可以破解上面几种(不包含枚举)实现方式!(可以在构造方法中手动抛出异常来控制)

  • 反序列化也可以破解上面几种(不包含枚举)实现方式,可以通过定义readResolve()防止获得不同对象

    • 反序列化时,如果对象所在类定义了readResolve()(实际是一种回调)定义返回哪个对象
import java.io.Serializable;

/**
 * 测试懒汉式单例模式(如何防止反射和反序列化漏洞)
 * @author Matrix42
 *
 */
public class Demo06 implements Serializable{


    private static final Demo06 instance = new Demo06();


    public static Demo06 getInstance(){
        return instance;
    }

    //更严密,一般不用考虑这么多
    private Demo06(){
        if(instance!=null){
            throw new RuntimeException();
        }
    }

    //反序列化时,如果定义了readResolve()则直接返回此方法指定的对象.而不需要创建新对象
    private Object readResolve(){
        return instance;
    }

}
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.InvocationTargetException;

public class Client {

    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, SecurityException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, IOException {

        Demo06 s1 = Demo06.getInstance();
        Demo06 s2 = Demo06.getInstance();

        System.out.println(s1);
        System.out.println(s2);

        //通过反射方式调用私有构造器
//        Class<Demo06> clazz = (Class<Demo06>) Class.forName("design.sington.Demo06");
//
//        Constructor<Demo06> constructor = clazz.getDeclaredConstructor(null);
//
//        constructor.setAccessible(true);
//
//        Demo06 s3 = constructor.newInstance();
//
//        System.out.println(s3);

      //通过反序列化破解
        FileOutputStream fos = new FileOutputStream("d:/a.txt");
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(s1);
        oos.close();
        fos.close();

        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("d:/a.txt")));
        Demo06 s3 = (Demo06) ois.readObject();
        System.out.println(s3);

    }

}

常见的五种单例模式在多线程环境下的效率测试

  • 只关注相对值即可,在不同环境下不同的程序测得值会不一样

    《GOF23设计模式之 : 单例模式(二)》

    • CountDownLatch

    • 同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待

    • countDown()当前线程调用此方法,则计数减一(建议放在finally里执行)

    • await()调用此方法会一直阻塞当前线程,直到计数器的值为0

import java.util.concurrent.CountDownLatch;

/**
 * 测试五种创建单例模式的效率
 * @author Matrix42
 *
 */
public class Client02 {

    public static void main(String[] args) throws InterruptedException {

        long start = System.currentTimeMillis();

        int threadNum = 10;

        CountDownLatch countDownLatch = new CountDownLatch(threadNum);

        for(int i=0;i<10;i++){
         new Thread(new Runnable() {
            @Override
            public void run() {
               for(int i=0;i<1000000;i++){
                   Object object = SingletonDemo01.getInstance();
               }
               countDownLatch.countDown();
            }
        }).start();
        }
        countDownLatch.await();
        long end = System.currentTimeMillis();
        System.out.println("总耗时: "+(end-start));
    }

}
点赞

发表评论

电子邮件地址不会被公开。 必填项已用*标注

15 + 19 =