zoukankan      html  css  js  c++  java
  • 设计模式(一)单例模式

    设计模式六大原则

    1、开闭原则(Open Close Principle)

    开闭原则的意思是:对扩展开放,对修改关闭在程序需要进行拓展的时候,不能去修改原有的代码,实现一个热插拔的效果。简言之,是为了使程序的扩展性好,易于维护和升级。想要达到这样的效果,我们需要使用接口和抽象类,后面的具体设计中我们会提到这点。

    2、里氏代换原则(Liskov Substitution Principle)

    里氏代换原则是面向对象设计的基本原则之一。 里氏代换原则中说,任何基类可以出现的地方,子类一定可以出现。LSP 是继承复用的基石,只有当派生类可以替换掉基类,且软件单位的功能不受到影响时,基类才能真正被复用,而派生类也能够在基类的基础上增加新的行为。里氏代换原则是对开闭原则的补充。实现开闭原则的关键步骤就是抽象化,而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。

    3、依赖倒转原则(Dependence Inversion Principle)

    这个原则是开闭原则的基础,具体内容:针对接口编程,依赖于抽象而不依赖于具体。

    4、接口隔离原则(Interface Segregation Principle)

    这个原则的意思是:使用多个隔离的接口,比使用单个接口要好。它还有另外一个意思是:降低类之间的耦合度。由此可见,其实设计模式就是从大型软件架构出发、便于升级和维护的软件设计思想,它强调降低依赖,降低耦合。

    5、迪米特法则,又称最少知道原则(Demeter Principle)

    最少知道原则是指:一个实体应当尽量少地与其他实体之间发生相互作用,使得系统功能模块相对独立。

    6、合成复用原则(Composite Reuse Principle)

    合成复用原则是指:尽量使用合成/聚合的方式,而不是使用继承。

    一、创建型

    1.单例模式(Singleton)

    1.1 介绍

    意图: 保证一个类仅有一个实例,并提供一个访问它的全局访问点。

    主要解决: 一个全局使用的类频繁地创建与销毁。

    何时使用: 当想控制实例数目,节省系统资源的时候。

    如何解决: 判断系统是否已经有这个单例,如果有则返回,如果没有则创建。

    关键代码: 构造函数是私有的。

    应用实例:

    1、Windows 是多进程多线程的,在操作一个文件的时候,就不可避免地出现多个进程或线程同时操作一个文件的现象,所以所有文件的处理必须通过唯一的实例来进行。

    2、一些设备管理器常常设计为单例模式,比如一个电脑有两台打印机,在输出的时候就要处理不能两台打印机打印同一个文件。

    优点:

    1、在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存)。

    2、避免对资源的多重占用(比如写文件操作)。

    缺点: 没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。

    使用场景:

    1、要求生产唯一序列号。

    2、WEB 中的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来。

    3、创建的一个对象需要消耗的资源过多,比如 I/O 与数据库的连接等,数据库连接池

    注意事项:getSingletonInstance() 方法中需要使用同步锁 synchronized (Singleton.class) 防止多线程同时进入造成 instance 被多次实例化。

    1.2 实现方式

    使用一个私有构造函数、一个私有静态变量以及一个公有静态函数来实现。

    私有构造函数保证了不能通过构造函数来创建对象实例,只能通过公有静态函数返回唯一的私有静态变量。

    懒汉式-线程不安全

    以下实现中,私有静态变量 singletonInstance 被延迟实例化(把对象的创建延迟到使用的时候创建,而不是对象实例化的时候创建 ),这样做的好处是,如果没有用到该类,那么就不会实例化 singletonInstance,从而节约资源。

    这个实现在多线程环境下是不安全的,如果多个线程能够同时进入 if (singletonInstance == null) ,并且此时 singletonInstance 为 null,那么会有多个线程执行 singletonInstance = new Singleton(); 语句,这将导致实例化多次 singletonInstance。

    package com.singleton.demo01;
    
    /**
     * @Auther: Jibny Zhan
     * @Date: 2019/9/12 15:59
     * @Description:懒汉式-线程不安全
     */
    public class Singleton {
    
        private static int count;
        private static Singleton singletonInstance;
    
        private Singleton() {
            System.out.println("Singleton 私有的构造方法被实例化 " + (++count) + " 次。");
        }
    
        public static Singleton getSingletonInstance() {
            if (singletonInstance == null) {
                singletonInstance = new Singleton();
            }
            return singletonInstance;
        }
    }
    
    

    饿汉式-线程安全

    线程不安全问题主要是由于 singletonInstance 被实例化多次,采取直接实例化 singletonInstance 的方式就不会产生线程不安全问题。但是直接实例化的方式也丢失了延迟实例化带来的节约资源的好处。

    package com.singleton.demo02;
    
    /**
     * @Auther: Jibny Zhan
     * @Date: 2019/9/12 16:44
     * @Description:饿汉式-线程安全
     */
    public class Singleton {
        private static int count;
        private static Singleton singletonIntance = new Singleton();
    
        private Singleton() {
            System.out.println("Singleton 私有的构造方法被实例化 " + (++count) + " 次。");
        }
    
        public static Singleton getSingletonIntance() {
            return singletonIntance;
        }
    }
    
    

    懒汉式-线程安全

    只需要对 getSingletonInstance() 方法加锁,那么在一个时间点只能有一个线程能够进入该方法,从而避免了实例化多次 singletonInstance。但是当一个线程进入该方法之后,其它试图进入该方法的线程都必须等待,即使 singletonInstance 已经被实例化了。这会让线程阻塞时间过长,因此该方法有性能问题,不推荐使用。

    package com.singleton.demo03;
    
    /**
     * @Auther: Jibny Zhan
     * @Date: 2019/9/12 16:53
     * @Description: 懒汉式-线程安全-同步锁阻塞
     */
    public class Singleton {
        private static int count;
        private static Singleton singletonInstance;
    
        private Singleton() {
            System.out.println("Singleton 私有的构造方法被实例化 " + (++count) + " 次。");
        }
    
        public static synchronized Singleton getSingletonInstance() {
            if (singletonInstance == null) {
                singletonInstance = new Singleton();
            }
            return singletonInstance;
        }
    }
    
    

    双重校验锁-线程安全

    singletonInstance 只需要被实例化一次,之后就可以直接使用了。加锁操作只需要对实例化那部分的代码进行,只有当 singletonInstance 没有被实例化时,才需要进行加锁。双重校验锁先判断 singletonInstance 是否已经被实例化,如果没有被实例化,那么才对实例化语句进行加锁。

    package com.singleton.demo04;
    
    /**
     * @Auther: Jibny Zhan
     * @Date: 2019/9/12 17:03
     * @Description: 双重校验锁-线程安全
     */
    public class Singleton {
        private static int count;
        private static volatile Singleton singletonInstance;
    
        private Singleton(){
            System.out.println("Singleton 私有的构造方法被实例化 " + (++count) + " 次。");
        }
    
        public static Singleton getSingletonInstance(){
            if(singletonInstance == null){
                synchronized (Singleton.class){
                    if(singletonInstance == null){
                        singletonInstance = new Singleton();
                    }
                }
            }
            return singletonInstance;
        }
    }
    
    

    如果上述代码中只使用外部一个if语句话,那么就有可能两个线程都进入到if语句内部,这样的话singletonInstance就会被实例化两次了,因此必须使用双重校验锁。

    singletonInstance 采用 volatile 关键字修饰也是很有必要的, singletonInstance = new Singleton(); 这段代码其实是分为三步执行:

    1. 为 singletonInstance 分配内存空间
    2. 初始化 singletonInstance
    3. 将 singletonInstance 指向分配的内存地址

    但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1>3>2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getSingletonInstance() 后发现 singletonInstance 不为空,因此返回 singletonInstance,但此时 singletonInstance 还未被初始化。

    使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。

    静态内部类实现

    当 Singleton 类加载时,静态内部类 SingletonHolder 没有被加载进内存。只有当调用 getSingletonInstance() 方法从而触发 SingletonHolder.singletonInstance 时 SingletonHolder 才会被加载,此时初始化singletonInstance 实例,并且 JVM 能确保 singletonInstance 只被实例化一次。

    这种方式不仅具有延迟初始化的好处,而且由 JVM 提供了对线程安全的支持。

    package com.singleton.demo05;
    
    /**
     * @Auther: Jibny Zhan
     * @Date: 2019/9/12 17:10
     * @Description:静态内部类实现(推荐)
     */
    public class Singleton {
        private static int count;
    
        private Singleton() {
            System.out.println("Singleton 私有的构造方法被实例化 " + (++count) + " 次。");
        }
    
        private static class SingletonHolder {
            private static final Singleton singletonInstance = new Singleton();
        }
    
        public static Singleton getSingletonInstance() {
            return SingletonHolder.singletonInstance;
        }
    }
    
    

    测试单例实现

    package com.singleton.test;
    
    import com.singleton.demo05.Singleton;
    
    /**
     * @Auther: Jibny Zhan
     * @Date: 2019/9/12 16:57
     * @Description:
     */
    public class Main {
        public static void main(String[] args) {
    //        Singleton singleton1 = Singleton.getSingletonInstance();
    //        Singleton singleton2 = Singleton.getSingletonInstance();
    //        System.out.println(singleton1 == singleton1);
    //        System.out.println(singleton1);
    //        System.out.println(singleton2);
    //        true
    
    //        线程测试,不安全
    //        线程 1 运行到 (1)处的时候,线程 2 抢到的 CPU 的执行权,进入 getInstance() 方法,
    //        运行了 instance = new Singleton();,但线程 2 创建了对象这件事情,线程 1 根本不知道,
    //        等到线程 1 重新获得 CPU 执行权的时候,从 (1) 处继续执行,又运行了 instance = new Singleton();
    //        这行代码,这样,多余的对象就被创建出来了
    
            Runnable task = () -> {
                String threadName = Thread.currentThread().getName();
                Singleton s1 = Singleton.getSingletonInstance();
                System.out.println("线程 " + threadName + "	 => " + s1.hashCode());
            };
            for (int i = 0; i < 100; i++) {
                new Thread(task, "" + i).start();
            }
    //        Singleton 私有的构造方法被实例化 1 次。
    //        Singleton 私有的构造方法被实例化 3 次。
    //        Singleton 私有的构造方法被实例化 1 次。
    //        Singleton 私有的构造方法被实例化 2 次。
    //        线程 2	 => 1133046463
    //        线程 3	 => 503674368
    //        线程 0	 => 2115147268
    //        线程 1	 => 151286434
    //        线程 4	 => 151286434 ......
        }
    }
    
    

    }

    1.3 JDK

    1.4 破坏单例模式

    1、反射

    通过反射获得单例类的构造函数,由于该构造函数是private的,通过setAccessible(true)指示反射的对象在使用时应该取消 Java 语言访问检查,使得私有的构造函数能够被访问,这样使得单例模式失效。

    如果要抵御这种攻击,要防止构造函数被成功调用两次。需要在构造函数中对实例化次数进行统计,大于一次就抛出异常。

    2、序列化

    一是可以实现数据的持久化;二是可以对象数据的远程传输。 如果过该类implements Serializable,那么就会在反序列化的过程中再创一个对象。这个问题的解决办法就是在反序列化时,指定反序化的对象实例。添加如下方法:

     private static final long serialVersionUID = -3706817479790597301L;
     
        private volatile static Singleton singleton;
     
        private Object readResolve() {
            return singleton;
        }
    
    

    3、克隆

    由克隆我们可以想到原型模式,原型模式就是通过clone方法实现对象的创建的,clone方式是Object方法,每个对象都有,那我使用一个单例模式类的对象,调用clone方法,再创建一个新的对象了,那岂不是上面说的单例模式失效了。

    当然答案是否定,某一个对象直接调用clone方法,会抛出异常,即并不能成功克隆一个对象。调用该方法时,必须实现一个Cloneable 接口。这也就是原型模式的实现方式。还有即如果该类实现了Cloneable接口,尽管构造函数是私有的,他也可以创建一个对象。

    即clone方法是不会调用构造函数的,他是直接从内存中copy内存区域的。

    所以单例模式的类是不可以实现cloneable接口的。

  • 相关阅读:
    免费的编程中文书籍索引 from github
    win7 Python 环境 准备 配置
    SQL Server 2008 允许远程链接,适用于广域网和局域网
    CTP API开发期货自动交易平台概论
    一步一步重写 CodeIgniter 框架 (4) —— load_class 管理多个对象实例的思路
    一步一步重写 CodeIgniter 框架 (3) —— 用面向对象重构代码
    一步一步重写 CodeIgniter 框架 (2) —— 实现简单的路由功能
    一步一步重写 CodeIgniter 框架 (1) —— url 如何映射到具体的方法
    一步一步重写 CodeIgniter 框架 -- 原因和思路
    GDI双缓冲绘图
  • 原文地址:https://www.cnblogs.com/binjz/p/12501308.html
Copyright © 2011-2022 走看看