设计模式之单件模式

《Head First 设计模式》阅读笔记

Posted by wangtiegang on October 27, 2019

单件模式:确保一个类只有一个实例,并提供一个全局访问点。

单件模式是一个很简单的设计模式,在开发过程中有一些对象其实我们只需要一个,比如线程池,缓存,日志对象等等。这类对象如果制造多个实例,就会导致许多问题产生,比如程序行为异常,资源使用过度,全局配置不一致等等。

我们知道,创建一个只有单个实例的对象,可以通过 static final 关键字修饰来达到,并且具有全局访问点,那为什么还需要单件模式呢?其实使用java静态变量有一些缺点,比如必须在程序一开始就创建好对象,如果这个对象非常耗费资源,并且一直没有用到它,就会造成浪费。这并不是说不能使用静态变量来实现功能,而是需要具体情况具体分析。

单件模式的经典实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package singleton;

public class Singleton {

    /**
     * 用一个静态变量记录Singleton类的唯一实例
     */
    private static Singleton singletonInstance;

    /**
     * 把构造方法声明为私有的,只能Singleton内部才能调用,不能外部 new 一个实例
     */
    private Singleton(){
        
    }

    /**
     * 实例化唯一对象,并返回这个实例
     * @return
     */
    public static Singleton getInstance(){
        if (singletonInstance == null){
            singletonInstance = new Singleton();
        }
        return singletonInstance;
    }
    
    // Singleton 的其他变量和方法 ...
    
}

经典单件模式通过 getInstance 方法来确保产生得到唯一实例,并且是延迟加载,只有在第一次使用的时候才会去创建这个实例。

看起来经典实现没有什么问题,实际上在多线程模式下会出现问题,没法保证只创建了一个实例,如果有两个线程同时第一次调用 getInstance 方法,那么就有可能同时判断 singletonInstance 为空,接着同时创建了两个实例,那么程序就会出现各种问题。

处理多线程

上面的问题只需要在 getInstance 方法上加 synchronized 关键字,变成同步方法就可以解决问题了,同步方法确保了同一时间只能有一个线程能进入方法。但是有一点不好,那就是同步会降低性能,而且更严重的是:只有第一次调用 getInstance 方法时才会需要同步,如果要很频繁的调用方法,则显然是一种浪费。

怎么样改变这种情况呢?书上给出了三种选择:

  • 如果对同步方法造成的性能降低没有影响,就使用 synchronized ,既简单又有效。

  • 使用“急切”创建实例,而不是延迟实例化的做法

    如果程序必定会创建并使用单件实例,或者在创建和运行时的负担不太繁重,可以一开始就创建好实例,就像使用静态变量一样。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    
    package singleton;
    
    public class SingletonInit {
    
      private static SingletonInit singletonInstance = new SingletonInit();
        
      private SingletonInit(){
    
      }
    
      public static SingletonInit getInstance(){
          return singletonInstance;
      }
    
      // SingletonInit 的其他变量和方法 ...
    }
    
  • 用“双重检查加锁”在 getInstance 中减少使用同步

    首先检查实例是否已经创建了,如果没有创建,才进行同步,这样一来,只有第一次会同步,就不会有性能问题了。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    
    package singleton;
    
    public class SingletonDoubleChecked {
    
      /**
       * volatile让变量每次在使用的时候,都从主存中取。而不是从各个线程的“工作内存”。
       */
      private volatile static SingletonDoubleChecked instance;
    
      private SingletonDoubleChecked(){}
    
      public static SingletonDoubleChecked getInstance(){
          if (instance == null){
              // 只有第一次才会执行
              synchronized (SingletonDoubleChecked.class){
                  // 进入同步区块后再次判断是否已被创建
                  if (instance == null){
                      instance = new SingletonDoubleChecked();
                  }
              }
          }
          return instance;
      }
    
    }
    

    这里得注意 volatile 这个关键字,在Java内存模型中,有主存,每个线程也有自己的工作内存 (例如寄存器)。为了性能,一个线程会在自己的内存中保持要访问的变量的副本。这样就会出现同一个变量在某个瞬间,在一个线程的内存中的值可能与另外一个线程内存中的值,或者主存中的值不一致的情况。 一个变量声明为 volatile ,就意味着这个变量是随时会被其他线程修改的,因此不能将它cache在线程内存中。

以上就是书中关于单件模式的全部内容了。