zoukankan      html  css  js  c++  java
  • 设计模式之单例模式及原型模式

    单例模式:

      单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

     应用场景:保证一个类仅有一个实例,并提供一个访问它的全局访问点。Spring 中的单例模式完成了后半句话,即提供了全局的访问点 BeanFactory。但没有从构造器级别去控制单例,这是因为 Spring 管理的是是任意的 Java 对象。 Spring 下默认的 Bean 均为单例。

      对于系统中的某些类来说,只有一个实例很重要,例如,一个系统中可以存在多个打印任务,但是只能有一个正在工作的任务;一个系统只能有一个窗口管理器或文件系统;一个系统只能有一个计时工具或ID(序号)生成器。如在Windows中就只能打开一个任务管理器。如果不使用机制对窗口对象进行唯一化,将弹出多个窗口,如果这些窗口显示的内容完全一致,则是重复对象,浪费内存资源;如果这些窗口显示的内容不一致,则意味着在某一瞬间系统有多个状态,与实际不符,也会给用户带来误解,不知道哪一个才是真实的状态。因此有时确保系统中某个对象的唯一性即一个类只能有一个实例非常重要。
      如何保证一个类只有一个实例并且这个实例易于被访问呢?定义一个全局变量可以确保对象随时都可以被访问,但不能防止我们实例化多个对象。一个更好的解决办法是让类自身负责保存它的唯一实例。这个类可以保证没有其他实例被创建,并且它可以提供一个访问该实例的方法。这就是单例模式的模式动机。

      单例对象(Singleton)是一种常用的设计模式。在Java应用中,单例对象能保证在一个JVM中,该对象只有一个实例存在。这样的模式有几个好处:

    1、某些类创建比较频繁,对于一些大型的对象,这是一笔很大的系统开销。

    2、省去了new操作符,降低了系统内存的使用频率,减轻GC压力。

    3、有些类如交易所的核心交易引擎,控制着交易流程,如果该类可以创建多个的话,系统完全乱了。(比如一个军队出现了多个司令员同时指挥,肯定会乱成一团),所以只有使用单例模式,才能保证核心交易服务器独立控制整个流程。

    单例模式的几种实现方式:

      主要的实现方式有:懒汉式,饿汉式,双检锁方式,注册登记式,静态内部类,枚举式。其中线程安全的懒汉式,饿汉式,双检锁方式,静态内部类方式请移步:https://www.cnblogs.com/wuzhenzhao/p/9923309.html。接下去来看一下注册登记式以及枚举式:

    注册登记式的实现:

    //Spring中的做法,就是用这种注册式单例
    public class BeanFactory {
    
        private BeanFactory(){}
    
        //线程安全的容器,饿汉式保证容器对象本身为单例
        private static Map<String,Object> ioc = new ConcurrentHashMap<String,Object>();
    
        public static Object getBean(String className){
    
            if(!ioc.containsKey(className)){
                Object obj = null;
                try {//用反射的方式创建对象(因为已经构造函数私有化),并登记到容器中
                    obj = Class.forName(className).newInstance();
                    ioc.put(className,obj);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                return obj;
            }else{//从容器中获取管理的单例对象并返回
                return ioc.get(className);
            }
        }
    } 

    枚举式:

      这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。

    //常量中去使用,常量不就是用来大家都能够共用吗?
    //通常在通用API中使用
    public enum Color {
        RED(){
           private int r = 255;
           private int g = 0;
           private int b = 0;
    
        },BLACK(){
            private int r = 0;
            private int g = 0;
            private int b = 0;
        },WHITE(){
            private int r = 255;
            private int g = 255;
            private int b = 255;
        };
    }

    反射机制导致单例破坏:

      如下的方式可以破坏单例,这个时候我们可以看到s1与s2是不一致的两个对象,为了避免这种情况,要防止构造函数被成功调用两次。需要在构造函数中对实例化次数进行统计,设置一个全局变量来标识构造函数调用次数。大于一次就抛出异常。

    public class Singleton {
        private static Singleton instance = new Singleton();  
     
        private Singleton() {}
     
        public static Singleton getInstance() {
            return instance;
        }
    }
    
    public class Test {
        public static void main(String[] args) throws Exception{
            Singleton s1 = Singleton.getInstance();
     
            Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
            constructor.setAccessible(true);
            Singleton s2 = constructor.newInstance();
     
            System.out.println(s1.hashCode());
            System.out.println(s2.hashCode());
     
        }
    }

    反序列化时导致单例破坏:

       原因是由于反序列化会通过反射调用无参数的构造方法创建一个新的对象。我们来看个案例:

      先构建一个实现了序列化接口的单例类:

    //反序列化时导致单例破坏
    public class Seriable implements Serializable {
        //序列化就是说把内存中的状态通过转换成字节码的形式
        //从而转换一个IO流,写入到其他地方(可以是磁盘、网络IO)
        //内存中状态给永久保存下来了
    
        //反序列化
        //讲已经持久化的字节码内容,转换为IO流
        //通过IO流的读取,进而将读取的内容转换为Java对象
        //在转换过程中会重新创建对象new
    
    
        public  final static Seriable INSTANCE = new Seriable();
        private Seriable(){}
    
        public static  Seriable getInstance(){
            return INSTANCE;
        }
    
    //    private  Object readResolve(){
    //        return  INSTANCE;
    //    }
    
    }
    

      测试:

    public class SeriableTest {
        public static void main(String[] args) {
    
            Seriable s1 = null;
            Seriable s2 = Seriable.getInstance();
    
            FileOutputStream fos = null;
            try {
                fos = new FileOutputStream("Seriable.obj");
                ObjectOutputStream oos = new ObjectOutputStream(fos);
                oos.writeObject(s2);
                oos.flush();
                oos.close();
    
    
                FileInputStream fis = new FileInputStream("Seriable.obj");
                ObjectInputStream ois = new ObjectInputStream(fis);
                s1 = (Seriable)ois.readObject();
                ois.close();
    
                System.out.println(s1);
                System.out.println(s2);
                System.out.println(s1 == s2);
    
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    

      这里输出的接口会发现两个示例并不相等,这不就破坏了单例了吗?这是为什么呢?简单来看一下通过 ObjectInputStream 去 readObject 的时候都经过了哪些处理,他的调用链如下:

    readObject() --> readObject0(boolean unshared) --> checkResolve(readOrdinaryObject(unshared)),在readOrdinaryObject(unshared)方法中可以发现以下部分代码:

    if (obj != null &&
                handles.lookupException(passHandle) == null &&
                desc.hasReadResolveMethod())
            {
                Object rep = desc.invokeReadResolve(obj);
                if (unshared && rep.getClass().isArray()) {
                    rep = cloneArray(rep);
                }
                if (rep != obj) {
                    // Filter the replacement object
                    if (rep != null) {
                        if (rep.getClass().isArray()) {
                            filterCheck(rep.getClass(), Array.getLength(rep));
                        } else {
                            filterCheck(rep.getClass(), -1);
                        }
                    }
                    handles.setObject(passHandle, obj = rep);
                }
            }
    

      其中有两个最重要的调用:

    hasReadResolveMethod:如果实现了serializable 或者 externalizable接口的类中包含readResolve则返回true。

    invokeReadResolve:通过反射的方式调用要被反序列化的类的readResolve方法。

    所以,原理也就清楚了,主要在Singleton中定义readResolve方法,并在该方法中指定要返回的对象的生成策略,就可以防止单例被破坏。

    原型模式:

      原型模式(Prototype Pattern)是用于创建重复的对象,同时又能保证性能。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。这种模式是实现了一个原型接口,该接口用于创建当前对象的克隆。当直接创建对象的代价比较大时,则采用这种模式。

      原型模式是一种创建型设计模式,Prototype模式允许一个对象再创建另外一个可定制的对象,根本无需知道任何如何创建的细节,工作原理是:通过将一个原型对象传给那个要发动创建的对象,这个要发动创建的对象通过请求原型对象拷贝它们自己来实施创建。java给我们提供的 Cloneable接口 进行实现。

      通过 clone 的方式创建克隆对象的过程中涉及到拷贝的深度问题,这里分为浅拷贝和深拷贝:

    浅拷贝:拷贝的级别浅。能复制变量,如果对象内还有对象,则只能复制对象的地址。

    深拷贝:是将对象及值复制过来,两个对象修改其中任意的值另一个值不会改变,这就是深拷贝,能复制变量,也能复制当前对象的 内部对象。

      先来看一下浅拷贝的方式的原型模式,创建一个实体类实现 Cloneable 接口 :

    public class Prototype implements Cloneable {
    
        public String name;
    
        public ArrayList<String> list = new ArrayList();
    
        @Override
        protected Object clone() throws CloneNotSupportedException {
            return super.clone();
        }
    }
    

      测试:运行一下代码会发现输出结果为 true 跟2,当我们修改了p实例中的list属性,clone对象中的 list也会跟着变,他们指向同样的地址。

    public class CloneTest {
        public static void main(String[] args) throws CloneNotSupportedException {
            Prototype p = new Prototype();
            p.name = "helloworld";
            p.list.add("1");
            Prototype clone = (Prototype)p.clone();
            p.list.add("2");
            System.out.println(p.list==clone.list);
            System.out.println(clone.list.size());
        }
    }
    

      但是在很多实际业务场景中我们不希望出现上诉的这种情况,那么我们需要怎么做去实现深拷贝呢?可以通过序列化的方式,通过流的读取来去做,因为在单例模式中我们就遇到了可以通过反序列化的方式去破坏单例模式。将实体类修改如下再运行测试类则完成深拷贝:

    public class Prototype implements Cloneable , Serializable {
    
        public String name;
    
        public ArrayList<String> list = new ArrayList();
    
        @Override
        protected Object clone() {
    //        return super.clone();
            return deepClone();
        }
    
        public Object deepClone(){
            try{
    
                ByteArrayOutputStream bos = new ByteArrayOutputStream();
                ObjectOutputStream oos = new ObjectOutputStream(bos);
                oos.writeObject(this);
    
                ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
                ObjectInputStream ois = new ObjectInputStream(bis);
    
                Prototype copy = (Prototype)ois.readObject();
    
                return copy;
    
            }catch (Exception e){
                e.printStackTrace();
                return null;
            }
        }
    } 
    优点: 1、性能提高。 2、逃避构造函数的约束。

    缺点: 1、配备克隆方法需要对类的功能进行通盘考虑,这对于全新的类不是很难,但对于已有的类不一定很容易,特别当一个类引用不支持串行化的间接对象,或者引用含有循环结构的时候。 2、必须实现 Cloneable 接口。

  • 相关阅读:
    ThinkPHP的ajaxReturn方法的使用
    PHP中如何获取网站根目录物理路径
    MySQL索引覆盖
    php对gzip的使用(实例)
    php对gzip的使用(开启)
    php对gzip的使用(理论)
    ThinkPHP中调用PHPExcel
    PHPExcel正确读取excel表格时间单元格(转载)
    Kubernetes pod网络解析
    vRO 添加已有磁盘到VM
  • 原文地址:https://www.cnblogs.com/wuzhenzhao/p/10288795.html
Copyright © 2011-2022 走看看