前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >漫画:什么是单例设计模式?

漫画:什么是单例设计模式?

作者头像
小灰
发布2022-07-05 15:01:56
4540
发布2022-07-05 15:01:56
举报
文章被收录于专栏:程序员小灰

————— 第二天 —————

单例模式第一版:

代码语言:javascript
复制
public class Singleton {
    private Singleton() {}  //私有构造函数
    private static Singleton instance = null;  //单例对象
    //静态工厂方法
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

为什么这样写呢?我们来解释几个关键点:

1.要想让一个类只能构建一个对象,自然不能让它随便去做new操作,因此Signleton的构造方法是私有的。

2.instance是Singleton类的静态成员,也是我们的单例对象。它的初始值可以写成Null,也可以写成new Singleton()。至于其中的区别后来会做解释。

3.getInstance是获取单例对象的方法。

如果单例初始值是null,还未构建,则构建单例对象并返回。这个写法属于单例模式当中的懒汉模式。

如果单例对象一开始就被new Singleton()主动构建,则不再需要判空操作,这种写法属于饿汉模式

这两个名字很形象:饿汉主动找食物吃,懒汉躺在地上等着人喂。

为什么说刚才的代码不是线程安全呢?

假设Singleton类刚刚被初始化,instance对象还是空,这时候两个线程同时访问getInstance方法:

因为Instance是空,所以两个线程同时通过了条件判断,开始执行new操作:

这样一来,显然instance被构建了两次。让我们对代码做一下修改:

单例模式第二版:

代码语言:javascript
复制
public class Singleton {
    private Singleton() {}  //私有构造函数
   private static Singleton instance = null;  //单例对象
   //静态工厂方法
   public static Singleton getInstance() {
        if (instance == null) {      //双重检测机制
         synchronized (Singleton.class){  //同步锁
           if (instance == null) {     //双重检测机制
             instance = new Singleton();
               }
            }
         }
        return instance;
    }
}

为什么这样写呢?我们来解释几个关键点:

1.为了防止new Singleton被执行多次,因此在new操作之前加上Synchronized 同步锁,锁住整个类(注意,这里不能使用对象锁)。

2.进入Synchronized 临界区以后,还要再做一次判空。因为当两个线程同时访问的时候,线程A构建完对象,线程B也已经通过了最初的判空验证,不做第二次判空的话,线程B还是会再次构建instance对象。

像这样两次判空的机制叫做双重检测机制

————————————

假设这样的场景,当两个线程一先一后访问getInstance方法的时候,当A线程正在构建对象,B线程刚刚进入方法:

这种情况表面看似没什么问题,要么Instance还没被线程A构建,线程B执行 if(instance == null)的时候得到false;要么Instance已经被线程A构建完成,线程B执行 if(instance == null)的时候得到true。

真的如此吗?答案是否定的。这里涉及到了JVM编译器的指令重排

指令重排是什么意思呢?比如java中简单的一句 instance = new Singleton,会被编译器编译成如下JVM指令:

memory =allocate(); //1:分配对象的内存空间

ctorInstance(memory); //2:初始化对象

instance =memory; //3:设置instance指向刚分配的内存地址

但是这些指令顺序并非一成不变,有可能会经过JVM和CPU的优化,指令重排成下面的顺序:

memory =allocate(); //1:分配对象的内存空间

instance =memory; //3:设置instance指向刚分配的内存地址

ctorInstance(memory); //2:初始化对象

当线程A执行完1,3,时,instance对象还未完成初始化,但已经不再指向null。此时如果线程B抢占到CPU资源,执行 if(instance == null)的结果会是false,从而返回一个没有初始化完成的instance对象。如下图所示:

如何避免这一情况呢?我们需要在instance对象前面增加一个修饰符volatile。

单例模式第三版:

代码语言:javascript
复制
public class Singleton {
    private Singleton() {}  //私有构造函数
   private volatile static Singleton instance = null;  //单例对象
   //静态工厂方法
   public static Singleton getInstance() {
       if (instance == null) {      //双重检测机制
       synchronized (this){  //同步锁
         if (instance == null) {     //双重检测机制
           instance = new Singleton();
             }
          }
       }
       return instance;
    }
}

The volatile keyword indicates that a value may change between different accesses, it prevents an optimizing compiler from optimizing away subsequent reads or writes and thus incorrectly reusing a stale value or omitting writes.

经过volatile的修饰,当线程A执行instance = new Singleton的时候,JVM执行顺序是什么样?始终保证是下面的顺序:

memory =allocate(); //1:分配对象的内存空间

ctorInstance(memory); //2:初始化对象

instance =memory; //3:设置instance指向刚分配的内存地址

如此在线程B看来,instance对象的引用要么指向null,要么指向一个初始化完毕的Instance,而不会出现某个中间态,保证了安全。

几点说明:

1. volatile关键字不但可以防止指令重排,也可以保证线程访问的变量值是主内存中的最新值。有关volatile的详细原理,我在以后的漫画中会专门讲解。

2.本漫画纯属娱乐,还请大家尽量珍惜当下的工作,切勿模仿小灰的行为哦。

—————END—————

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2017-11-27,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 程序员小灰 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档