软件设计之单例模式(二)

本文从实际场景出发,深入探讨单例模式。通过分析代码,解释了如何在多线程环境下保证单例对象的唯一性,讨论了在不使用静态初始化时,如何通过加锁机制确保线程安全,确保无论何时调用,都返回同一对象。
摘要由CSDN通过智能技术生成

 

昨天介绍了最简单的单例模式,需要回看的可以从这里直接进入:

软件设计之单例模式(一)

今天我们继续来深入探讨单例模式,还是以宇宙、地球和人的关系为例,

昨天我们为了创建地球这样一个全局的、唯一的、共享的对象,我们的代码是这样设计的:

public class Earth {

    private static Earth earth = new Earth();

    private Earth(){
        System.out.println("构造函数");
    }
    
    public static Earth getInstance(){
        return earth;
    }
}

我们仔细分析代码👆,可以看到private static Earth earth = new Earth()这行代码是声明了一个静态对象,这个静态对象是在类初始化的时候就会生成,当外部其他类来调用地球这个对象的时候,就不需要做任何初始化了,看起来是一件未雨绸缪的事情。

这时候我们加一个新的需求:当有外部类调用地球这个对象的时候,地球再生成对象;这个需求我们可以这样假设,假如人类一开始是直接生存在宇宙中的,不寄生于任何星球,有一天人类意识到宇宙实在是太大,大家通信实在不方便、成本高,于是向宇宙这个系统发出一个需求指令:能不能创建一个新的星球,让人类移居到这个新的星球上,大家聚在一块,这样沟通成本低、通信方便。 这时候,我们就得给这个需求设定一个新的逻辑:

假设地球一开始是不存在,当人类需要的时候,及时给创建出来,如何设计呢?

还是在原有的代码上做改良,地球这个类唯一暴露给外部类访问的一个方法就是getInstance()👇:

public static Earth getInstance(){
        return earth;
}

而之前的代码,我们知道,其实earth这个对象是早已经生成好的,这时候我们需要对这里的逻辑做改动👇:

public class Earth {
     // private static Earth earth = new Earth();
     private static Earth earth;
     
     private Earth(){
        System.out.println("构造函数");
     }
     
     /** public static Earth getInstance(){
        return earth;
     }*/

     public static Earth getInstance(){
        return new Earth();
     }
     
}

通过注释掉的代码👆,我们可以发现,我们把对象创建的时机挪到了外部类调用的时候,这样就满足了调用时再生成地球对象的原则,但是问题来了,这样的话,如果有5个人来调用getInstance()方法,就会得到5个不同的对象,我们仍然可以用上一节介绍的测试方式:多线程模拟5个人类请求的方式来测试下:

 你会发现,测试的结果是打印出了5个不同的对象地址,这就一下子破坏了单例对象唯一性的原则,我们接着来推敲代码,有人可能会发现,每次我们都是return new Earth(); 如果在这句话上面加一个IF判断,是不是就能解决这个问题了呢:

if (earth == null){
   earth = new Earth();
}
return earth;

你会惊奇地发现5个人类去调用,其中有4个人类调用的结果是获取同一个对象,而其中1个人获取到一个不同的对象,如果再多运行几遍,你可能会发现这样的结果:

这回是3个人调用的结果是获取同一个对象,而另外2个人分别获取了不同的对象,这是为什么?其实当你运行测试代码多次后,通过采样,会发现,有时候能够获得预期的结果,5个人获取同一个对象,而有时获得是不同的对象,接下来我们解决这个概率问题,让每一次获得的对象都是同一个对象,首先要找出问题到底出现在哪里? 归根结底的问题还是在这里👇: 

if(earth == null){
       earth = new Earth();
  }
  return earth;

我们模拟用户调用行为用的是多线程并发的测试方式,而上面这段代码👆是一段任何线程都可以公共访问的代码段,而多线程并发访问这段代码时的顺序有快慢,我们把这段代码分成3个语句块: 

语句1 : if(earth == null)
语句2 : earth = new Earth();
语句3 : return earth;

假设有A、B两个线程,因为线程的执行和速度和操作系统本身的调度有关,所以可能会有如下几种情况发生:

  • 当A还没访问语句1的时候,B已经去访问语句2了,所以这时候,A再去访问语句1,就发现对象已经初始化过了,那么这时候A、B会得到相同的对象

  • 当A访问语句1的时候,B已经访问语句2了,这时候A再去访问语句2,会生成一个新的对象,结果A、B会得到不同的对象

所以这就导致了我们上面的测试结果,那么可能你会疑问,就不能让多个线程去顺序排队去调用这个公用方法吗?答案是可以的,的确,如果多个线程顺序排队去访问这段公用方法,就能让所有线程得到同一个对象,因为只要第一个线程访问过语句2后,其他线程再去访问语句1的时候,就会得到earth != null的结果,然后就执行语句3,最终使所有线程得到的是同一个对象。

那么如果现在要应对的是线程无法排队顺序访问的情况呢,或者说你都不知道调用方是如何调用的,但是我们必须确保的是不管调用方怎么调用,我们都能返回一个唯一的对象,我们继续来对代码推敲:

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

不难发现,我们在外层加了一层锁机制:sychronized,  这把锁就能保证不管有多少个线程,在锁里面的代码同一时刻只能被一个线程访问,再来看下测试结果:

 结果会发现,不管你运行多少次,每一次都能返回同一个对象,到这里就解决了多线程访问的唯一性问题,并且也解决了上面提出的需求:当需要地球这个对象的时候再去生成。

今天主要介绍了单例模式的另外一种写法,下一篇会介绍单例模式的优化以及常用的几种单例模式写法上的区别。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值