前段时间参加了几场面试,正式面试之前都需要你先做一份笔试题,我发现有三家公司的笔试题都出现了同一个题目:写出一个你认为最优的单例实现方式?
单例模式,23种设计模式中很常见的一种,意思就是一个类只能有一个对象实例。此题一出,脑子里是不是立马回想起当年课堂上讲的两种方式“懒汉式”和“饿汉式”,是不是心中暗喜面试有戏了,但只有这两种吗?这两种是最优的吗?如果你真的只知道这两种,只能送你一句话:城市套路深,还是回农村。
下面就介绍下几种单例实现方式:
1、懒汉式(非线程安全)
public class SingletonTest {
private static SingletonTest instance;
//注意构造方法的访问控制符是private
private SingletonTest(){}
public static SingletonTest getInstance(){
if(instance == null){
instance = new SingletonTest();
}
return instance;
}
}
这种懒加载方式缺点很明显,多个线程执行时,就出问题了,可以简单测试下。
public class SingletonMain {
public static void main(String[] args) {
Test test = new Test();
Thread thread1 = new Thread(test, "线程1");
Thread thread2 = new Thread(test, "线程2");
Thread thread3 = new Thread(test, "线程3");
thread1.start();
thread2.start();
thread3.start();
}
public static class Test implements Runnable{
@Override
public void run() {
SingletonTest instance = SingletonTest.getInstance();
System.out.println(instance.hashCode());
}
}
}
执行结果:
可以看出,当启动三个线程后,所获取的对象实例有三个不同的哈希码(不一定每次都是三个不一样的,如果是同一个对象实例,哈希码必定一样),获取了三个不同的对象,所以这种懒加载方式已经失去了单例的意义。
2、懒汉式(线程安全)
与第1种相比,区别只是在getInstance()前加了synchronized,锁定对象是整个类。
public class SingletonTest {
private static SingletonTest instance;
//注意构造方法的访问控制符是private
private SingletonTest(){}
public static synchronized SingletonTest getInstance(){
if(instance == null){
instance = new SingletonTest();
}
return instance;
}
}
继续执行上面的小测试,可以看到获取的哈希码都相同(还可以再多创建几个线程),可以看出此方法在多线程下可以正常工作,只是效率较低,99%的情况下不用同步。
3、饿汉式
public class SingletonTest {
private static SingletonTest instance = new SingletonTest();
//注意构造方法的访问控制符是private
private SingletonTest(){}
public static SingletonTest getInstance(){
return instance;
}
}
这种方式在多线程情况下亦能正常工作,调用对象的时候不用创建,直接使用已经创建好的对象,节省了时间,但是却占用了空间,因instance在类装载时就实例化。
4、静态内部类(推荐)
public class SingletonTest {
private static class SingletonHolder{
private static SingletonTest instance = new SingletonTest();
}
//注意构造方法的访问控制符是private
private SingletonTest(){}
public static SingletonTest getInstance(){
return SingletonHolder.instance;
}
}
相比第2、3种方式,这种方式不仅是线程安全的,而且实现了延迟初始化。SingletonTest 被装载了,instance不一定实例化,因为SingletonHolder类没有被主动使用,只有显示通过调用getInstance方法时,才会显示装载SingletonHolder类,从而实例化instance。
5、双重检查加锁(DCL)
针对第2种懒汉式(线程安全)的实现方式的缺点,如果getInstance()方法被多个线程频繁调用,势必导致性能的下降。因此,出现了DCL这种方式,通过两次检查锁定来降低同步的开销。
public class SingletonTest { //1
private static SingletonTest instance; //2
//注意构造方法的访问控制符是private //3
private SingletonTest(){} //4
public static SingletonTest getInstance(){ //5
if(instance == null){ //6
synchronized (SingletonTest.class){ //7
if(instance == null){ //8
instance = new SingletonTest(); //9
}
}
}
return instance;
}
}
创建一个对象正常的套路应该是:①分配对象的内存空间;②初始化对象;③设置instance指向内存空间。这种方式看似没有问题,但如果在多线程环境下发生了重排序(重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种方式,但必须保证排序后程序的执行结果不变,也就是as-if-serial语义),比如说执行到第6行,发现instance不为null,但是instance可能还没有完成初始化,就会访问一个未初始化的对象,就有问题了。
如果发生了重排序,在单线程和多线程下的时序图如下(波浪线上为单线程,波浪线下为多线程):
那如果禁止第2步和第3步重排序不就解决了吗?只需要把instance声明为volatile即可,实现线程安全的延迟初始化。(关于volatile特性,后续介绍)
public class SingletonTest {
private volatile static SingletonTest instance;
//注意构造方法的访问控制符是private
private SingletonTest(){}
public static SingletonTest getInstance(){
if(instance == null){
synchronized (SingletonTest.class){
if(instance == null){
instance = new SingletonTest();
}
}
}
return instance;
}
}
总结
本篇主要介绍了实现单例的几种方式,只有第1种方式是线程非安全的,其余的几种方式中,第2种效率低一点,第3种占用空间多一点,第5种主要是弥补第2种的缺点,比较推荐使用第4种方式,线程安全,并且实现了延迟初始化。
略陈固陋,如有不当之处,欢迎各位看官批评指正!