单例模式

单例模式

作用:保证一个类只有一个实例,并且提供一个访问该实例的全局访问点(方法/属性)-->比如静态的getInstance()方法。


常见的应用场景:
  Windows的Task Manager(任务管理器)
  windows的Recycle Bin(回收站),回收站一直维护着仅有的一个实例。
  读取配置文件的类,一般也只有一个对象。
  网站的计数器。
  应用程序的日志应用,日志文件一直处于打开状态,因为只能有一个实例去操作 ,否则内容不好追加。
  数据库连接池的设计一般也是采用单例模式,因为数据库连接是一种数据库资源。 
  操作系统的文件系统,也是大的单例模式实现,一个操作系统只能有一个文件系统。 
  Application 也是单例的典型应用(Servlet编程中会涉及到)
  在servlet编程中,每个Servlet也是单例 。
  在spring MVC框架/struts1框架中,控制器对象也是单例。

单例设计模式的优点:
  1、只生成一个实例,减少了系统性能花销。
  当一个对象的创建需要比较多的资源时,如读取配置、产生其他依赖对象,则可以通过在应用启动时直接产生一个单例对象,然后永久驻留内存的方式来解决。
  2、单例模式可以在系统设置全局的访问点,优化共享资源访问。例如可以设计一个单例类,负责所有数据表的映射处理。

一、五种单例模式实现方式
主要:
  1、饿汉式(线程安全,调用效率高。 但是,不能延时加载。)
  2、懒汉式(线程安全,调用效率不高。 但是,可以延时加载。)
  3、双重检测锁式(由于JVM底层内部模型原因,偶尔会出问题。不建议使用。)
  4、静态内部类式(线程安全,调用效率高。 但是,可以延时加载。)
  5、枚举单例(线程安全,调用效率高,不能延时加载。并且可以天然的防止反射和反序列化漏洞!)

1.1、饿汉式实现(单例对象立即加载-->声明该类的时候)

public class SingletonDemo02 {
    private static /*final*/ SingletonDemo02 s = new SingletonDemo02();    //唯一实例
    private SingletonDemo02(){};    //私有化构造器
    public static /*synchronized*/ SingletonDemo02 getInstance(){ //synchronized 可以省略。因为JVM只会加载一次,没有并发。
        return s;
    }
}
public class Client {
    public static void main(String[] args) {
        SingletonDemo02 s1 = SingletonDemo02.getInstance();
        SingletonDemo02 s2 = SingletonDemo02.getInstance();
        System.out.println(s1==s2);   //true    
    }
}

  饿汉式单例模式,static(static 方法的加载时机-->在类加载的初始化阶段)变量会在类装载时初始化。-->而懒加载是在调用方法时才初始化。
  此时也不会涉及多线程对象访问该对象的问题。虚拟机保证只会装载一次该类,并不会发生并发访问问题。因此可以省略synchronized关键字。
  存在的问题:只是加载该类,并没有调用getInstance()。就没有应用到此单例对象。造成了资源的浪费。


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

public class SingletonDemo01 {
    private static SingletonDemo01 s;
    private SingletonDemo01(){}
    public static synchronized SingletonDemo01 getInstance(){
        if(s==null){
            s = new SingletonDemo01();
        }
        return s;
    }
}

  lazyload:懒加载,延迟加载,真正用到的时候才加载。
  存在的问题:资源利用率提高。但是每次调用gettInstance()方法都要同步,并发效率低。


1.3、双重检测锁实现

public class SingletonDemo03 {
    private static SingletonDemo03 instance = null;
    public static SingletonDemo03 getInstance(){
        if(instance==null){
            SingletonDemo03 s;
            synchronized (SingletonDemo03.class){
                s = instance;
                if(s ==null){
                    synchronized (SingletonDemo03.class){
                        if(s==null){
                            s = new SingletonDemo03();
                        }
                    }
                    instance = s;
                }
            }
        }
        return instance;
    }
}

 

1.4、静态内部类实现方式(懒加载的一种)

public class SingletonDemo04 {
    private SingletonDemo04(){
    }
    private static class SingletonInstance{
        private static final SingletonDemo04 instance = new SingletonDemo04();
    }
    public static SingletonDemo04 getInstance(){
        return SingletonInstance.instance;
    }
}

  外部类没有static属性,则不会像饿汉式那样立即加载对象。
  只有真正调用getInstance()才会加载静态内部类。加载类时线程是安全的。instance是static final(不能在修改) 类型的,保证了内存中只有这样一个实例存在。而且只能被赋值一次,从而保证了线程安全。
  兼备了并发高效调用和延迟加载的优势。

存在的问题:
  1、反射可以破解上述几种(不包含枚举式)实现方式。(可以在构造函数中手动抛出异常控制。)
  2、反序列化可以破解上面几种(不含枚举式)实现方式。
    可以通过定义readResolve()方式获得不同的对象。
    反序列化时,如果对象所在类定义了readResolve()(实际是一种回调),定义返回哪个对象。
      在ObjectInputStream 类中 会检查序列化的类中是否有 readResolve 方法。
单例类:

public class SingletonDemo01 implements Serializable{
    private static SingletonDemo01 s;

    private SingletonDemo01(){ //私有化构造器
        if(s!=null){
            throw new RuntimeException("只能创建一个对象");
            //通过手动抛出异常,避免通过反射创建多个单例对象! 
        }
    }
    
    public static synchronized SingletonDemo01 getInstance() throws Exception {
        if(null==s){
            s= new SingletonDemo01();
        }
        return s;
    }
    
    //反序列化时,如果对象所在类定义了readResolve(),(实际是一种回调),定义返回哪个对象
    private Object readResolve() throws ObjectStreamException {
        return s;
    }

}
View Code

破解与解决:

public class SingleDemo01Hack {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException {
//        Class cls = Class.forName("com.itheima.demo04.singletonhack.SingletonDemo01");
        Class cls = SingletonDemo01.class;
        
/*        //1、通过暴力反射,并且将私有化的构造方法设置为public的,使用构造方法对象的newInstance(),就能破解单例模式
        Constructor constructor = cls.getDeclaredConstructor();
        constructor.setAccessible(true);
        Object o1 = constructor.newInstance();

        Object o2 = constructor.newInstance();
        System.out.println(o1==o2); //false*/

        //2、通过暴力反射,并且将私有化的构造方法设置为public的,使用getInstance(),并在私有的构造方法中进行单例对象是否为空的判断
        Method getInstance = cls.getMethod("getInstance");
        Constructor constructor = cls.getDeclaredConstructor();
        constructor.setAccessible(true);
        Object o1 = constructor.newInstance();
        Object obj1 = getInstance.invoke(o1);
        Object obj2 = getInstance.invoke(o1);
//        Object obj3 = constructor.newInstance();    //此时要通过构造方法创建新的对象就会报错
        System.out.println(obj1==obj2);            //true

        //使用反序列化破解,和解决方法
        writeSingleton(o1);
        readSingleton(o1);
    }
    
    private static void writeSingleton(Object o1){
        ObjectOutputStream oos = null;
        try {
            oos = new ObjectOutputStream(new FileOutputStream("day10IO/singleton.txt"));
            oos.writeObject(o1);
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            try {
                oos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    
    private static void readSingleton(Object o1){
        ObjectInputStream ois = null;
        try {
            ois = new ObjectInputStream(new FileInputStream("day10IO/singleton.txt"));
            Object obj1 = ois.readObject();      //即使是构造方法私有化了,也能读取对象

            ois = new ObjectInputStream(new FileInputStream("day10IO/singleton.txt"));
            Object obj2 = ois.readObject();
            System.out.println(obj1==obj2);        //true

        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } finally {
            try {
                ois.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
View Code



1.5、使用枚举实现单例模式

public enum  SingletonDemo05 {
    /*定义一个枚举元素,代表了SingletonDemo05的一个实例*/
    INSTANCE;
    /*单例可以有自己的操作*/
    public void singletonOperations(){
        //功能处理
    }
}
public class Demo05Test {
    public static void main(String[] args) {
        SingletonDemo05 sd1 = SingletonDemo05.INSTANCE;
        SingletonDemo05 sd2 = SingletonDemo05.INSTANCE;
        System.out.println(sd1==sd2);
    }
}
View Code

优点:
  实现简单。
  枚举本身就是单例模式。由JVM从根本上提供保障。避免通过反射和反序列化的漏洞。
缺点:
  无延时加载。

常见的五种单例模式在多线程环境下的效率测试
  饿汉式  22ms
  懒汉式  636ms
  静态内部类  28ms
  枚举类  32ms
  双重检测锁式  65ms

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

posted @ 2018-11-23 10:09  payn  阅读(184)  评论(0)    收藏  举报