设计模式——单例模式(创建型模式)

引言

设计模式是我们开发中为了代码的可维护性和可复用性必须掌握的一项技能,设计模式分为23种,在讲解工厂方法模式之前,我们先来了解一下设计模式的原则以及分类。

设计模式原则
设计模式原则 英文缩写 意义
单一职责原则 SRP(Single responsibility principle) 就一个类而言应该只有一个引起它变化的原因
开放关闭原则 OCP(Open Closed Principle) 一个软件实体应该对扩展开放,对修改关闭
里氏替换原则 LSP liskov substitution principle 所有引用基类(父类)的地方必须能透明地使用其子类的对象
依赖倒置原则 DIP Dependency Inversion Principle 抽象不应该依赖于细节,细节应该依赖于抽象
接口隔离原则 ISP Interface Segregation Principle 使用多个专门的接口,而不使用单一的接口
迪米特法则 LOD Low Of Demeter 一个软件实体应当尽可能少地与其它实体发生相互作用

单一职责原则:是为了让我们的接口职责单一,代码高内聚低耦合。
开放关闭原则:表格中的软件实体可以指一个软件模块,或者一个由多个类组成的局部结构或者一个独立类。
里氏替换原则:是实现开闭原则的重要方式之一,可以扩展父类功能,但是不能改变父类功能,尽量不要重写父类方法。
依赖倒置原则:在变量类型声明,参数声明,方法返回值类型声明使用接口或者抽象类。
接口隔离原则:即接口大小要适中,太小会接口泛滥,太大将违背接口隔离原则。
迪米特法则:可以降低系统的耦合度,使类与类之间保持松散的耦合关系,当一个模块需要修改时候,需要尽量少地影响其它模块,这样扩展起来会比较容易。

设计模式分类
类别 设计模式
创建型模式(五种) 工厂方法模式 抽象工厂模式 单例模式 建造者模式 原型模式
结构型模式(七种) 适配器模式 装饰者模式 代理模式 外观模式 桥接模式 组合模式 享元模式
行为型模式(十一种) 策略模式 模板方法模式 观察者模式 迭代器模式 责任链模式 命令模式 备忘录模式 状态模式 访问者模式 中介者模式 解释器模式
单例模式概述:

单例模式在我们的开发中是用到频率最高的一个,而且在著名的开源项目中也频频被使用到,比如ImageLoader, EventBus 等,所以掌握它是必须的,这个模式比较简单,但是也有一些容易出现的问题,下面就一起来详细的看一下。
单例模式其实就是为了在全局中仅仅存在一个类的实例对象存在,这样省去了很多的问题和节约了开销,想要实现单例模式有两个注意事项:

  1. 构造方法私有化 外部不能通过构造方法创建实例
  2. 提供唯一的获取实例的入口是getInstance。

下面我们来看不同的单例模式

  • 饿汉模式
public class Person {
    private static final Person INSTANCE = new Person();

    private Person() {}

    public static Person getInstance() {
        return INSTANCE;
    }
}

饿汉模式比较简单,但是有一个问题就是不管我们使用还是没使用这个类的实例,都会创建一个实例INSTANCE,这样显然是不太好的,比如如果这个对象比较占内存呢,那么怎么办呢?

  • 懒汉模式
public class Person2 {

    private static Person2 INSTANCE = null;

    private Person2() {}

    public static Person2 getInstance() {
        if (null == INSTANCE) {
            INSTANCE = new Person2();
        }
        return INSTANCE;
    }
}

这样虽然我们不先创建实例了,但是如果在多线程中显然我们的对象会被创建很多次,那么单例也就无效了,怎么办呢?对了,加锁

  • 安全的饿汉模式(同步方法)
public class Person3 {

    private static Person3 INSTANCE = null;

    private Person3() {}

    public synchronized static Person3 getInstance() {
        if (null == INSTANCE) {
            INSTANCE = new Person3();
        }
        return INSTANCE;
    }
}

加锁固然解决了我们的问题,但是这样效率就低了很多,并且锁还是在方法上加的,因为同步方法比同步代码块效率低的多,那么怎么办呢?

  • 安全的饿汉模式(同步代码块)
public class Person4 {

    private static Person4 INSTANCE = null;

    private Person4() {}

    public static Person4 getInstance() {
        synchronized (Person4.class) {
            if (null == INSTANCE) {
                INSTANCE = new Person4();
            }
        }
        return INSTANCE;
    }
}

这样对效率其实没多大的提升,我们尝试使用Double-Check Locking 双重锁定单例模式。

  • Double-Check Locking 双重锁定单例模式
public class Person5 {

    private volatile static Person5 INSTANCE = null;

    private Person5() {}

    public static Person5 getInstance() {
        if (INSTANCE == null) {    // 1 
            synchronized (Person5.class) {
                if (INSTANCE == null) { 
                    INSTANCE = new Person5();  //2
                }
            }
        }
        return INSTANCE;
    }
}

这样的话就貌似可以了,为什么时候貌似呢,是因为它其实还不是完整的单例,下面说一下原因。
在执行INSTANCE = new Person5();这一行的时候,因为不是原子性操作,所以是分步进行的,又如下三个步骤:

1 分配对象的内存空间
2 初始化对象
3 将INSTANCE指向刚分配的内存地址

我们都知道重排序,就是创建对象和赋值操作,JVM不保证执行顺序,那如果先执行3再执行2,就会出问题了,比如有两个线程A和B:

  1. A和B同时进入了上面1处的if判断;
  2. A首先获取了锁,分配内存有了地址值,但是还没有来得及在堆中实例化
  3. B线程判断了不为空,这个时候就会返回对象到方法的调用处,这个时候因为对象还没有实例化,就会出错。

怎么解决这个问题呢?

  • 基于volatile的Double-Check Locking 双重锁定单例模式
public class Person6 {

    private volatile static Person6 INSTANCE = null;

    private Person6() {}

    public static Person6 getInstance() {
        if (INSTANCE == null) {
            synchronized (Person6.class) {
                if (INSTANCE == null) {
                    INSTANCE = new Person6();
                }
            }
        }
        return INSTANCE;
    }
}

为什么volatile是可以达到效果呢,因为volatile有禁止指令重排序的功能,volatile关键字的用法内容比较多,此处不过多描述,读者可自行搜索。

  • 基于类的初始化的解决方案
public class Person7 {
    private Person7() {}

    private static class SingletonHolder {
        private static final Person7 INSTANCE = new Person7();
    }

    /**
     * 静态内部类的方式获取单例
     */
    public static Person7 getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

JVM在类的初始化阶段会执行类的初始化,JVM会去获取一个锁,这个锁可以同步多个线程对同一个类的初始化,这种写法比双重锁定单例模式简单。

好了,单例模式的讲解就到这里了。

推荐阅读设计模式——抽象工厂模式(创建型模式)

你可能感兴趣的:(Java设计模式)