zoukankan      html  css  js  c++  java
  • 设计模式:单例模式

    一.

      单例模式指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点。

    二.饿汉模式

    public class HungrySingleton {
    
        private HungrySingleton(){}
    
        private final static HungrySingleton HUNGRY_SINGLETON ;
    
        static {
            HUNGRY_SINGLETON = new HungrySingleton();
        }
    
        public static HungrySingleton getInstance(){
            return HUNGRY_SINGLETON;
        }
    }
    

     饿汉式是指类的被加载的时候,就被初始化,并创建单例对象,不会存在访问安全问题。

    缺点:所有的饿汉单例对象都会在项目启动时初始化,会造成大量内存资源浪费。

    三.懒汉模式

    (1)考虑到饿汉模式的缺点后,加以修改,在类被调用的时候,才初始化。

    public class LazySimpleSingleton {
    
        private LazySimpleSingleton(){}
    
        private static LazySimpleSingleton LAZY_SIMPLE_SINGLETON = null;
    
        public static LazySimpleSingleton getIn,stance(){
            if(null==LAZY_SIMPLE_SINGLETON){
                LAZY_SIMPLE_SINGLETON = new LazySimpleSingleton();
                return LAZY_SIMPLE_SINGLETON;
            }
            return LAZY_SIMPLE_SINGLETON;
        }
    }
    

     但是会带来一个新的问题,就是在多线程环境下,有两个线程同一时间进入getInstance方法,同事满足null=LAZY_SIMPLE_SINGLETON时,会创建两个对象,然后后创建的会覆盖先创建的单例对象。

    考虑到这个问题后,进一步优化,使用synchronized关键字,给方法加锁。

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

     当一个线程调用getInstance方法时,另一个线程也调用该方法,会出现阻塞,直到第一个线程执行结束,才继续调用,完美解决了线程安全问题。

    出现新的问题:

    如果线程数量暴增,给getInstatnce加锁,只有一个线程运行该方法,其他线程全部阻塞等待,用户体验不好。新的解决方案--双重检查锁单例写法应运而生。

    (2)双重检查锁单例  (进门安检一次,闸口再检查一次)

    改造一下写法:

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

     这样的写法,其实和上一种写法差不多,都会造成大量线程阻塞。那如果把if条件往上升一级呢,先判断,再加锁。

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

     经过此次修改后,还是会出现线程安全问题。

    因为当两个线程同事满足null==instance条件后,会执行sychronized代码块的代码,该对象还是会被创建两次。

    再优化一下,我们在sychronized代码块中再进行对象的非空检查,这样该对象就不会被创建两次。

    public class LazyDoubleSingleton {
        private volatile static LazyDoubleSingleton instance;
        private LazyDoubleSingleton(){}
    
        public static LazyDoubleSingleton getInstance(){
            //检查是否要阻塞
            if(null==instance){
                synchronized (LazyDoubleSingleton.class){
                    //检查是否要创建对象
                    if(null==instance){
                        instance = new LazyDoubleSingleton();
                    }
    
                }
            }
            return instance;
        }
    }
    

    四.静态内部类单例的写法

      双重检查锁单例这种方式虽然解决了线程安全问题和性能问题,但是用到了sychronized总是要上锁,对性能还是有一些影响。我们可以采用静态内部类的方式进行优化。

    /**
     * @Author wen.jie
     * @Description 使用InnerClassSingleton类时,会默认先初始化内部类,如果没有使用,则内部类不初始化
     **/
    public class InnerClassSingleton {
    
        private InnerClassSingleton(){}
    
        public static InnerClassSingleton getInstance(){
            return InnerClass.INNER_CLASS_SINGLETON;
        }
    
        private static class InnerClass{
            private static final InnerClassSingleton INNER_CLASS_SINGLETON = new InnerClassSingleton();
        }
    }
    

     这种方式兼顾了饿汉单例写法的内存浪费问题和sychronized的性能问题,内部类一定要在方法调用之前就被初始化,巧妙的避开了线程安全问题。静态内部类单例写法真的完美了吗?

    如果我们用反射强行创建对象呢?

    public static void main(String[] args) {
            try {
                Constructor<InnerClassSingleton> constructor = InnerClassSingleton.class.getDeclaredConstructor(null);
                constructor.setAccessible(true);
    
                InnerClassSingleton o1 = constructor.newInstance();
                InnerClassSingleton o2 = constructor.newInstance();
    
                System.out.println(o1==o2);
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InstantiationException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }
        }
    

     

    发现创建了两个不同的实例。优化方法,在构造器中,进行判断。

    public class InnerClassSingleton {
    
        private InnerClassSingleton(){
            if(InnerClass.INNER_CLASS_SINGLETON!=null){
                throw new RuntimeException("不允许创建多个实例");
            }
        }
    
        public static InnerClassSingleton getInstance(){
            return InnerClass.INNER_CLASS_SINGLETON;
        }
    
        private static class InnerClass{
            private static final InnerClassSingleton INNER_CLASS_SINGLETON = new InnerClassSingleton();
        }
    }
    

     当我们再用反射去创建对象时,就会抛出异常。

     但这种写法值得斟酌,在构造器中抛出异常,不够优雅,有没有比静态内部类更优雅的单例写法呢?

     五.枚举式单例写法(jdk1.5以后)

    标准写法:

    public enum EnumSingleton {
        INSTANCE;
        private Object data;
    
        public Object getData(){
            return data;
        }
    
        public void setData(Object data){
            this.data = data;
        }
    
        public static EnumSingleton getInstance(){
            return INSTANCE;
        }
    }
    

     这种写法更加简洁,无偿提供序列化机制,绝对防止多次实例化,即使是在面对复杂序列化或者反射攻击的时候。虽然这种方法还没有广泛采用,但是单元素的枚举类型已经成为实现单例的最佳方法。(Effective Java原文)

    下面对其测试:

    对该单例对象进行序列化和反序列化测试

    public static void main(String[] args) {
    try {
    EnumSingleton instance1 = null;

    EnumSingleton instance2 = EnumSingleton.getInstance();
    instance2.setData(new Object());

    FileOutputStream fos = new FileOutputStream("EnumSingleton.dat");
    ObjectOutputStream oos = new ObjectOutputStream(fos);
    oos.writeObject(instance2);
    oos.flush();
    oos.close();

    FileInputStream fis = new FileInputStream("EnumSingleton.dat");
    ObjectInputStream ois = new ObjectInputStream(fis);
    instance1 = (EnumSingleton)ois.readObject();
    System.out.println(instance1.getData());
    System.out.println(instance2.getData());
    System.out.println(instance1.getData()==instance2.getData());
    } catch (IOException | ClassNotFoundException e) {
    e.printStackTrace();
    }
    }

     测试结果:

    六.反序列化破坏单例模式

    反序列化会从磁盘文件中读取对象,转为内存对象,反序列化的对象会重新分配内存,如果反序列化的对象是单例对象,则违背了单例模式的初衷。

    下面对之前写过的双重检查锁单例对象进行序列化和反序列化(先实现Serializable接口):

        public static void main(String[] args) {
            try {
                LazyDoubleSingleton instance1 = null;
                LazyDoubleSingleton instance2 = LazyDoubleSingleton.getInstance();
                FileOutputStream fos = new FileOutputStream("LazyDoubleSingleton.dat");
                ObjectOutputStream oos = new ObjectOutputStream(fos);
                oos.writeObject(instance2);
                oos.flush();
                oos.close();
    
                FileInputStream fis = new FileInputStream("LazyDoubleSingleton.dat");
                ObjectInputStream ois = new ObjectInputStream(fis);
                instance1 = (LazyDoubleSingleton)ois.readObject();
                System.out.println(instance1==instance2);
            } catch (IOException | ClassNotFoundException e) {
                e.printStackTrace();
            }
        }
    

     结果:

     那我们如何保证序列化的情况下也能够实现单例模式呢?

    其实只需要在单例对象中增加一个readResolve()方法(注意返回值必须是Object类型,不能是单例对象类型):

    public class LazyDoubleSingleton implements Serializable {
        private volatile static LazyDoubleSingleton instance;
        private LazyDoubleSingleton(){}
    
        public static LazyDoubleSingleton getInstance(){
            //检查是否要阻塞
            if(null==instance){
                synchronized (LazyDoubleSingleton.class){
                    //检查是否要创建对象
                    if(null==instance){
                        instance = new LazyDoubleSingleton();
                    }
    
                }
            }
            return instance;
        }
    
        private Object readResolve(){
            return instance;
        }
    }
    

     再看运行结果,就是true了

     具体原理是在readObject方法中判断了反序列化的对象中是否有无参的readResolve()方法(jdk源码中)。

  • 相关阅读:
    OpenGL纹理映射总结
    研究生常用网站:
    Oracle 11g,10g数据库软件下载地址
    <转>乔布斯羡慕嫉妒恨的人:Android之父安迪·鲁宾
    VC6里面的中文名字或者注释复制乱码解决
    基于CentOs的Hadoop集群全分布式部署<转>
    centos架设FTP服务器
    centos 卸载 jdk
    ESX的 企业版许可证
    vsftpd的 553 Could not create file
  • 原文地址:https://www.cnblogs.com/wwjj4811/p/13581741.html
Copyright © 2011-2022 走看看