跟我学设计模式之单例模式

1、设计模式

1.1 设计模式是什么?

  1. 设计模式是解决特定问题的一系列套路,是前辈们的代码设计经验的总结,具备必定的广泛性,能够反复使用。其目的是为了提升代码的可重用性、代码的可读性和代码的可靠性。
  2. 设计模式的本质是面向对象设计原则的实际运用,是对类的封装性、继承性和多态性以及类的关联关系和组合关系的充分理解。

1.2 为何要使用设计模式?

项目的需求是永远在变的,为了应对这种变化,使得咱们的代码可以轻易的实现解耦和拓展。java

1.3 设计模式类型

  • 建立型模式

建立型模式的主要关注点是怎样建立对象,它的主要特色是将对象的建立与使用分离。这样能够下降系统的耦合度,使用者不须要关注对象的建立细节。git

  • 结构型模式

结构型模式描述如何将类或对象按某种布局组成更大的结构。它分为类结构型模式和对象结构型模式,前者采用继承机制来组织接口和类,后者釆用组合或聚合来组合对象。github

  • 行为型模式

行为型模式用于描述程序在运行时复杂的流程控制,即描述多个类或对象之间怎样相互协做共同完成单个对象都没法单独完成的任务,它涉及算法与对象间职责的分配。它分为类行为模式和对象行为模式,前者采用继承机制来在类间分派行为,后者采用组合或聚合在对象间分配行为。算法

建立型模式 结构型模式 行为型模式
单例模式、抽象工厂模式、建造者模式、工厂模式、原型模式 适配器模式、桥接模式、装饰模式、组合模式、外观模式、享元模式、代理模式 模版方法模式、命令模式、迭代器模式、观察者模式、中介者模式、备忘录模式、解释器模式、状态模式、策略模式、职责链模式(责任链模式)、访问者模式

2、面向对象设计的六大设计原则

2.1 开闭原则

一个软件实体如类、模块和函数应该对扩展开放,对修改关闭数据库

  • 解读
  1. 用抽象构建框架,用实现扩展细节;
  2. 不以改动原有类的方式来实现新需求,而是应该以实现事先抽象出来的接口(或具体类继承抽象类)的方式来实现。
  • 优势
  1. 能够在不改动原有代码的前提下给程序扩展功能,增长了程序的可扩展性;
  2. 同时也下降了程序的维护成本。

2.2 单一职责原则

一个类只容许有一个职责,即只有一个致使该类变动的缘由。编程

  • 解读
  1. 类职责的变化每每就是致使类变化的缘由:也就是说若是一个类具备多种职责,就会有多种致使这个类变化的缘由,从而致使这个类的维护变得困难;设计模式

  2. 每每在软件开发中随着需求的不断增长,可能会给原来的类添加一些原本不属于它的一些职责,从而违反了单一职责原则。若是咱们发现当前类的职责不只仅有一个,就应该将原本不属于该类真正的职责分离出去缓存

  3. 不只仅是类,函数(方法)也要遵循单一职责原则,即:一个函数(方法)只作一件事情。若是发现一个函数(方法)里面有不一样的任务,则须要将不一样的任务以另外一个函数(方法)的形式分离出去。安全

  • 优势
  1. 提升代码的可读性,更实际性地更下降了程序出错的风险;
  2. 有利于bug的追踪,下降了程序的维护成本。

2.3 依赖倒置原则

  1. 依赖抽象,而不是依赖实现;
  2. 抽象不该该依赖细节;细节应该依赖抽象;
  3. 高层模块不能依赖低层模块,两者都应该依赖抽象。
  • 解读
  1. 面向接口编程,而不是面向实现编程;
  2. 尽可能不要从具体的类派生,而是以继承抽象类或实现接口来实现;
  3. 关于高层模块与低层模块的划分能够按照决策能力的高低进行划分。业务层天然就处于上层模块,逻辑层和数据层天然就归类为底层。
  • 优势
  1. 经过抽象来搭建框架,创建类和类的关联,以减小类间的耦合性;
  2. 以抽象搭建的系统要比以具体实现搭建的系统更加稳定,扩展性更高,同时也便于维护。
  • 里氏替换原则

子类能够扩展父类的功能,但不能改变父类原有的功能。也就是说,子类继承父类时,除添加新的方法完成新增功能外,尽可能不要重写父类的方法。bash

2.4 接口隔离原则

多个特定的客户端接口要好于一个通用性的总接口。

  • 解读
  1. 客户端不该该依赖它不须要实现的接口;
  2. 不创建庞大臃肿的接口,应尽可能细化接口,接口中的方法应该尽可能少。

注意:接口的粒度也不能过小。若是太小,则会形成接口数量过多,使设计复杂化。

  • 优势

避免同一个接口里面包含不一样类职责的方法,接口责任划分更加明确,符合高内聚低耦合的思想。

2.5 迪米特法则(最少知道原则)

一个对象应该对尽量少的对象有接触,也就是只接触那些真正须要接触的对象。

  • 解读

一个类应该只和它的成员变量,方法的输入,返回参数中的类做交流,而不该该引入其余的类(间接交流)。

  • 优势

能够良好地下降类与类之间的耦合,减小类与类之间的关联程度,让类与类之间的协做更加直接。

2.6 组合聚合复用原则

全部引用基类的地方必须能透明地使用其子类的对象,也就是说子类对象能够替换其父类对象,而程序执行效果不变。

-解读

在继承体系中,子类中能够增长本身特有的方法,也能够实现父类的抽象方法,可是不能重写父类的非抽象方法,不然该继承关系就不是一个正确的继承关系。

  • 优势

能够检验继承使用的正确性,约束继承在使用上的泛滥。

3、 单例模式概念

3.1 单例模式是什么?

单例模式就是在程序运行中只实例化一次,建立一个全局惟一对象。有点像 Java 的静态变量,可是单例模式要优于静态变量:

  1. 静态变量在程序启动的时候JVM就会进行加载,若是不使用,会形成大量的资源浪费;
  2. 单例模式可以实现懒加载,可以在使用实例的时候才去建立实例。

开发工具类库中的不少工具类都应用了单例模式,比例线程池、缓存、日志对象等,它们都只须要建立一个对象,若是建立多份实例,可能会带来不可预知的问题,好比资源的浪费、结果处理不一致等问题。

3.2 为何要使用单例模式?

单例模式属于设计模式三大分类中的第一类——建立型模式,跟对象的建立相关。也就是说,这个模式在建立对象的同时,还致力于控制建立对象的数量,是的,只能建立一个实例,多的不要。

👉那么问题来了,咱们为何要控制对象建立的个数?

  1. 有些场景下,不使用单例模式,会致使系统同一时刻出现多个状态缺少同步,用户天然没法判断当前处于什么状态;
  2. 经过控制建立对象的数量,能够节约系统资源开销(像线程、数据库链接等);
  3. 全局数据共享。

3.3 单例的实现思路

  1. 静态化实例对象;
  2. 私有化构造方法,禁止经过构造方法建立实例;
  3. 提供一个公共的静态方法,用来返回惟一实例。

4、 饿汉模式

在定义静态属性时,直接实例化了对象

4.1 示例

public class HungryMode {

    /** * 利用静态变量来存储惟一实例 */
    private static final HungryMode instance = new HungryMode();

    /** * 私有化构造函数 */
    private HungryMode(){
        // 里面能够有不少操做
    }

    /** * 提供公开获取实例接口 * @return */
    public static HungryMode getInstance(){
        return instance;
    }
}
复制代码

4.2 饿汉模式的优势

因为使用了static关键字,保证了在引用这个变量时,关于这个变量的因此写入操做都完成,因此保证了JVM层面的线程安全。

4.3 饿汉模式的缺点

不能实现懒加载,形成空间浪费:若是一个类比较大,咱们在初始化的时就加载了这个类,可是咱们长时间没有使用这个类,这就致使了内存空间的浪费。

因此,能不能只有用到 getInstance()方法,才会去初始化单例类,才会加载单例类中的数据。因此就有了:懒汉式

5、懒汉模式

懒汉模式是一种偷懒的模式,在程序初始化时不会建立实例,只有在使用实例的时候才会建立实例,因此懒汉模式解决了饿汉模式带来的空间浪费问题。

5.1 懒汉模式的通常实现

public class LazyMode {
    /** * 定义静态变量时,未初始化实例 */
    private static LazyMode instance;

    /** * 私有化构造函数 */
    private LazyMode(){
        // 里面能够有不少操做
    }
    /** * 提供公开获取实例接口 * @return */
    public static LazyMode getInstance(){
        // 使用时,先判断实例是否为空,若是实例为空,则实例化对象
        if (instance == null) {
            instance = new LazyMode();
        }
        return instance;
    }
}
复制代码

可是这种实如今多线程的状况下是不安全的,有可能会出现多份实例的状况:

if (instance == null) {
    instance = new LazyMode();
}
复制代码

假设有两个线程同时进入到上面这段代码,由于没有任何资源保护措施,因此两个线程能够同时判断的 instance 都为空,都将去初始化实例,因此就会出现多份实例的状况。

5.2 懒汉模式的优化

咱们给getInstance()方法加上synchronized关键字,使得getInstance()方法成为受保护的资源就可以解决多份实例的问题。

public class LazyModeSynchronized {
    /** * 定义静态变量时,未初始化实例 */
    private static LazyModeSynchronized instance;
    /** * 私有化构造函数 */
    private LazyModeSynchronized(){
        // 里面能够有不少操做
    }
    /** * 提供公开获取实例接口 * @return */
    public synchronized static LazyModeSynchronized getInstance(){
        /** * 添加class类锁,影响了性能,加锁以后将代码进行了串行化, * 咱们的代码块绝大部分是读操做,在读操做的状况下,代码线程是安全的 * */
        if (instance == null) {
            instance = new LazyModeSynchronized();
        }
        return instance;
    }
}
复制代码

5.3 懒汉模式的优势

实现了懒加载,节约了内存空间。

5.4 懒汉模式的缺点

  1. 在不加锁的状况下,线程不安全,可能出现多份实例;
  2. 在加锁的状况下,会使程序串行化,使系统有严重的性能问题。

懒汉模式中加锁的问题,对于getInstance()方法来讲,绝大部分的操做都是读操做,读操做是线程安全的,因此咱们没必让每一个线程必须持有锁才能调用该方法,咱们须要调整加锁的问题。由此也产生了一种新的实现模式:双重检查锁模式

6、双重检查锁模式

6.1 双重检查锁模式的通常实现

public class DoubleCheckLockMode {

    private static DoubleCheckLockMode instance;

    /** * 私有化构造函数 */
    private DoubleCheckLockMode(){

    }
    /** * 提供公开获取实例接口 * @return */
    public static DoubleCheckLockMode getInstance(){
        // 第一次判断,若是这里为空,不进入抢锁阶段,直接返回实例
        if (instance == null) {
            synchronized (DoubleCheckLockMode.class) {
                // 抢到锁以后再次判断是否为空
                if (instance == null) {
                    instance = new DoubleCheckLockMode();
                }
            }
        }
        return instance;
    }
}
复制代码

双重检查锁模式解决了单例、性能、线程安全问题,可是这种写法一样存在问题:在多线程的状况下,可能会出现空指针问题,出现问题的缘由是JVM在实例化对象的时候会进行优化和指令重排序操做。

6.2 什么是指令重排?

private SingletonObject(){
	  // 第一步
     int x = 10;
	  // 第二步
     int y = 30;
     // 第三步
     Object o = new Object(); 
}
复制代码

上面的构造函数SingletonObject()JVM 会对它进行指令重排序,因此执行顺序可能会乱掉,可是无论是那种执行顺序,JVM 最后都会保证因此实例都完成实例化。 若是构造函数中操做比较多时,为了提高效率,JVM 会在构造函数里面的属性未所有完成实例化时,就返回对象。双重检测锁出现空指针问题的缘由就是出如今这里,当某个线程获取锁进行实例化时,其余线程就直接获取实例使用,因为JVM指令重排序的缘由,其余线程获取的对象也许不是一个完整的对象,因此在使用实例的时候就会出现空指针异常问题

6.3 双重检查锁模式优化

要解决双重检查锁模式带来空指针异常的问题,只须要使用volatile关键字,volatile关键字严格遵循happens-before原则,即:在读操做前,写操做必须所有完成。

public class DoubleCheckLockModelVolatile {
    /** * 添加volatile关键字,保证在读操做前,写操做必须所有完成 */
    private static volatile DoubleCheckLockModelVolatile instance;
    /** * 私有化构造函数 */
    private DoubleCheckLockModelVolatile(){

    }
    /** * 提供公开获取实例接口 * @return */
    public static DoubleCheckLockModelVolatile getInstance(){

        if (instance == null) {
            synchronized (DoubleCheckLockModelVolatile.class) {
                if (instance == null) {
                    instance = new DoubleCheckLockModelVolatile();
                }
            }
        }
        return instance;
    }
}
复制代码

7、静态内部类模式

静态内部类模式也称单例持有者模式,实例由内部类建立,因为 JVM 在加载外部类的过程当中, 是不会加载静态内部类的, 只有内部类的属性/方法被调用时才会被加载, 并初始化其静态属性。静态属性由static修饰,保证只被实例化一次,而且严格保证明例化顺序。

public class StaticInnerClassMode {

    private StaticInnerClassMode(){

    }

    /** * 单例持有者 */
    private static class InstanceHolder{
        private  final static StaticInnerClassMode instance = new StaticInnerClassMode();

    }

    /** * 提供公开获取实例接口 * @return */
    public static StaticInnerClassMode getInstance(){
        // 调用内部类属性
        return InstanceHolder.instance;
    }
}
复制代码

这种方式跟饿汉式方式采用的机制相似,但又有不一样。二者都是采用了类装载的机制来保证初始化实例时只有一个线程。不一样的地方:

  1. 饿汉式方式是只要Singleton类被装载就会实例化,没有Lazy-Loading的做用;
  2. 静态内部类方式在Singleton类被装载时并不会当即实例化,而是在须要实例化时,调用getInstance()方法,才会装载SingletonInstance类,从而完成Singleton的实例化。

类的静态属性只会在第一次加载类的时候初始化,因此在这里,JVM帮助咱们保证了线程的安全性,在类进行初始化时,别的线程是没法进入的。

因此这种方式在没有加任何锁的状况下,保证了多线程下的安全,而且没有任何性能影响和空间的浪费

8、枚举类实现单例模式

由于枚举类型是线程安全的,而且只会装载一次,设计者充分的利用了枚举的这个特性来实现单例模式,枚举的写法很是简单,并且枚举类型是所用单例实现中惟一一种不会被破坏的单例实现模式

8.1 示例

public class EnumerationMode {
    
    private EnumerationMode(){
        
    }

    /** * 枚举类型是线程安全的,而且只会装载一次 */
    private enum Singleton{
        INSTANCE;

        private final EnumerationMode instance;

        Singleton(){
            instance = new EnumerationMode();
        }

        private EnumerationMode getInstance(){
            return instance;
        }
    }

    public static EnumerationMode getInstance(){

        return Singleton.INSTANCE.getInstance();
    }
}
复制代码

8.2 适用场合:

  1. 须要频繁的进行建立和销毁的对象;
  2. 建立对象时耗时过多或耗费资源过多,但又常常用到的对象;
  3. 工具类对象;
  4. 频繁访问数据库或文件的对象。

9、单例模式的问题及解决办法

除枚举方式外, 其余方法都会经过反射的方式破坏单例

9.1 单例模式的破坏

/** * 以静态内部类实现为例 * @throws Exception */
@Test
public void singletonTest() throws Exception {
    Constructor constructor = StaticInnerClassMode.class.getDeclaredConstructor();
    constructor.setAccessible(true);

    StaticInnerClassMode obj1 = StaticInnerClassMode.getInstance();
    StaticInnerClassMode obj2 = StaticInnerClassMode.getInstance();
    StaticInnerClassMode obj3 = (StaticInnerClassMode) constructor.newInstance();

    System.out.println("输出结果为:"+obj1.hashCode()+"," +obj2.hashCode()+","+obj3.hashCode());
}
复制代码

控制台打印:

输出结果为:1454171136,1454171136,1195396074
复制代码

从输出的结果咱们就能够看出obj1obj2为同一对象,obj3为新对象。obj3是咱们经过反射机制,进而调用了私有的构造函数,而后产生了一个新的对象。

9.2 如何阻止单例破坏

能够在构造方法中进行判断,若已有实例, 则阻止生成新的实例,解决办法以下:

public class StaticInnerClassModeProtection {

    private static boolean flag = false;

    private StaticInnerClassModeProtection(){
        synchronized(StaticInnerClassModeProtection.class){
            if(flag == false){
                flag = true;
            }else {
                throw new RuntimeException("实例已经存在,请经过 getInstance()方法获取!");
            }
        }
    }

    /** * 单例持有者 */
    private static class InstanceHolder{
        private  final static StaticInnerClassModeProtection instance = new StaticInnerClassModeProtection();
    }

    /** * 提供公开获取实例接口 * @return */
    public static StaticInnerClassModeProtection getInstance(){
        // 调用内部类属性
        return InstanceHolder.instance;
    }
}
复制代码

测试:

/** * 在构造方法中进行判断,若存在则抛出RuntimeException * @throws Exception */
@Test
public void destroyTest() throws Exception {
    Constructor constructor = StaticInnerClassModeProtection.class.getDeclaredConstructor();
    constructor.setAccessible(true);

    StaticInnerClassModeProtection obj1 = StaticInnerClassModeProtection.getInstance();
    StaticInnerClassModeProtection obj2 = StaticInnerClassModeProtection.getInstance();
    StaticInnerClassModeProtection obj3 = (StaticInnerClassModeProtection) constructor.newInstance();

    System.out.println("输出结果为:"+obj1.hashCode()+"," +obj2.hashCode()+","+obj3.hashCode());
}
复制代码

控制台打印:

Caused by: java.lang.RuntimeException: 实例已经存在,请经过 getInstance()方法获取!
	at cn.van.singleton.demo.mode.StaticInnerClassModeProtection.<init>(StaticInnerClassModeProtection.java:22)
	... 35 more
复制代码

10、总结

10.1 各类实现的对比

名称 饿汉模式 懒汉模式 双重检查锁模式 静态内部类实现 枚举类实现
可用性 可用 不推荐使用 推荐使用 推荐使用 推荐使用
特色 不能实现懒加载,可能形成空间浪费 不加锁线程不安全;加锁性能差 线程安全;延迟加载;效率较高 避免了线程不安全,延迟加载,效率高。 写法简单;线程安全;只装载一次

10.2 示例代码地址

Github 示例代码

10.3 技术交流

  1. 风尘博客:https://www.dustyblog.cn
  2. 风尘博客-掘金
  3. 风尘博客-博客园
  4. Github
  5. 公众号
    风尘博客

10.4 参考文章

面向对象设计的六大设计原则