设计模式 --单例模式

前言

单例模式应该是我们最熟悉的模式了,如果说要随便抓一个程序员,让他说一说最熟悉的集中设计模式,我想肯定有单例模式。

我们这节就全面的来讲解一下单例模式。

为什么要用单例模式

单例模式理解起来非常简单。在一个系统中,一个类只允许创建一个对象,那这个类就是单例类,这种设计模式就叫做单例设计模式。

为什么需要单例模式呢?首先我们得熟知他的运用场景。就是某个类比如工厂类,配置类,我们系统中只需要一份得,无需多次重复创建的类,我们就可以用单例模式。

单例模式的实现方式

饿汉式

饿汉式的实现方式比较简单。在类加载的时候,instance静态实例就已经创建并初始化好了,所以,instance实例的创建过程是线程安全的。不过,这样的实现方式不支持延迟加载(在真正使用到的时候,再创建),代码如下:

public class IdGenerator { 
  private AtomicLong id = new AtomicLong(0);
  private static final IdGenerator instance = new IdGenerator();
  private IdGenerator() {}
  public static IdGenerator getInstance() {
    return instance;
  }
  public long getId() { 
    return id.incrementAndGet();
  }
}

IdGenerator类是被静态变量修饰并且是直接实例化的。所以当我们调用getInstance()方法,jvm会加载IdGenerator,(加载的过程中,jvm会经历加载->验证->准备->解析->初始化,该过程是天然加锁的),初始化完成以后成员变量instance就指向了IdGenerator实例对象。又因为IdGenerator的构造方法是private修饰的。所以通过该方法我们就实现了饿汉式的单例模式。

懒汉式

有饿汉式,就有对应的懒汉式。懒汉式相对于饿汉式的优势是支持延迟加载。具体代码如下:

public class IdGenerator { 
  private AtomicLong id = new AtomicLong(0);
  private static IdGenerator instance;
  private IdGenerator() {}
  public static synchronized IdGenerator getInstance() {
    if (instance == null) {
      instance = new IdGenerator();
    }
    return instance;
  }
  public long getId() { 
    return id.incrementAndGet();
  }
}

不过懒汉式的缺点也很明显,我们给getInstance()方法加了一把大锁,导致这个函数的并发度很低。量化一下的话,并发度是1,也就相当于串行操作了。而这个函数实在单例使用期间,一直会被调用。如果这个单例类偶尔会被用到,那这种实现方式还可以接受。但是,如果频繁地使用到,那频繁的加锁和释放锁以及并发度低等问题,会导致性能很差。

双重检查锁

饿汉式不支持延迟加载,懒汉式有性能问题。那么有没有一种及支持延迟加载又支持高并发的单例实现方式呢?

有的,双重检查锁登场~我们来看如下代码:

public class IdGenerator { 
  private AtomicLong id = new AtomicLong(0);
  private static volatile IdGenerator instance;
  private IdGenerator() {}
  public static IdGenerator getInstance() {
    if (instance == null) {
      synchronized(IdGenerator.class) { // 此处为类级别的锁
        if (instance == null) {
          instance = new IdGenerator();
        }
      }
    }
    return instance;
  }
  public long getId() { 
    return id.incrementAndGet();
  }
}

这种实现方式中,只要instance被创建之后,即使再调用geiInstance()函数也不会再进入加锁逻辑中。所以,这种方式实现解决了懒汉式并发度低的问题。

注意,我们给instance成员变量加上了volatile的关键字。为什么呢?

重排序可能会让该代码出现问题,因为我们new一个对象的顺序是 开辟内存空间->创建对象->指向该内存空间。如果我们不加上volatile关键字,就可能会发生重排序,变成 开辟内存空间->指向该内存空间->创建对象。

大家想一想,是不是就有可能导致IdGenerator对象被new出来,并且赋值给instance之后,并没有来得及初始化,就被另一个线程使用了。

静态内部类

我们再来看一种比双重检查锁更加简单的实现方法,那就是利用java的静态内部类。它有点类似饿汉式,但又能做到延迟加载。我们来看看它的实现:


public class IdGenerator { 
  private AtomicLong id = new AtomicLong(0);
  private IdGenerator() {}

  private static class SingletonHolder{
    private static final IdGenerator instance = new IdGenerator();
  }
  
  public static IdGenerator getInstance() {
    return SingletonHolder.instance;
  }
 
  public long getId() { 
    return id.incrementAndGet();
  }
}

SingletonHolder是一个静态内部类,当外部类IdGenerator被加载的时候,并不会创建SingletonHolder实例对象。只有当调用getInstance()方法的时候,SingletonHolder才会被加载,这个时候才会创建instance。instance的唯一性、创建过程的线程安全性,都由JVM来保证,所以,这种实现方式既保证了线程安全,又能做到延迟加载。

枚举

我们再来看最后一种实现方式,枚举。

这种方式通过java枚举类型本身的特性,保证了实例创建的线程安全性和实例的唯一性。具体代码如下:

public enum IdGenerator {
  INSTANCE;
  private AtomicLong id = new AtomicLong(0);
 
  public long getId() { 
    return id.incrementAndGet();
  }
}

总结

单例模式是设计模式中比较简单一种,也是每个程序员必须掌握的设计模式。五种实现方式都应该掌握。

我们最后再讨论一个问题,延迟加载的好坏?

其实就我而言,我反而觉得饿汉式是最简单,也是相对最优的实现方式。为什么呢?其实大家好像有一个共识,提前初始化是一种浪费资源的行为。最好的方式应该是再用到的时候再去初始化。但是仔细想一想真的是这样的吗?

我们大部分同学作为业务性开发。而单例模式是创建好就一直存在我们系统中。如果说创建的过程中发生资源不够,或者异常的时候。我们是希望在系统启动的时候就发现还是在系统运行到一半的时候呢?

肯定是系统一创建我们就发现问题,就能立即去修复。这样也能避免程序在运行一段时间后,突然因为初始化这个实例占用资源过多,导致系统奔溃,影响系统的可用性。所以我的建议还是作为业务性开发,尽量还是使用饿汉式~

如果你有什么不同的想法,欢迎留言~

posted @ 2020-07-27 18:58  正号先生  阅读(278)  评论(0编辑  收藏  举报