【趣味设计模式系列】之【单例模式】
1. 简介
单例模式(Singleton):确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。
2. 图解
类图如下:
3. 案例实现
单例特点:
- 外部类不能随便对单例类创建,故单例的
构造方法必须为private
,在类的内部自行实例化; - 提供一个
public方法入口
,作为唯一调用单例类的途径得到实例。
3.1 饿汉式
package com.wzj.singleton;
/**
* @Author: wzj
* @Date: 2020/2/13 12:41
* @Desc: 饿汉式
* 类加载到内存后,就实例化一个单例,JVM保证线程安全
* 唯一缺点:不管用到与否,类装载时就完成实例化
*/
public class HungrySingleton {
private static final HungrySingleton HUNGRY_SINGLETON = new HungrySingleton();
/**
* 私有构造方法,只有本类才能调用
*/
private HungrySingleton() {
}
public static HungrySingleton getInstance() {
return HUNGRY_SINGLETON;
}
public static void main(String[] args) {
HungrySingleton h1 = HungrySingleton.getInstance();
HungrySingleton h2 = HungrySingleton.getInstance();
System.out.println(h1 == h2);
}
}
执行结果:
true
- 分析:类加载到内存,就实例化一个单例,通过
final
的静态变量保证唯一实例。 - 优点:线程安全。
- 缺点:不管是否用到,类装载时就完成实例化。
3.2 懒汉式
package com.wzj.singleton;
/**
* @Author: wzj
* @Date: 2020/2/13 12:50
* @Desc: 懒汉式-线程不安全
*/
public class LazySingleton {
private static LazySingleton LAZY_SINGLETON;
private LazySingleton() {
}
public static LazySingleton getInstance() {
if (null == LAZY_SINGLETON) {
//睡眠1毫秒,在new对象前,增加被其他线程打断的机会,保证能被多个线程执行
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
LAZY_SINGLETON = new LazySingleton();
}
return LAZY_SINGLETON;
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(() ->
System.out.println(LAZY_SINGLETON.getInstance().hashCode())
).start();
}
}
}
执行结果
1338303845
1276043710
874014640
2116194883
835777993
527405373
38860249
772510047
484927678
1375039115
.
.
.
- 分析:懒加载在调用
getInstace
方法的时候创建实例,通过100个线程测试发现其hashcode
并不相等,并不是单例。 - 优点:改善了饿汉式中实例不用也加载的弊端。
- 缺点:引入了新的问题,线程不安全,并不能保证单例
3.3 synchronized修饰方法单例
package com.wzj.singleton;
/**
* @Author: wzj
* @Date: 2020/2/13 13:50
* @Desc: 懒汉式-线程安全
* 通过synchronized解决,但获取锁带来性能开销,效率下降
*/
public class ThreadSafeSingleton {
private static ThreadSafeSingleton LAZY_SINGLETON;
private ThreadSafeSingleton() {
}
public static synchronized ThreadSafeSingleton getInstance() {
if (null == LAZY_SINGLETON) {
//睡眠1毫秒,在new对象前,增加被其他线程打断的机会,保证能被多个线程执行
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
LAZY_SINGLETON = new ThreadSafeSingleton();
}
return LAZY_SINGLETON;
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(() ->
System.out.println(LAZY_SINGLETON.getInstance().hashCode())
).start();
}
}
}
执行结果
2003664945
2003664945
2003664945
2003664945
2003664945
2003664945
2003664945
2003664945
.
.
.
- 分析:通过
synchronized
关键字保证线程安全。 - 优点:线程安全,保证单例。
- 缺点:方法上加锁导致性能开销。
3.4 synchronized修饰代码块的单例
package com.wzj.singleton;
/**
* @Author: wzj
* @Date: 2020/2/13 13:50
* @Desc: 懒汉式-线程不安全
* 试图通过减小同步代码块的方式提高效率,带来了线程不安全
*/
public class ThreadUnSafeSingleton {
private static ThreadUnSafeSingleton LAZY_SINGLETON;
private ThreadUnSafeSingleton() {
}
public static ThreadUnSafeSingleton getInstance() {
if (null == LAZY_SINGLETON) {
//试图通过减小同步代码块的方式提高效率,带来了线程不安全
synchronized (ThreadUnSafeSingleton.class) {
//睡眠1毫秒,在new对象前,增加被其他线程打断的机会,保证能被多个线程执行
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
LAZY_SINGLETON = new ThreadUnSafeSingleton();
}
}
return LAZY_SINGLETON;
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(() ->
System.out.println(LAZY_SINGLETON.getInstance().hashCode())
).start();
}
}
}
结果:
1276043710
1276043710
1276043710
1276043710
1276043710
1276043710
2116194883
2116194883
2116194883
2116194883
.
.
.
- 分析:试图通过减小同步代码块的方式提高效率,带来了线程不安全。
- 优点:减小了加锁的范围,提高了性能。
- 缺点:线程不安全,结果显示不能保证单例。
3.5 双重检测安全单例
package com.wzj.singleton;
/**
* @Author: wzj
* @Date: 2020/2/13 13:50
* @Desc: 双重检测下线程安全
*
*/
public class DoubleCheckedThreadSafeSingleton {
private volatile static DoubleCheckedThreadSafeSingleton LAZY_SINGLETON;
private DoubleCheckedThreadSafeSingleton() {
}
public static DoubleCheckedThreadSafeSingleton getInstance() {
if (null == LAZY_SINGLETON) {
//试图通过减小同步代码块的方式提高效率,带来了线程不安全
synchronized (DoubleCheckedThreadSafeSingleton.class) {
if (null == LAZY_SINGLETON) {
//睡眠1毫秒,在new对象前,增加被其他线程打断的机会,保证能被多个线程执行
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
LAZY_SINGLETON = new DoubleCheckedThreadSafeSingleton();
}
}
}
return LAZY_SINGLETON;
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(() ->
System.out.println(LAZY_SINGLETON.getInstance().hashCode())
).start();
}
}
}
执行结果
527405373
527405373
527405373
527405373
527405373
527405373
527405373
527405373
.
.
.
- 分析:第一个判空语句
if (null == LAZY_SINGLETON)
,用来检测如果内存中有单例生成以后,永不进入下面的代码,直接走return
语句返回已有的单例,第二个判空语句,保证当前线程拿到锁的前后,内存中都没有单例,才执行创建单例操作,防止中途被其他线程创建单例,进而重复创建;volatile
关键字保证在执行语句LAZY_SINGLETON = new DoubleCheckedThreadSafeSingleton()
时,可以分解为如下的3行伪代码。
memory = allocate(); // 1:分配对象的内存空间
ctorInstance(memory); // 2:初始化对象
instance = memory; // 3:设置instance指向刚分配的内存地址
上面3行伪代码中的2和3之间,可能会被重排序(在一些JIT编译器上,这种重排序是真实发生的)。2和3之间重排序之后的执行时序如下。
memory = allocate(); // 1:分配对象的内存空间
instance = memory; // 3:设置instance指向刚分配的内存地址
// 注意,此时对象还没有被初始化!
ctorInstance(memory); // 2:初始化对象
上面的代码在编译器运行时,可能会出现重排序 从1-2-3 排序为1-3-2,如果发生重排序,另一个并发执行的线程B就有可能在判断instance不为null。线程B接下来将访问instance所引用的对象,但此时这个对象可能还没有被A线程初始化!
- 优点:保证单例与线程安全。
- 缺点:增加了代码的复杂度。
3.6 内部静态类单例
package com.wzj.singleton;
/**
* @Author: wzj
* @Date: 2020/2/13 15:45
* @Desc: 静态内部类方式
* JVM保证单例
* 加载外部类时不会加载内部类,这样可以实现懒加载
*/
public class InnerStaticClassSingleton {
private InnerStaticClassSingleton() {
}
//内部静态类
private static class InnerStaticClassSingletonHolder {
private static final InnerStaticClassSingleton INSTANCE = new InnerStaticClassSingleton();
}
public static InnerStaticClassSingleton getInstance() {
return InnerStaticClassSingletonHolder.INSTANCE;
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(() ->
System.out.println(InnerStaticClassSingleton.getInstance().hashCode())
).start();
}
}
}
执行结果:
772510047
772510047
772510047
772510047
772510047
772510047
772510047
772510047
772510047
772510047
.
.
.
- 分析:因为虚拟机加载类的时候只加载一次,并且加载外部类时不会加载内部类,只有调用getInstance方法的时候,内部类才被加载,所以内部类的静态变量也只加载一次,JVM保证了单例与线程安全。
- 优点:线程安全。
- 缺点:无。
3.7 枚举单例
package com.wzj.singleton;
/**
* @Author: wzj
* @Date: 2020/2/13 16:05
* @Desc: 枚举实现,既可以保证线程安全,还可以防止反序列化。
*/
public enum EnumSingleton {
INSTANCE;
public static void main(String[] args) {
for(int i=0; i<100; i++) {
new Thread(()->{
System.out.println(EnumSingleton.INSTANCE.hashCode());
}).start();
}
}
}
- 优点:线程安全,同时保证不被反序列化,因为枚举类型没有构造方法,不能反序列化后创建对象。
- 缺点:写法优点怪异。
4. 框架源码分析
以下源码分析基于Spring5.0.6 RELEASE。DefaultSingletonBeanRegistry.class
部分源码如下.
@Nullable
public Object getSingleton(String beanName) {
return this.getSingleton(beanName, true);
}
@Nullable
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
// 从单例缓存容器中加载 bean
Object singletonObject = this.singletonObjects.get(beanName);
// 单例缓存容器中的 bean 为空,且当前 bean 正在创建
if (singletonObject == null && this.isSingletonCurrentlyInCreation(beanName)) {
// 加锁
synchronized(this.singletonObjects) {
// 从 earlySingletonObjects 容器中获取
singletonObject = this.earlySingletonObjects.get(beanName);
// earlySingletonObjects容器中没有,且允许提前创建
if (singletonObject == null && allowEarlyReference) {
// 从 singletonFactories 中获取对应的 ObjectFactory
ObjectFactory<?> singletonFactory = (ObjectFactory)this.singletonFactories.get(beanName);
// ObjectFactory 不为空,则创建 bean
if (singletonFactory != null) {
// 获取 bean
singletonObject = singletonFactory.getObject();
// 添加 bean 到 earlySingletonObjects 中
this.earlySingletonObjects.put(beanName, singletonObject);
// 从 singletonFactories 中移除对应的 ObjectFactory
this.singletonFactories.remove(beanName);
}
}
}
}
return singletonObject;
}
- 第一步,从
singletonObjects
中,获取 Bean 对象。 - 第二步,若为空且当前
bean
正在创建中,则从earlySingletonObjects
中获取Bean
对象。 - 第三步,若为空且允许提前创建,则从
singletonFactories
中获取相应的ObjectFactory
对象。若不为空,则调用其ObjectFactory
的getObject(String name)
方法,创建Bean
对象,然后将其加入到earlySingletonObjects
,然后从singletonFactories
删除。
由此可见,Spring
在创建单例bean
的时候,采用的是双重检测加锁机制创建bean
的。
5. 单例与静态方法的比较
- 单例支持延迟加载,静态类第一次加载就初始化;
- 单例常驻内存,除非
JVM
退出,静态方法中的对象,会随着静态方法执行完被释放,gc
回收。
6. 应用场景
- 数据库连接池,因为频繁建立或者关闭数据库连接,损耗性能非常大,因为何用单例模式来维护,就可以大大降低这种损耗;
- 线程池,因为线程是一种稀缺资源,频繁创建线程,会导系统开销增大,线程之间的频繁切换也导致性能下降,由统一的线程池管理线程;
- 开发中常用的配置工具类,因为配置类是共享的资源;
- 日志应用,因为日志属于工享文件,一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加;
- Windows的任务管理器,多次打开只会弹出一个对话框,确保系统由一个任务管理器管理;
- 网站的计数器,如果多个,计数难以同步;
综上,单例应用在共享资源上,要么方便管理,要么节约性能,避免不必要的性能开销。
7. 总结
单例的具体写法,需要结合场景与业务要求,确认是否支持线程安全,是否支持延迟加载,单例比较简单的写法是饿汉式,唯一的不足是不支持懒加载,还有静态内部类;比较完美的写法是双重检测加锁,虽然写法复杂,但支持延迟加载,线程安全,也是Spring源码使用的方式。