单例模式
单例模式
定义
单例模式确保一个类只有一个实例,并提供一个全局访问点
通常使用一个私有构造器、一个静态函数、一个私有静态变量来实现。
- 为了保证类只有一个实例,所以就不能用new关键字和构造器来创建对象实例,因此需要将构造器声明为私有的,只有在类的内部才能调用构造器。
- 与此同时,需要一个私有静态变量来记录这个唯一的对象实例
- 还需要一个私有静态函数来返回这个唯一的私有静态变量。
实现
1.懒汉模式与饿汉模式
以下为单例模式中的懒汉模式代码。它的特点是:uniqueInstance被延迟实例化(lazy instantiaze),也就是说如果我们不需要这个实例(不适用getUniqueInstance()函数),那么它就永远不会产生。可以节约资源。
1 |
|
但以上实现并非线程安全的。考虑线程A和线程B同时调用Singleton.getUniqueInstance(),进入以下的if语句:
1 |
|
此时实例并未被创建,所以A和B都通过了if判断,接下来A使用私有构造器构造了一个对象1。但B因为之前已通过了if判断,所以它也会构造一个对象2。如此一来就会多次实例化,在多线程的情况下无法保证只有一个实例对象。
但如果我们在类初始化时就创建单例的话,就可以保证线程安全。以下为单例模式中的饿汉模式代码。
1 |
|
利用这个做法,JVM在加载这个类时马上创建此唯一的单件实例。JVM保证在任何线程访问uniqueInstance静态变量之前,一定先创建此实例。
2.双重校验锁:
首先检查实例uniqueInstance是否已经创建了,如果尚未创建,才对实例化语句进行加锁。
1 |
|
双重校验锁使用两个if语句来保证线程安全:
- 第一个if语句保证了只有在第一次调用getIntance()方法的时候才进行加锁操作,之后直接return已被构造的单例即可,无需加锁。
- 第二个if语句避免了重复实例化对象。例如:两个线程同时进入synchronized临界区,若没有这步if判断,当线程A构建完对象,B由于已经通过了第一个if语句,不知道A已经构造好了对象,于是它也会再构造另一个对象。
对单例uniqueInstacne使用volatile关键字的原因 :
在Java中这样一句uniqueInstance = new Singleton(),会被JVM编译成如下指令:
给uniqueInstance分配内存空间
初始化该内存空间的对象
将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 |
|
参考
- Eric Freeman. Head First 设计模式 [M]. 中国电力出版社, 2007.
- 漫画:什么是单例模式?
- Cyc2018:设计模式-单例