设计模式:单例模式
应用场景和解决的问题
单例模式的主要目的是对资源开销,或者说是对对象的创建进行管理。在很多的应用场景中,一个对象是可以被许多应用所共同使用的,我们并不希望对不同的应用重复创建对象。比如说一个数据库的连接管理对象,线程池的管理对象等等。这些对象对任何的应用不会有差别,一个对象完全可以覆盖所有的应用场景,在此场景下,单例模式就能够很好地对对象的构建进行封装和管理。
除了保证全局只有一个对象,单例模式还可以按需创建对象,这对创建开销比较大的对象很有帮助。
最基本的单例模式
单例模式的基本框架:
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
关键字或许是最为有效且直观的办法,最后一个例子虽然很高效,但是它增加了代码的复杂性,降低了可读性,只有真正需要的时候在考虑。