遥想当年,在西南某大学校花下,手捧《设计模式》的时候,玩得最6的就是单例模式了。想起那几行代码就能够得满分的那种感觉啊,再伴着校花的芳香,简直...了...
说写就写
单例模式,故名思议就是实例在任何时候只有一个,有时候需要一个类在任意时刻只存在一个实例,比如你看到的太阳和我看到的太阳就是同一个,不多说,来敲代码。
public class Singleton{
private static Singleton instance = null;
private Singleton(){....}
public static Singleton getInstance(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
}
单例模式,要满足的是实例只有一个,既然只能一个,那就不能够随便实例化,这里将构造方法进行私有化,那么在外部就无法调用构造器进行实例化了,那么只能在类内部去调用构造器进行实例化,再提供一个获取实例的静态方法,这样基础的单例模式也就成了。
你以为的单例还不是单例
what?上面实现的不是单例模式那还能是什么?仔细想想,单例模式就是要控制对象只能创建一个,上面通过将构造器私有化来保证构造器被别人调用,但是想想对象会不会通过别的途径来创建呢? 对象创建的途径有四种,构造器、克隆、反序列化、反射,那我们就得保证所有途径都不会产生多余的实例。
首先构造被私有化了,问题没了,克隆需要实现对应的接口Cloneable 才能够进行,都要单例了,克隆功能也就可以排除不用了。
但是反序列化可不能随便去掉,反序列化这么好的东西总不能因为实现单例模式就给抛弃掉吧。那又如何处理呢?首先类实现了接口(Serializable)可以通过反序列化来生成一个对象,我们要保证的就是在单例模式下,反序列化生成的对象不会破坏我们的单例,这也不用我们保证,Java为我们保证了这一切。
public class Singleton implements Serializable {
private static final long serialVersionUID = 1L;
private static Singleton instance = null;
private Singleton(){....}
public static Singleton getInstance(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
private Object readResolve() {return singleton;}
}
对比第一段代码和第二段代码,在加了Serializable 接口以外我们还增加了方法readResolve(),这个方法在Serializable 接口中是这样描述的:“Classes that need to designate a replacement when an instance of it is read from the stream should implement this special method with the exact signature.” 大概意思就是说可以通过这个方法返回一个实例来替代从流中读取的实例了。这样便解决了反序列化对单例模式的破坏。
还有反射,我们知道发射可以通过使用newInstance()方法来创建一个实例,但是有人会说,newInstance也是调用的构造方法进行实例,现在构造方法为私有的,怎么能够调用呢?别忘了反射是个强大的功能,一句setAccessible(true)就可以调用你私有的构造器了。为了阻止这样的情况发生,那又该如何来写呢?实际上我们要做的就是防止构造器被多次调用,来看代码.
public class Singleton{
private static Singleton instance = null;
private static flag = false;
private Singleton(){
if(flag == false){
flag = true;
....
}else{
throw new RuntimeException("不能乱来,实例已经创建好了,请走正门")
}
}
public static Singleton getInstance(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
}
实在想不出什么场景下会把已有的instance给清空,所以就不考虑构造器内部多次调用了。
这样这个单例模式看上去就没什么问题了,每调用一次先判断是否有实例,没有就创建,下次调用就有了,直接返回,通过以上的途径来保证不会重复创建对象。等等,会不会出现两次调用实例都没有的情况呢?
线程安全的单例模式才是王道
如果两个线程同时来请求调用getInstance(),会出现什么样的情况呢?首先在程序运行过程中,是一段一段的顺序执行我们的代码,多线程情况下如果两个线程同时执行到if(instance == null),那肯定不用多说这是会出问题的。那么我们需要一个线程安全的单例类,来保证在多线程情况下是没有问题的。
public class Singleton{
private static Singleton instance = null;
private static flag = false;
private Singleton(){
if(flag == false){
flag = true;
....
}else{
throw new RuntimeException("不能乱来,实例已经创建好了,请走正门")
}
}
public static synchronized Singleton getInstance(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
}
在getInstance方法前加上synchronized就能够保证我们的单例类是安全的了,在每一个线程进行请求时,如果没有线程进入这个方法它就进入执行,有则等待,这样就没有人同时去执行if(instance == null)的判断了,但是这似乎又给我们带来了累赘,在绝大多数的时候,单例类的实例是已经创建好了的,我们只用去请求,这时候并不需要进行线程安全控制,不需要同步,用同步反而影响了多线程的运行效率,那又当如何改善呢,这也不是什么难事,Google一下你就知道有种叫double-check 的处理方式,如何处理呢,看名称就知道要进行两次检查,先看代码
public class Singleton{
private static Singleton instance = null;
private static flag = false;
private Singleton(){
if(flag == false){
flag = true;
....
}else{
throw new RuntimeException("不能乱来,实例已经创建好了,请走正门")
}
}
public static Singleton getInstance(){
if(instance == null){//A
synchronized(Singleton.class){
if(instance == null){//B
instance = new Singleton();
}
}
}
return instance;
}
}
这是何意呢,我们 来模拟一下线程执行就知道了 线程1和线程2同时调用了getInstance(),都判断出了Instance为null,线程1先进入了同步代码块,进行了锁定,开始实例化,线程2就等着,实例化完成以后,同步代码块取消锁定,线程2进入,注意这时候实例已经有了,不需要在进行创建了,所以这里要做第二次检查,这就是double-check的由来了。这样也算是相当完美的单例模式了,为啥是相当完美,因为有的前辈在使用的时候就遇到了JVM指令重排的坑了。
首先什么叫指令重排,概念什么的自行Google,大致就是说JVM为了优化执行可能会对程序指令进行重排,虽然程序指令执行顺序进行了改变,但是执行结果是必须得保持一致的,JVM如何来保证呢,就是仅对互相没有依赖关系的指令进行重排,这样就保证了执行结果一致。
这样的指令重排不会影响最终结果那为何还会是个坑呢,首先我们来看看坑在哪儿,注意Instance = new Singleton();这句代码,实际上,JVM对于这句代码是三个步骤。
1. 分配内存
2. 在分配好的内存中初始化实例
3. 将内存地址引用赋值给instance
这三个步骤,2依赖于1,3依赖与1,但是2和3直接没有相互依赖,所以在JVM中可能对2、3步骤进行重排,执行顺序变成了1,3,2,这样的重排在单线程应用中也没有什么问题。但是在多线程中就不一样了。现在我们把一个线程在获取单例的重要步骤列举一下
A. 判断Instance是否为null
B. 为null 进行实例化
1. 分配内存
2. 在分配好的内存中初始化实例
3. 将内存地址引用赋值给instance
C. 不为null 直接返回
在指令重排的情况下,线程一的执行顺序为A->B->B1->B3->B2,当需要实例化的对象比较大时,线程一执行到B2,此时Instance已经有值了,就是B2正在初始化的那片内存;此时线程二再执行就直接是A->C,因为Instance已经有值了,这样问题就出来了,现在线程二获取到的实例是没有完成初始化的实例,这可是要出大事情的。
为了解决这个坑,在Jdk1.5以后,Java对内存模型做了修改,这样在JDK1.5+,volatile这个关键字就可以来解决这个问题了,也就是说在jdk1.5之前,double-check是有毛病的。现在我们来看看volatile是怎么样解决这个问题的,先了解一下volatile;volatile关键字的学问还是有一点大,在这里先讲讲解决这个问题所用的的特性,其他的下次专门分析分析;在这里volatile的作用就是禁止JVM指令重排,由volatile声明的变量时,将禁止JVM对这个变量的相关指令进行重排,也就是不会出现前面的那种情况了,操作起来特比简单,直接代码。
public class Singleton{
private volatile static Singleton instance = null;
private static flag = false;
private Singleton(){
if(flag == false){
flag = true;
....
}else{
throw new RuntimeException("不能乱来,实例已经创建好了,请走正门")
}
}
public static Singleton getInstance(){
if(instance == null){//A
synchronized(Singleton.class){
if(instance == null){//B
instance = new Singleton();
}
}
}
return instance;
}
}
就这样在instance = new Singleton();就不会发生指令重排了,这样单例模式也就可以了。
本文结合网络上各路大神所写的文章进行分析,并通过自己的理解进行文,难免会出现相同或者我理解有错误,欢迎批评指正,谢谢。
网友评论