详解单例模式
Posted 小刘你最强
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了详解单例模式相关的知识,希望对你有一定的参考价值。
概念:
java中单例模式是一种常见的设计模式,单例模式的写法有好几种,这里主要介绍三种:懒汉式单例、饿汉式单例、登记式单例。
单例模式有以下特点:
1、单例类只能有一个实例。
2、单例类必须自己创建自己的唯一实例。
3、单例类必须给所有其他对象提供这一实例。
单例模式确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例。在计算机系统中,线程池、缓存、日志对象、对话框、打印机、显卡的驱动程序对象常被设计成单例。这些应用都或多或少具有资源管理器的功能。每台计算机可以有若干个打印机,但只能有一个Printer Spooler,以避免两个打印作业同时输出到打印机中。每台计算机可以有若干通信端口,系统应当集中管理这些通信端口,以避免一个通信端口同时被两个请求同时调用。总之,选择单例模式就是为了避免不一致状态,避免政出多头。
1.恶汉式单例模式
//饿汉式单例类.在类初始化时,已经自行实例化
public class Singleton {
private Singleton() {}
private static final Singleton instance = new Singleton();
//静态工厂方法
public static Singleton getInstance() {
return instance;
}
}
饿汉式在类创建的同时就创建好了对象,是线程安全的。
2.懒汉单例模式
//懒汉式单例类.在第一次调用的时候实例化自己
public class Singleton {
private Singleton() {}
private static Singleton instance = null;
//静态工厂方法
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return single;
}
}
懒汉式的单例模式在并发环境下,就不是单例的了,下面对懒汉式进行改造
将getInstance()方法加锁
public class Singleton {
private Singleton() {}
private static Singleton instance = null;
//静态工厂方法
public synchronized static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
虽然保证了线程安全,但是当多个线程调用时效率太低了。
双重校验锁
public class Singleton {
private Singleton() {}
private static volatile Singleton instance = null;
//静态工厂方法
public static Singleton getInstance() {
if(instance == null) {
synchronized (Singleton.class) {
if(instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
接下来我们对双重校验锁进行分析
第一个if判断主要是为了效率,如果第一次检查instance不为null,那么就不需要执行下面的加锁和初始化操作,因此可以大幅度降低synchronized带来的性能开销。
第二个if判断主要是为了安全,当有两个线程都经过了第一个if之后,其中的一个线程获取到了锁,然后创建对象释放锁,第二个线程获取到了锁之后,instance不为null,直接使用已经创建好的对象了。
细心的同学会发现,在声明变量时,有instance被volatile这个关键字修饰,那么这个volatile有什么用?为什么要加这个关键字呢?
首先我们直到volatile的作用是保证可见性和禁止指令重排序。在懒汉式中,好像没有用到可见性,如果有请各位批评纠正。。。
getInstance()方法对应的字节码指令:
0: getstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
3: ifnonnull 37
6: ldc #3 // class cn/itcast/n5/Singleton
8: dup
9: astore_0
10: monitorenter
11: getstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
14: ifnonnull 27
17: new #3 // class cn/itcast/n5/Singleton
20: dup
21: invokespecial #4 // Method "<init>":()V
24: putstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
27: aload_0
28: monitorexit
29: goto 37
32: astore_1
33: aload_0
34: monitorexit
35: aload_1
36: athrow
37: getstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
40: areturn
其中
- 17 表示创建对象,将对象引用入栈 // new Singleton
- 20 表示复制一份对象引用 // 引用地址
- 21 表示利用一个对象引用,调用构造方法
- 24 表示利用一个对象引用,赋值给 static INSTANCE
从字节码可以看到创建一个对象实例,可以分为三步:
- 分配对象内存
- 调用构造器方法,执行初始化
- 将对象引用赋值给变量。
也许 jvm 会优化为:先执行 24,再执行 21,即先将引用赋值给变量,在初始化。如果两个线程 t1,t2 按如下时间序列执行:
解释一下,t1线程执行到将引用赋值给变量时(此时的引用时内存中的一些无效地址),当t2线程执行时instance不为null,直接return,然后使用这个对象时还没有调用构造方法,此时出错。
而volatile的禁止指令重排序就能解决这个问题
volatile可以保证在赋值给共享变量之前,已经创建好了对象,当t2线程读取时,instance不为空且时一个真正的对象了。
以上是关于详解单例模式的主要内容,如果未能解决你的问题,请参考以下文章