单例模式

单例模式

定义

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

通常使用一个私有构造器、一个静态函数、一个私有静态变量来实现。

  • 为了保证类只有一个实例,所以就不能用new关键字和构造器来创建对象实例,因此需要将构造器声明为私有的,只有在类的内部才能调用构造器。
  • 与此同时,需要一个私有静态变量来记录这个唯一的对象实例
  • 还需要一个私有静态函数来返回这个唯一的私有静态变量。

实现

1.懒汉模式与饿汉模式

以下为单例模式中的懒汉模式代码。它的特点是:uniqueInstance被延迟实例化(lazy instantiaze),也就是说如果我们不需要这个实例(不适用getUniqueInstance()函数),那么它就永远不会产生。可以节约资源。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Singleton {
//私有静态变量:记录这个唯一的实例
private static Singleton uniqueInstance;
//私有构造器
private Singleton() {
}
//私有静态函数
public static Singleton getUniqueInstance() {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
}

但以上实现并非线程安全的。考虑线程A和线程B同时调用Singleton.getUniqueInstance(),进入以下的if语句:

1
if (uniqueInstance == null)

此时实例并未被创建,所以A和B都通过了if判断,接下来A使用私有构造器构造了一个对象1。但B因为之前已通过了if判断,所以它也会构造一个对象2。如此一来就会多次实例化,在多线程的情况下无法保证只有一个实例对象。


但如果我们在类初始化时就创建单例的话,就可以保证线程安全。以下为单例模式中的饿汉模式代码。

1
2
3
4
5
6
7
8
public class Singleton{
private static Singleton uniqueInstance = new Singleton();
private Singleton(){
}
public static Singleton getInstance(){
return uniqueInstance;
}
}

利用这个做法,JVM在加载这个类时马上创建此唯一的单件实例。JVM保证在任何线程访问uniqueInstance静态变量之前,一定先创建此实例。

2.双重校验锁:

首先检查实例uniqueInstance是否已经创建了,如果尚未创建,才对实例化语句进行加锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Singleton{
//volatile关键字确保当uniqueInstacne被实例化时,多个线程能正确地处理uniqueInstacne变量
private volatile static Singleton uniqueInstacne;

private Singleton(){
}

public static Singleton getIntance(){
if(uniqueInstance == null){
synchronized(Singleton.class){ //同步锁
if(uniqueInstance == null){
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}

双重校验锁使用两个if语句来保证线程安全:

  • 第一个if语句保证了只有在第一次调用getIntance()方法的时候才进行加锁操作,之后直接return已被构造的单例即可,无需加锁。
  • 第二个if语句避免了重复实例化对象。例如:两个线程同时进入synchronized临界区,若没有这步if判断,当线程A构建完对象,B由于已经通过了第一个if语句,不知道A已经构造好了对象,于是它也会再构造另一个对象。

对单例uniqueInstacne使用volatile关键字的原因
在Java中这样一句uniqueInstance = new Singleton(),会被JVM编译成如下指令:

  1. 给uniqueInstance分配内存空间

  2. 初始化该内存空间的对象

  3. 将uniqueInstance指向已分配的内存地址

    但这几步顺序也有可能经过JVM和CPU的优化,被重排成1、3、2的顺序。在多线程的情况下:当线程A完成指令1、3后,对象还未被初始化但是uniqueInstance已经不指向null。这时如果线程B走到了第一步if判断,会发现uniqueInstance不为null,于是直接return uniqueInstance,但这时单例还未被初始化。从而会返回一个尚未初始化完成的对象。

使用volatile可以避免JVM的指令重排,如此一来将始终保证1、2、3的顺序。所以uniqueInstacne要么指向null,要么指向一个已初始化的对象,不会出现中间状态,保证了线程安全。

3.静态内部类实现

从外部无法访问静态内部类LazyHolder,当调用Singleton.getInstance()方法时,才能得到单例对象INSTANCE。

当加载类Singleton时,类LazyHolder并没有被加载,因此单例INSTANCE并未被初始化。当调用Singleton.getInstance()方法时,内部类LazyHolder才被加载,INSTANCE才被实例化。

1
2
3
4
5
6
7
8
9
10
11
public class Singleton {
private static class LazyHolder {
private static final Singleton INSTANCE = new Singleton();
}

private Singleton (){}

public static Singleton getInstance() {
return LazyHolder.INSTANCE;
}
}

参考