为了能让接下来的几篇设计模式串起来,这篇就先写单例设计模式。初学java基础课程的时候,无论是在课上还是自学看视频,老师第一次提到的设计模式就是单例模式。当时由于知识受限,听的时候也不太懂,只记住了一些概念。随着学习的深入,有时候在写代码的自然而然的就想到了该模式,就在这篇文章中仔细的总结一下。
1.为什么需要使用单例设计模式
(1)软件设计
在软件开发或使用过程中,有时候我们往往会依赖或使用一个对象,且该对象是有且仅有一个的(不然就会有逻辑或功能上的问题),如mac os中的系统偏好设置、windows的任务管理器只能打开一个、overwatch的客户端也只能打开一个等等
(2)节省资源
在功能性重复且不变的情况下,我们往往可以使用一个对象去节省资源,避免去重复的new对象,如上一篇文章中的简单计算器,Operator的子类,各种运算类功能是不会变的,再比如在web开发中用数据库连接池的时候,用单例设计模式确保只返回一个链接对象。如果上述情况每次都去new一个新对象,无疑会增大资源的开销
上述两种情况,就诞生了单例设计模式,因为我们不能保证用户在使用软件时只打开一个客户端,也不能保证程序员在编程时用到所依赖类的时候只创建一次对象。正如计划生育每家生一个不能只靠自觉,也得靠国家出台的政策。(当然上有政策下有对策,也有违规超生的。也就像反射也可以破坏单例模式,这儿咱们就不抬杠了= =)
2.代码实现
单例模式较为简单,暂且不假设一个业务逻辑了,直接上代码。
public class Singleton {
//用static关键字确保在Singleton类在内存中只有一个实例化对象
private static Singleton singleton=null;
//私有化构造函数,不允许通过new的方式创建对象,就像上面提到的计划生育政策
private Singleton(){
}
public static Singleton getSingleton(){
//判断Singleton是否已经被实例化,如果没有创建对象,如果有直接返回
if(singleton==null)
singleton = new Singleton();
return singleton;
}
}
上面就是直接提现单例模式思想的代码,不过我们再看看代码是否存在问题。如果单线程的话,一切ok,可是在多线程的情况下,Singleton类就无法确保只有一个实例化对象了。我们来分析一下错误是怎么产生的,假设现在有两个线程,ThreadA和ThreadB,在Singleton还未被实例化的时候,ThreadA执行到了if(singleton==null)语句,判断为true,准备开心的new一个对象出来,说时迟那时快,在ThreadA还未new出对象的时(new对象也需要时间的),ThreadB抢到了CPU的时间片,也执行了if(singleton==null),判断为true,于是乎,他们两个线程都执行new Singleton(),两个Singleton就产生了。
学过多线程的同学很容易就想到那个经典的银行存取款模型,加锁不就解决问题了~上修改后的代码
public class Singleton {
private static Singleton singleton = null;
private Singleton() {
}
public static Singleton getSingleton() {
synchronized (Singleton.class) {
if (singleton == null)
singleton = new Singleton();
}
return singleton;
}
}
这样线程就安全了,不过,这个锁付出的代价挺高的。当Singleton类已经在内存中被实例化以后,singleton已经不为null了,以后每次执行getSingleton方法都需要进行校验。咱们写点小demo还体现不出来,要是web服务器有大量的人并发访问,效率可是会大大降低。
继续修改代码
public class Singleton {
//使用volatile关键字保其可见性
volatile private static Singleton singleton = null;
private Singleton() {
}
public static Singleton getSingleton() {
if (singleton == null)//一次检查
synchronized (Singleton.class) {
if (singleton == null)//二次检查
singleton = new Singleton();
}
return singleton;
}
}
在经过双重检查锁定过后,我们再来分析一下是否会出问题,依旧假设有两个线程,ThreadA和ThreadB,当ThreadA执行到第一次检查判断为ture,准备进入下一行,这时候ThreadB抢到CPU时间片,也执行了第一次检查,可后面的代码是有锁的,ThreadB可占不到便宜了,只能ThreadA进入synchronized代码块,然后执行二次检查,开开心心的new了一个Singleton。然后在后面调用getSingleton方法中,无论是并发还是非并发访问,都无法通过第一次检查,也就不会执行加锁的代码块了,完全o**k。
然后我们再来谈谈为什么还要加一个volatile关键字
首先先说一个重排序(reorder)的概念,正常情况下代码有指令序列,通常我们又认为执行序列按照我们所想象的执行,然而到了汇编层次,这个指令序列可能和我们假设的不一样。比如new Singleton()语句,我们认为它有三个步骤,1.分配内存空间、2.调用构造函数、3.让singleton对象指向内存空间(执行完这一步以后,singleton才为非null)
不使用volatile会怎么样呢?
如果正确的顺序是1-2-3,结果最终执行顺序是1-3-2。如果是后者,ThreadA在3执行后,2未执行之前,ThreadB线程抢到了时间片,这个时候singleton已经不为null了(但又没有执行初始化),所以ThreadB会直接返回singleton,然后使用,当然也就会出问题。
用volatile的意义并不在于其他线程一定要去内存总读取instance,而在于它限制了CPU对内存操作的重排序(reorder),使其他线程在看到3之前2一定是执行过的。
再说明一点,上面代码实现的单例叫懒汉式单例,顾名思义,就是很懒,要在使用的时候再实例化该类。
下面介绍饿汉式单例,所谓饿汉,也就是火急火燎的,在类装载时就实例化该单例类
public class Singleton {
private static Singleton singleton = null;
//类装载时执行静态代码块,实例化Singleton
static {
instance = new Singleton();
}
private Singleton() {
}
public static Singleton getInstance() {
return singleton;
}
}
最后总结一下,在确定会使用单例模式的情况下,一些肯定会被实例化的类以及一些十分巨大的单例bean中等情况中,建议使用饿汉式,到用到的时候就省去了再实例的时间,所以速度和反应快。对于一些带有选择的类,如工厂模式中的具体产品角色,则可以使用懒汉式,只有用到这个用例的时候,再将它实例化,不会浪费。