Post

设计模式:单例模式

应用场景和解决的问题

单例模式的主要目的是对资源开销,或者说是对对象的创建进行管理。在很多的应用场景中,一个对象是可以被许多应用所共同使用的,我们并不希望对不同的应用重复创建对象。比如说一个数据库的连接管理对象,线程池的管理对象等等。这些对象对任何的应用不会有差别,一个对象完全可以覆盖所有的应用场景,在此场景下,单例模式就能够很好地对对象的构建进行封装和管理。

除了保证全局只有一个对象,单例模式还可以按需创建对象,这对创建开销比较大的对象很有帮助。

最基本的单例模式

单例模式的基本框架:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Singleton {
  private static Singleton uniqueInstance;

  // ...

  private Singleton() {}

  public static Singleton getInstance() {
    if (uniqueInstance == null) {
      uniqueInstance = new Singleton();
    }
    return uniqueInstance;
  }

  // ...
}

这里的重点是,我们将构造器方法设置成私有,这样外部就没办法直接通过常规的构造方法来构造对象,取而代之的是通过静态方法 getInstance 来获取对象。另外,一开始的时候,我们并没有构造对象,也就是 uniqueInstance 是空的,只有 getInstance 被调用,对象才会被创建,这其实是 lazy instantiation,也就是我们之前提到的按需创建,这适用于开销比较大,应用场景少的对象的创建。

但这其实是最为基本的单例模式,它其实存在着一些问题,我们需要根据具体的场景做一些调整。

多线程下的单例模式

上面提到的单例模式在单线程模式下不会存在问题,但到了多线程下,我们需要考虑到的一个情况是 getInstance 方法会被多个线程同时调用。比如,两个线程同时叫了 getInstance 方法,并且此时的 unqiueInstance 为空,很可能两个线程都开始创建新的对象并返回结果(引用),但是前面一个线程创建的对象会被后面线程创建的对象所覆盖掉,虽然在 Singleton 类中存放的内容被覆盖,但是全局其实有两个 unqiueInstance,并且其中一个还不在我们的控制范围内。这样 unqiueInstance 中更新的结果就会不准确。

想要解决这个问题也很简单,就是加锁:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Singleton {
  private static Singleton uniqueInstance;

  // ...

  private Singleton() {}

  public static synchronized Singleton getInstance() {
    if (uniqueInstance == null) {
      uniqueInstance = new Singleton();
    }
    return uniqueInstance;
  }

  // ...
}

这样,在多线程的环境下,getInstance 一次只能被一个线程调用,无法同时被两个或者多个线程调用。这样做确实解决了问题,但这又带来了另外一个问题,synchronized 其实是一个开销相对较大的操作,而这里的问题仅仅发生在对象还没有创建时,或者将要被创建时。而加上这个方法锁,等于说是任何时候 getInstance 都不能在多线程下被高效的使用了。

至于解决方案,我们就需要考虑实际的应用场景了,如果说创建对象的开销不大,并且是必须的,那么我们可以不使用 lazy instantiation:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Singleton {
  private static Singleton uniqueInstance = new Singleton();

  // ...

  private Singleton() {}

  public static Singleton getInstance() {
    return uniqueInstance;
  }

  // ...
}

这样的话,就不需要在运行环境下创建对象,也就不需要担心多线程下的线程安全问题了。

当然了,如果你还是想要 lazy instantiation,也可以考虑缩小锁的范围,我们只需要在创建对象时上锁,而不是将整个方法上锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Singleton {
  private volatile static Singleton uniqueInstance;

  // ...

  private Singleton() {}

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

  // ...
}

上面代码中,我们两次判断 uniqueInstance 是否为空,这是非常有必要的,因为第一次判断时有可能对象在正在创建中,等对象创建完了,等待的线程就会进入到创建对象的模块中,这里必须要再进行一次判断,才能保证对象不被重复创建。

最后提一点,上面给出的多线程下的范例并没有好坏之分。我们需要结合具体的场景来选择。如果 getInstance 方法并不常被调用,那么直接加上 synchronized 关键字或许是最为有效且直观的办法,最后一个例子虽然很高效,但是它增加了代码的复杂性,降低了可读性,只有真正需要的时候在考虑。

This post is licensed under CC BY 4.0 by the author.