单利模式简单的来说由两部分组成:
单利模式的好坏在于线程安全、性能(效率)、懒加载这三个属性的好坏
那怎么实现单利模式呢?也就是怎么才能保证一直只有一个实例呢?
很快我们就能想到static关键字
public class HungerySingletonDemo {
private static HungerySingletonDemo instance=new HungerySingletonDemo();
private HungerySingletonDemo() {}
public static HungerySingletonDemo getInstance() {
return instance;
}
}
线程安全、性能好,但是不能实现懒加载,当HungerySingletonDemo占用变大时会非常占用内存(一开始不使用这个对象却要占用大量内存)
先要实现懒加载,就在第一次调用的时候实例它好了
public class HoonSingleton {
private static HoonSingleton instance=null;
private HoonSingleton() {}
public static HoonSingleton getInstance() {
if(instance==null) {
instance = new HoonSingleton();
}
return instance;
}
}
实现了懒加载,但是线程不安全,因为如果当多个线程同时访问到if(instance==null)
这行代码,都会执行到 if 内部,这样就会创建多个实例。线程都不安全就没有性能好坏这个说法了。
那有没有这么方法来改进呢?也就是怎么控制线程的并发访问呢?
很明显我们会想到synchronized关键字。
public static synchronized HoonSingleton getInstance()
这样的话线程就可同步访问getInstance这个方法,就不会创建这个实例了
但问题又随着而来了,synchronized效率太低了,性能不好。那怎么才能提高性能呢?这时,我们可能会想到让同步代码执行次数少点不就好了吗,那怎么才能使同步代码执行次数变少呢?请看下例DCL。
public static HoonSingleton getInstance() {
if(instance==null) {
synchronized(HoonSingleton.class) {
instance = new HoonSingleton();
}
}
return instance;
}
如上,是不是减少了同步代码的执行次数(instance==null就不会执行同步代码)
但是这样的话有出现了线程安全的问题,因为如果有多个线程都进入到instance==null
里面的 if 语句块,就会在synchronized代码块进行并发操作,无论如何最后都会并发完,结果也会创建多个实例。那如何改进呢?这就是DCL(执行两次判断语句)。
public class DCLSingleton {
private static DCLSingleton instance=null;
private DCLSingleton() {}
public static DCLSingleton getInstance() {
if(instance==null) {
synchronized(DCLSingleton.class) {
if(instance==null) {
instance = new DCLSingleton();
}
}
}
return instance;
}
}
如上,判断了两次instance==null
,这就防止了创建多个实例(synchronized内部再判断一次,如果不为空就不创建实例)。但这个有可能会存在问题,如下:
存在问题:
假设线程一执行到instance = new DCLSingleton();
这句,这里看起来是一句话,但实际上其被编译后在JVM执行的对应会变代码就发现,这句话被编译成8条汇编指令,大致做了三件事情:
1)给instance实例分配内存;
2)初始化instance的构造器;
3)将instance对象指向分配的内存空间(注意到这步时instance就非null了)
如果指令按照顺序执行倒也无妨,但JVM为了优化指令,提高程序运行效率,允许指令重排序。如此,在程序真正运行时以上指令执行顺序可能是这样的:
a)给instance实例分配内存;
b)将instance对象指向分配的内存空间;
c)初始化instance的构造器;
这时候,当线程一执行b)完毕,在执行c)之前,被切换到线程二上,这时候instance判断为非空,此时线程二直接来到return instance语句,拿走instance然后使用,接着就顺理成章地报错(对象尚未初始化)。
具体来说就是synchronized虽然保证了线程的原子性(即synchronized块中的语句要么全部执行,要么一条也不执行),但单条语句编译后形成的指令并不是一个原子操作(即可能该条语句的部分指令未得到执行,就被切换到另一个线程了)。
根据以上分析可知,解决这个问题的方法是:禁止指令重排序优化(相对的),即使用volatile变量。
private static volatile DCLSingleton instance=null;//在该属性上加volatile
声明类时不声明实例变量,而放到一个内部静态类中去实例化该类
public class HolderSingleton {
private static class Holder{
private static HolderSingleton instance=new HolderSingleton();
}
public static HolderSingleton getInstance() {
return HolderSingleton.Holder.instance;
}
}
加载HolderSingleton时,也会加载内部类Holder,但它不会初始化,当首次调用getInstance时才初始化化(就是实现了懒加载)。
Holder模式的内部类替换成enum类型的,因为枚举类型的成员时(INSTANCE)也是public static final的,且是在static{}中初始化的(就是实现了懒加载)。
public class EnumSingleton {
private EnumSingleton() {}
private enum EnumHolder{
INSTANCE;//public static final EnumHolder实例,在static块初始化
private EnumSingleton instance=null;//EnumHolder实例的私有变量
EnumHolder(){
instance = new EnumSingleton();
}
}
public static EnumSingleton getInstance() {
return EnumHolder.INSTANCE.instance;
}
}