• 核心作用

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

  • 常见应用场景

    • 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);

    }

}

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

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

    QQ截图20161026155116.png

  • 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));
    }

}