关键字: singleton 单例 写法 双锁 线程安全
饿汉式单例类
-
public class Singleton
-
{
-
private Singleton(){
-
-
}
-
-
private static Singleton instance = new Singleton();
-
-
private static Singleton getInstance(){
-
return instance;
-
}
-
}
饿汉式提前实例化,没有懒汉式中多线程问题,但不管我们是不是调用getInstance()都会存在一个实例在内存中
内部类式单例类
-
public class Singleton
-
{
-
private Singleton(){
-
-
}
-
-
private class SingletonHoledr(){
-
private static Singleton instance = new Singleton();
-
}
-
-
private static Singleton getInstance(){
-
return SingletonHoledr.instance;
-
}
-
}
内部类式中,实现了延迟加载,只有我们调用了getInstance(),才会创建唯一的实例到内存中.并且也解决了懒汉式中多线程的问题.解决的方式是利用了Classloader的特性.
懒汉式单例类
-
public class Singleton
-
{
-
private Singleton(){
-
-
}
-
-
private static Singleton instance;
-
public static Singleton getInstance(){
-
if(instance == null){
-
return instance = new Singleton();
-
}else{
-
return instance;
-
}
-
}
-
}
在懒汉式中,有线程A和B,当线程A运行到第8行时,跳到线程B,当B也运行到8行时,两个线程的instance都为空,这样就会生成两个实例。解决的办法是同步:
可以同步但是效率不高:
双检锁写法:
-
public class Singleton{
-
private static Singleton instance; //声明静态的单例对象的变量
-
private Singleton(){} //私有构造方法
-
-
public static Singleton getSingle(){ //外部通过此方法可以获取对象
-
if(instance== null){
-
synchronized (Singleton.class) { //保证了同一时间只能只能有一个对象访问此同步块
-
if(instance== null){
-
instance= new Singleton();
-
}
-
}
-
}
-
return instance; //返回创建好的对象
-
}
-
}
public class Singleton{
private static Singleton instance; //声明静态的单例对象的变量
private Singleton(){} //私有构造方法
public static Singleton getSingle(){ //外部通过此方法可以获取对象
if(instance== null){
synchronized (Singleton.class) { //保证了同一时间只能只能有一个对象访问此同步块
if(instance== null){
instance= new Singleton();
}
}
}
return instance; //返回创建好的对象
}
}
为什么要在 if 语句中使用两次判断 instance== null ,这里涉及到一个名词 Double-Check Locking ,也就是双重检查锁定,为何要使用双重检查锁定呢?
考虑这样一种情况,就是有两个线程同时到达,即同时调用 GetInstance(),此时由于 instance== null ,所以很明显,两个线程都可以通过第一重的 instance== null ,进入第一重 if 语句后,由于存在锁机制,所以会有一个线程进入 lock 语句并进入第二重 instance== null ,而另外的一个线程则会在 lock 语句的外面等待。而当第一个线程执行完 new Singleton()语句后,便会退出锁定区域,此时,第二个线程便可以进入 lock 语句块,此时,如果没有第二重 instance== null 的话,那么第二个线程还是可以调用 new Singleton()语句,这样第二个线程也会创建一个 Singleton 实例,这样也还是违背了单例模式的初衷的,所以这里必须要使用双重检查锁定。
细心的朋友一定会发现,如果我去掉第一重 instance== null ,程序还是可以在多线程下完好的运行的,考虑在没有第一重 instance== null 的情况下,当有两个线程同时到达,此时,由于 lock 机制的存在,第一个线程会进入 lock 语句块,并且可以顺利执行 new Singleton(),当第一个线程退出 lock 语句块时, instance这个静态变量已不为 null 了,所以当第二个线程进入 lock 时,还是会被第二重 instance== null 挡在外面,而无法执行 new Singleton(),所以在没有第一重 instance== null 的情况下,也是可以实现单例模式的?那么为什么需要第一重 instance== null 呢?这里就涉及一个性能问题了,因为对于单例模式的话,new Singleton()只需要执行一次就 OK 了,而如果没有第一重 instance== null 的话,每一次有线程进入 GetInstance()时,均会执行锁定操作来实现线程同步,这是非常耗费性能的,而如果我加上第一重 instance== null 的话,那么就只有在第一次,也就是 instance ==null 成立时的情况下执行一次锁定以实现线程同步,而以后的话,便只要直接返回 Singleton 实例就 OK 了而根本无需再进入 lock 语句块了,这样就可以解决由线程同步带来的性能问题了。
这就是所谓的“双检锁”机制(顾名思义)。
很可惜,这样的写法在很多平台和优化编译器上是错误的。
原因在于:instance = new Singleton()这行代码在不同编译器上的行为是无法预知的。一个优化编译器可以合法地如下实现instance = new Singleton():
1. instance = 给新的实体分配内存
2. 调用Singleton的构造函数来初始化instance的成员变量
现在想象一下有线程A和B在调用getInstance,线程A先进入,在执行到步骤1的时候被踢出了cpu。然后线程B进入,B看到的是instance 已经不是null了(内存已经分配), 于是它开始放心地使用instance,但这个是错误的,因为在这一时刻,instance的成员变量还都是缺省值,A还没有来得及执行步骤2来完成instance的初始化。
当然编译器也可以这样实现:
1. temp = 分配内存
2. 调用temp的构造函数
3. instance = temp
如果编译器的行为是这样的话我们似乎就没有问题了,但事实却不是那么简单,因为我们无法知道某个编译器具体是怎么做的,因为在Java的memory model里对这个问题没有定义。
双检锁对于基础类型(比如int)适用。很显然吧,因为基础类型没有调用构造函数这一步