headfirst设计模式(6)—单例模式
这一章的课题看起来就很和蔼可亲了,比起前面绕的我不要不要的工厂模式,那感觉真是太好了,但是正是因为简单,那么问题就来了,我怎么才能把这个东西叙述清楚?怎么样才能老少咸宜呢?
如何能够在把这个东西讲清楚的同时,引入一些新的东西让这个设计模式能显得不那么普通呢?我不知道能不能做到,不过,吹x马上开始
首先,还是贴一波headfirst源码地址:
github地址:https://github.com/bethrobson/head-first-design-patterns
单例入门浅析
headfirst的原文是由一个巧克力锅炉的例子引入了经典的单例模式,具体例子不赘述,直接进入经典单例模式的贴代码环节(注意:以下所有代码为了方便区分和源代码稍有不同)
public class classicsingleton { private static classicsingleton unique_instance; private classicsingleton() { } public static classicsingleton getinstance() { if (unique_instance == null) { unique_instance = new classicsingleton(); } return unique_instance; } public string getdescription() { return "i'm a classic classicsingleton!"; } }
首先来说,为什么它是一个非常适合入门的单例?
因为它确实很简单,这段代码用一段话来描述就是:保证需要使用的对象在内存中的唯一性
个人觉得,这个就是单例的核心思想,后面各种单例模式都是为了这个操作在做各种各样的努力,只是实现的优劣之分而已。
再来聊聊为什么不推荐使用?
因为这个写法是一个线程不安全的,多线程下会有问题。所以这里用词是不推荐,万一你的用法就是单线程呢?这样写也没问题,so,只要能符合业务,通俗易懂,那么它就没有问题。老生常谈的问题,没有最好的,只有最适合的,简单高效才是硬道理,个人认为设计模式也只是为了辅助达成这个目标吧
记得我刚实习的时候,看网上的单例要用volatile, 要用synchronized,看得我那真是一愣一愣的,当时我synchronized,知道是干啥的,但是用得上,volatile,还要靠百度才知道是个啥。本着追求牛x技术的心情,直接就拷了一个个人感觉最牛x的volatile双重判断的写法上去。其实当时并不明白其中原理,只是觉得很牛x而已,幸好没出什么生产环境的bug,也是万幸。
不知道原理的代码是很恐怖的,因为这个东西有些可能是没有通过时间,业务检验的,即便是通过了测试,只要生产环境出问题,那就是毁灭性的(别问我怎么知道的)。技术是为了支撑业务,而不是为了炫技,写出简单,易用,高效的代码才是技术应该做的事情(当然并非是不鼓励尝试新技术,只是需要控制在一个可控的范围内)
打个比方,我写了一个处理权限的功能,其他人需要接进来的时候,我告诉他们,你们要去配一个xml文件,properties文件里面加两个参数,最后使用的时候,要调用xx方法,他们第一感觉就是,你写的这个太难用了。如果你告诉他们,把这个包引进去加个注解就可以了,其他的都不用管呢?是不是感觉完全不一样?
我擦,扯远了,总的来说就是它适合用于学习,不适合用于商业,那么有没有适合用于商业的呢?当然有,网上文章一大堆
public class threadsafesingleton { private static threadsafesingleton unique_instance; private threadsafesingleton() { } public static synchronized threadsafesingleton getinstance() { if (unique_instance == null) { unique_instance = new threadsafesingleton(); } return unique_instance; } public string getdescription() { return "i'm a thread safe singleton!"; } }
效率很低,但是能用,依然是不推荐的类型,有的朋友可能要问了,那不推荐你写出来干啥?
它还是有优势的,它理解起来真的很简单,同时编程也不复杂,这个不就是我们一直追寻的东西吗?如果一个问题没有更好的解决方案,那么理解简单,编程简单的方案也不失为一个方案吧?至少能看懂啊
public class staticallyinitializedsingleton { private static staticallyinitializedsingleton unique_instance = new staticallyinitializedsingleton(); private staticallyinitializedsingleton() {} public static staticallyinitializedsingleton getinstance() { return unique_instance; } public string getdescription() { return "i'm a statically initialized classicsingleton!"; } }
这种算是最推荐的写法了,首先它写法简单,其次线程安全问题可以通过jvm去保证(每个类的静态变量在jvm中只会被初始化一次),最后,获取单例类的操作没有加锁处理,性能很高
这个东西就要看这个单例对于项目是不是强依赖了,仁者见仁智者见智了,此处就不赘述,不然又要跑偏了
public class doublechecksingleton { private volatile static doublechecksingleton unique_instance; private doublechecksingleton() {} public static doublechecksingleton getinstance() { if (unique_instance == null) { synchronized (doublechecksingleton.class) { if (unique_instance == null) { unique_instance = new doublechecksingleton(); } } } return unique_instance; } public string getdescription() { return "i'm a double check singleton!"; } }
这个就是个人觉得一看就是很牛x的那种写法,当然,它的各方面也都是相当优秀的,线程安全,容错,性能都不错
但是,如果对它的理解比较模糊的话,那么写的时候是很容易写掉一些重点的东西的,举两个点:
2,synchronized里面那个判空容易写掉吧?
对于2这点,我是深有体会的,如果a,b线程同时调用getinstance()方法,假设unique_instance还没有初始化,同时a线程先进入synchronized块,没有if null判断,那么它就new了一个对象出来吧,当a执行完了以后释放了锁,这个时候b就会进入,没有if null判断,b也new一个对象出来,这就有问题了啊。
对于1这点,如果不理解volatile,是很容易写掉的,毕竟,如果能很好的理解第2点的话,就会感觉,不加这啥volatile感觉也没啥问题啊,双重锁稳的不行啊。但是不加还真有可能有问题
先说说volatile一般的作用:禁止指令重排序,内存可见性
这里的作用是禁止指令重排序,在创建对象并访问的过程中,可以分为4个步骤:
memory = allocate(); //1:分配对象的内存空间 ctorinstance(memory); //2:初始化对象 instance = memory; //3:设置 instance 指向刚分配的内存地址 instance.invoke() //4:初次访问对象
只要保证在访问对象之前完成1,2,3,对于java语言规范来说都是允许的,所以这里2和3是可以重排序的,但是多线程的情况下,假如a线程正在进行对象初始化,b线程可能会在第一个if null判断的时候拿到一个不为空,但是还没有初始化完成的对象(2,3被重排序),然后就会出现一些未知的错误
如果使用volatile的话,那么2,3就不会重排序,即使有其他线程拿到对象,也就说明,肯定是已经执行了2,3两步,不然的话if null判断肯定是空
这里是参考了一位大佬的文章:
前面大量的篇幅用来说明了怎么在多线程的情况下保证单例模式,这里讲讲怎么从语法层面上来保证。
首先,语法层面上保证单例的一般操作是这样的:
private staticallyinitializedsingleton() { }
相当于告诉所有人,这个类只能我自己初始化,你们别搞事情哈,但是它并不是牢不可破的
public class singletonclient { public static void main(string[] args) throws nosuchmethodexception, illegalaccessexception, invocationtargetexception, instantiationexception, interruptedexception { constructor cons = staticallyinitializedsingleton.class.getdeclaredconstructor(null); cons.setaccessible(true); staticallyinitializedsingleton singleton = (staticallyinitializedsingleton)cons.newinstance(null); system.out.println(singleton.getdescription()); } }
这种操作怎么防御呢?可以去控制它的类只能初始化一次,具体的操作可以这样:
public class staticallyinitializedsingleton implements serializable{ private static staticallyinitializedsingleton unique_instance = new staticallyinitializedsingleton(); private static boolean initialized; private staticallyinitializedsingleton() { if(initialized){ throw new runtimeexception(); } initialized = true; } public static staticallyinitializedsingleton getinstance() { return unique_instance; } public string getdescription() { return "i'm a statically initialized classicsingleton!"; } }
第二种,使用java的序列化与反序列化
当然,首先要实现serializable接口,不然也没有这个问题
public class serializetest { public static void main(string [] args) throws ioexception, classnotfoundexception { staticallyinitializedsingleton s = staticallyinitializedsingleton.getinstance(); objectoutputstream objectoutputstream = new objectoutputstream(new fileoutputstream(new file("e:/test.txt"))); objectoutputstream.writeobject(s); objectinputstream ois = new objectinputstream(new fileinputstream("e:/test.txt")); staticallyinitializedsingleton s1 = (staticallyinitializedsingleton)ois.readobject(); system.out.println(s); system.out.println(s1); } }
执行结果就会发现s,s1不是一致的,解决这种情况,需要在单例类中加入readresolve()方法来控制,jvm在反序列化的时候,使用我们自定义的类作为结果
public class staticallyinitializedsingleton implements serializable { private static staticallyinitializedsingleton unique_instance = new staticallyinitializedsingleton(); private static boolean initialized; private staticallyinitializedsingleton() { if(initialized){ throw new runtimeexception(); } initialized = true; } public static staticallyinitializedsingleton getinstance() { return unique_instance; } public string getdescription() { return "i'm a statically initialized classicsingleton!"; } //解决序列化与反序列化问题 private object readresolve(){ return uniqueinstance; } }
花了这么多功夫,终于解决了,那么实际开发中,有没有必要这样去处理这些问题呢?
这个我给不出答案,可以给出的参考有两个:1,编程成本;2,程序边界
通俗点讲就是:1,改这个要花多久时间(编写,测试,上线);2,不按规范来调用,是不是程序需要关注地方
当然,有没有一劳永逸的方法来解决各种各样问题,并且编写简单,容易理解呢?
请看:
public enum singleton { instance; public string getdescription() { return "枚举单例,就是这么简单"; } }
没错,枚举类,是不是感觉比上面所有都简单;)
上一篇: 我都憋不住气了
下一篇: 工厂模式讲解, 引入Spring IOC