设计模式之单例模式

老铁们,今天开始,我们聊聊设计模式。

前言

有人说了,为啥要用设计模式,我不用它,不是照样能实现业务么。是的,你说的没错,确实是这样。但是,要知道,好的设计模式下,程序才会有更好的抽象性、复用性和扩展性,程序代码才能在业务需求发生变化时,对原逻辑改动最小,他好你也好嘛。

比如,策略模式,就是先抽象出一个父类,而将不同的逻辑用不同的子类去继承实现,如果需要增加新的逻辑分支,只需要再扩展出一个子类即可,而不需要修改父类及其他子类。

正如梅耶大爷提到的软件设计中最重要的原则之一:开闭原则

对扩展开放,对修改关闭。

再比如,适配器模式。假设有个十年前的老系统,现在也需要加入到目前流行的微服务注册中心去管理,时间紧,任务重,怎么办。这时,就可用适配器的设计思想,用微服务的方式包装一层,适配一下。既不影响老系统的业务逻辑,又可以实现新的管理方式。

在编码中,设计模式的运用,体现了一个程序员的良好修养。目前,公认的设计模式有23种,后面我们都会一一讲到。

单例模式

今天,咱们先从最简单的单例模式开始。 所谓单例模式,是指系统中只允许有一个实例存在。怎么来保证呢?

public class Singleton01 {
 private static Singleton01 sg = new Singleton01();
 private Singleton01(){
 }
 public static Singleton01 getInstance(){
    return sg;
 }
 public static void main(String[] args) {
  int i=0;
  while(i<100){
   new Thread(()->{
    System.out.println(getInstance());
   }).start();
   i++;
  }
 }
}

我们首先定义一个private 的构造函数,防止其他程序再去new它。为什么呢?A程序new一个,B程序new一个,当然就不是单例了。

同时,提供一个静态公共方法 public static Singleton01 getInstance(),供其它程序调用,来获取该类的实例。有人说了,这里不用static行吗,那太不行了,这个方法就是要去让其他类调用去创建对象的嘛。

由于sg对象被定义为static的,Singleton01类在加载、初始化时,就会执行 new Singleton01() 操作, 把sg对象初始化好。

在getInstance()方法中,我们直接返回类加载时创建的唯一sg对象。

这就叫做单例模式的饿汉式方法,顾名思义就是迫不及待,先创建好实例。

我们先看下这样写有没有问题,是不是确实是单例。运行下(为了避免重复,main方法在下面代码中不再展示,其实目的就是在多线程环境下打印出对象的内存地址,来验证是否是同一个对象),可以看到输出的是同一个地址,没问题。

Singleton.Singleton01@1e60980
Singleton.Singleton01@1e60980
Singleton.Singleton01@1e60980
Singleton.Singleton01@1e60980
Singleton.Singleton01@1e60980
...

饿汉式存在的问题

有人说了,你这个对象一上来就加载好了,万一我压根儿不用呢,这不是浪费内存吗?

那我们换个写法,调用 getInstance 时,先判断,为空时再创建对象。对,这就是传说的 懒汉式,它不着急嘛。

懒汉式

1.

public class Singleton02 {
 private static Singleton02 sg;
 private Singleton02(){
 }
 public static Singleton02 getInstance(){
  if(sg==null){
     dosth();
     sg = new Singleton02();
  }
  return sg;
 }
  //模拟耗时的逻辑
 private static void dosth(){
  try {
     Thread.sleep(10);
  } catch (InterruptedException e) {
   e.printStackTrace();
  }
 }

Singleton.Singleton02@172603c
Singleton.Singleton02@172603c
Singleton.Singleton02@1e60980
Singleton.Singleton02@1406116
...

明显可以看到,内存地址不一样了,也就是说,产生了多个Singleton02对象,说明这样写是不对的。

那问题出在哪儿了呢?是 dosth 吗?看起来好像是,把他去掉就好了。其实不是,我们这里加 dosth(为避免重复,以下代码省略) 只是模拟了创建对象的过程。

我们分析一下原因。

线程1和线程2上来,都判断对象为空,都去创建对象了。

所以,问题在于,多线程环境下,并发访问,没有加锁。

2.

public class Singleton03 {
 private static Singleton03 sg;
 private Singleton03(){
 }
 public synchronized static Singleton03 getInstance(){
  if(sg==null){
     dosth();
     sg = new Singleton03();
  }
  return sg;
 }
}
Singleton.Singleton03@173340f
Singleton.Singleton03@173340f
Singleton.Singleton03@173340f
Singleton.Singleton03@173340f
Singleton.Singleton03@173340f
...

现在来看,代码没问题,是单例了。但是我们这样直接在方法声明上加synchronized关键字会有效率问题,因为每次调用该方法取对象时,即使对象已经存在了,还是要去申请锁。

3.

为了改进上述写法上存在的效率问题,我们改进一下,缩小synchronized的范围。增加判断,如果为空,需要创建对象时才加锁。

public class Singleton04 {
 private static Singleton04 sg;
 private Singleton04(){
 }
 public static Singleton04 getInstance(){
  if(sg==null){
   synchronized(Singleton04.class){
      dosth();
      sg = new Singleton04();
   }
  }
  return sg;
 }
}
Singleton.Singleton04@afc10b
Singleton.Singleton04@afc10b
Singleton.Singleton04@14c637e
Singleton.Singleton04@14c637e
Singleton.Singleton04@df4aed
Singleton.Singleton04@12c6b2b

从运行结果上看,是有问题的,问题出在哪儿呢,我们分析一下。

 

 

假设有2个线程同时访问getInstance方法,线程1和线程2在时刻1都判断sg为空,线程1在时刻2拿到锁并且创建了对象,返回,然后释放锁。随后,线程2在时刻3拿到锁并且又创建了对象。此时,系统中便产生了两个实例。

那怎么办呢?我们改进一下,在加锁之后,再次判断,实例是否为空。

4.

public class Singleton05 {
 private static volatile Singleton05 sg;
 private Singleton05(){
 }
 public static Singleton05 getInstance(){
  if(sg==null){
   synchronized(Singleton05.class){
    if(sg==null){
        dosth();
        sg = new Singleton05();
    }
   }
  }
  return sg;
 }
}
Singleton.Singleton05@142188f
Singleton.Singleton05@142188f
Singleton.Singleton05@142188f
Singleton.Singleton05@142188f
Singleton.Singleton05@142188f
...

这种写法就叫做DCL(Double Check Lock),判断两次是否为空。其实第一次的判断能拦截大多数的请求了,如果对象已经创建,就不会进行后续的加锁处理了。相比上面一种写法,效率上会有很大提升。

另外,还有一个点,不知道大家注意到没有。对,就是关键字 volatile 。我们都知道,volatile的两大作用:线程可见和禁止指令重排。

这里sg对象声明时增加volatile修饰,是为了防止JVM进行指令重排的。

简单来讲,就是防止对象在没有完全初始化的情况下返回。在这先不展开了,我们以后讲JVM的时候再细说。

静态内部类

我们再来看最后一种,静态内部类的写法。

public class Singleton06 {
 private Singleton06(){
 }
 private static class Singleton06_Inner{
  private static Singleton06 sg = new Singleton06();
 }
 public static Singleton06 getInstance(){
  return Singleton06_Inner.sg;
 }
}
Singleton.Singleton06@3c0755
Singleton.Singleton06@3c0755
Singleton.Singleton06@3c0755
Singleton.Singleton06@3c0755

我们还是将构造方法定义成private的,其他类不能去创建对象。 在这里声明了一个内部类Singleton06_Inner,它是可以去调用外部类的构造方法去创建对象的。

那有人说了,都是一上来就创建对象,这跟上面的饿汉式有啥区别。

我们说,内部类是在属性或方法被调用的时候才会被加载,而虚拟机加载一个类的时候,只加载一次,这是虚拟机内部去保证的。

所以说,这个方式可以实现懒加载且是单例。

使用场景

单例模式一般应用于资源共享的情况下,例如数据库连接池、线程池、缓存、日志对象、读取配置文件的类等。

Spring中也有用过单例模式,比如默认情况下的bean等。

总结

我们今天讲了单例模式,即保证系统中只能创建一个类实例。 主要讲了创建单例的三种写法,饿汉式(类初始化时就创建对象)、懒汉式(DCL)以及静态内部类的方式。

饿汉式相对简单,但是可能会浪费系统资源; 懒汉式稍复杂,需要注意到多线程访问时的处理; 静态内部类是简单又安全,推荐使用。

好了,本次分享就到这里,下个设计模式见。

最后,如果你觉得有用,写的还可以的话,欢迎转发、点赞,感谢。

我是冷风,专注于技术开发领域,关注我,让我们一起成长。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

冷风在北京

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值