今天看啥  ›  专栏  ›  defaultCoder

设计模式之-单例模式

defaultCoder  · 简书  ·  · 2019-05-12 23:08

文章预览

众所周知, 单例模式是最常见的设计模式之一.

定义

顾名思义, 单例模式的意思就是控制一个类只有单个实例的模式.

实现
饿汉式

知道了单例模式的定义后, 我们可以对其实现方式进行一系列思考.(接下来我们就设计一个单例类Singleton)

  1. 既然Singleton类只能允许存在一个实例, 那么我们必须限制对构造方法的使用, 让该类无法进行new对象, 所以我们创建这个类时应该讲他的构造方法私有化.
  2. 但是这样就没法进行new对象了, 所以要在Singleton类的内部将这个实例对象实例化出来.
  3. 既然只能在内部new对象, 外界想要得到这个实例, 那就得对外界提供一个可以获取该实例的方法.

所以就以上的两点思考中可以得到以下代码:

public class Singleton {
    private static Singleton instance = new Singleton();
    private Singleton (){}
    public static Singleton getInstance() {
        return instance;
    }
}

非常简单的我们就这么实现了一个单例模式, 这就是单例模式中的饿汉式了.

饿汉式一上来就在类中将这个实例给创建了, 等需要的时候再进行getInstance获取该实例, 这样没有考虑到是否真的需要该对象, 显然做到对内存的极致节约.

懒汉式

那么我们能不能在需要的时候再创建这个对象呢? 于是我们接着思考:

  1. 我们希望不要在类加载的时候就把实例创建, 希望在需要getInstance获取实例的时候再创建, 那么一定要在getInstance时再进行创建实例的操作了.
  2. 可是如果直接在getInstance中写return 一个new Singleton显然不合理, 这样每次getInstance肯定都创建了一次新的对象, 我们希望只创建一次, 所以在getInstance时我们判断一次对象是否为null, 如果为null再进行new对象的操作.

思考到这里, 不难能写出如下代码:

public class Singleton {
    private static Singleton instance;
    private Singleton (){}
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

这样似乎就完成了调用时再创建对象的方式了, 是不是相当简单? 但是这样的单例模式真的没有问题吗?

其实这样是存在线程安全问题的:
我们考虑一下多线程的场景, 如果两条线程同时调用了
getInstance方法, 线程A拿到了执行权后进行判断, 变量instance为null, 于是进入了if语句块中, 这时线程A的执行权被线程B给抢去了, 线程B拿到了执行权后进行判断, 由于线程A还没有执行new对象的语句, 变量instance依旧为null, 于是线程B也进入了if语句块中, 这时候线程A与B都进入到了if语句块中, 将会new出两次对象, 这时候便不是单例了.

所以上述的实现方式是线程不安全的懒汉模式.

所以我们不难想到, 加个锁不就完事了吗?
所以线程安全的懒汉式便是如下实现方式:

public class Singleton {  
    private static Singleton instance;  
    private Singleton (){}  
    public static synchronized Singleton getInstance() {  
    if (instance == null) {  
        instance = new Singleton();  
    }  
    return instance;  
    }  
}
双检锁机制

但是大家觉得这样的方式在性能方面是不是就太差了呢? 将锁加在getInstance方法中, 那么每次调用getInstance方法都会阻塞, 不管这个实例是否已经创建好了, 每个线程并发走到这, 都会做无用的等待. 我们并不希望这样. 那可以对其进行优化吗?

于是我们又要进行一系列的思考:

  1. 如果对象已经被创建了, 那我们就直接返回. 并不需要额外的加锁操作阻塞线程.
  2. 我们只希望创建对象的时候对创建对象的过程加锁.
  3. 通过以上两点, 我们不难想到, 我们可以先进行对instance是否为空进行判断, 如果已经被创建了就返回, 如果为空就进入锁中进行new实例.

通过思考, 我们得到以下代码:

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

或许大家: ???咋搞的这么多的判断? 还有那个volatile 是什么鬼哦?

这个代码跨度或许有点难以接受, 或许大家不明白为什么搞这么多的判断, 没关系, 我们再理一下思路, 为什么要判断两次null值?

  1. 这判断是有目的的,第一层判断如果instance实例不为空,那皆大欢喜,说明对象已经被创建过了,直接返回该对象即可,不会走到 synchronized 部分,所以班长对象被创建了之后,不会影响到性能.
  2. 第二层判断是在 synchronized 代码块里面,为什么要再做一次判断呢?假如 instance对象是 null,那么第一层判断后,肯定有很多线程已经进来第一层了,那么即使在第二层某个线程执行完了之后,释放了锁,其他线程还会进入 synchronized 代码块,如果不判断,那么又会被创建一次,这就导致了多个instance对象的创建。所以第二层起到了一个防范作用。

volatile 是什么鬼?
我们先看下instance= new Singleton();在这个操作中,JVM主要干了三件事:

  1. 在堆空间里分配一部分空间
  2. 执行instance的构造方法进行初始化
  3. 把instance对象指向在堆空间里分配好的空间

把第3步执行完,这个instance对象就已经不为空了.
但是, 当我们编译的时候, 编译器在生成汇编代码的时候会对流程顺序进行优化. 优化的结果不是我们可以控制的, 有可能是按照1、2、3的顺序执行, 也有可能按照1、3、2的顺序执行.
如果是按照1、3、2的顺序执行, 恰巧在执行到3的时候()还没执行2), 突然跑来了一个线程, 进来 getInstance 方法之后判断 instance不为空就返回了 instance实例。此时 instance 实例虽不为空, 但它还没执行构造方法进行初始化(即没有执行2), 所以该线程如果对那些需要初始化的参数进行操作那就悲剧了. 但是加了 volatile 关键字的话, 就不会出现这个问题. 这是由 volatitle 本身的特性决定的.

这里就简单说一下这个关键词在此处的作用吧, 其实这并不是单例模式中的知识点, 而是java基础知识. 大家感兴趣可以查阅资料深入了解, 接下来我也会补充相关文章

这就是双检锁机制的单例模式了, 这样的单例模式安全而已可以在多线程的情况下保持高性能.

优点以及适用场景

优点

  • 在内存中只有一个对象, 可以节省空间
  • 避免频繁的创建和销毁对象, 可以提高性能
  • 避免对共享资源的多重占用
  • 可以进行全局访问

适用场景

  • 需要频繁实例化然后销毁的对象。
  • 创建对象时耗时过多或者耗资源过多,但又经常用到的对象。
  • 有状态的工具类对象。
  • 频繁访问数据库或文件的对象。
  • 以及其他我没用过的所有要求只有一个对象的场景。

今天就对单例模式就讨论到这儿了, 如果文中有错误请大家能够包容并私信我进行修正. 感谢阅读.

………………………………

原文地址:访问原文地址
快照地址: 访问文章快照
总结与预览地址:访问总结与预览