第一篇:单例模式
单例模式是大家最为熟悉的设计模式也是大多数程序员接触的第一种设计模式,但是要真的去全面了解一波单例模式还是要点时间.
什么是单例:
确保某一个类在全局只有一个实例,最为常见的的场景就是全局变量以及全局的工具类,重复创建带来资源与性能的浪费.那么创建单例模式需要注意什么呢?
- 重写构造函数并声明为私有(private);
- 通过静态方法或者枚举返回单例对象;
- 确保全局至多只有一个对象,尤其在多线程高并发的情况下;
- 确保单例对象在反序列化的时候也不会重新创建新的对象;
从单例的实现,前人又将其分为多种类型,我们来分析一波他们的优劣:
饿汉模式:
我们知道在一个国家里面国王只有一个,大臣可以有多个,我们就拿这个来举例说明:
1 | package top.huyuxin.singleton; |
1 | package top.huyuxin.singleton; |
1 | package top.huyuxin.singleton; |
我们可以看下因为King的构造器被声明为private,我们只能通过静态方法getInstance来获取King的对象,而Minister则可以通过构造来new获得.通过内存地址的打印,我们可以判断出他们是否是同一个对象,结果显而易见:
1 | king:top.huyuxin.singleton.King@770848b9 |
懒汉模式
在分析饿汉模式之前我们先来看下懒汉模式,再来分析这是两个最为简单实现:
1 | package top.huyuxin.singleton; |
就像名字一样饿汉模式一上来就直接创建一个对象,不管你要不要,懒汉模式则是你需要才会创建对象.这样在初始化内容比较少时我们可以考虑饿汉模式,这样可以在初始化时便可得到,在后面使用方便,如果初始化内容比较多那么可以放在后面需要的时候初始化,以免初始化后面却没用到,浪费资源.
有的同学说,当两个线程同时来获取单例对象上面的都不能保证获取到的是同一个,getInstance()方法不是一个原子操作.
懒汉模式的改进:
1 | package top.huyuxin.singleton; |
将getInstance()用声明为synchronized同步操作,这样能一定程度的避免多线程同时获取单例对象的问题,但是却还不够并且每次执行getInstance()方法都是一个同步操作,这是不能忍受的.并且在高并发下这种方式也是形同虚设.我们下面会分析.
Double Check Lock(DCL)模式
1 | package top.huyuxin.singleton; |
通过两层的判空,从而使得当单例对象不为空时不用被加锁直接返回,里面那层判空则是为了确保king在为空才会new,很多人会说,这不是多此一举吗?外层不是已经判空了?这主要是 king=new King();操作不是原子操作,这一句代码在编译成汇编指令时大致分为三步:
- 给单例的实例分配内存
- 通过调用单例的构造,初始化成员字段
- 将单例对象指向分配的内存空间(这里单例对象已经不为空)
但是由于编译器允许处理器乱序执行,以及java1.5之前JVM(java内存模型)中对于二三步写入的顺序是无法保证的,所以还是有可能导致DCL失效,这是让人十分难以接受的,于是我们想到了一个关键字volatile,声明单例对象的时候,将其用volatile修饰,如 private volatile static King king;那么我们通过修改得到是相对于完善的DCL模式.
1 | package top.huyuxin.singleton; |
通过DCL模式我们能一定程度的解决资源消耗,多余同步,线程安全问题.但是通过volatile来修饰单例对象从而达到一种打补丁式的一定程度的修复DCL失效的问题,这是让人十分难受的.
静态内部类单例模式
1 | package top.huyuxin.singleton; |
通过这种方式避免了懒汉模式那种类加载就被初始化的尴尬处境,只有在调用getInstance的时候才会获取单例对象.这种方式既能保证线程安全,也使得单例的初始化进程得以推迟,初始化时序可控.但是这种就完美了吗?no..no..no..!
在工作中将对象序列化用户网络传输与持久化是十分常见的场景,我们将King实现(implements) Serializable接口,然后将其序列化后保存到文件(持久化),再通过反序列化,将文件内的对象再读取出来,看下还是不是同一个对象
1 | /** |
结果却是让人失望的:
1 | top.huyuxin.singleton.King@336bc75c |
他们不是同一个对象,这就让我想到了,把乌鸡国国王藏到井里.跳出个妖精.可怕可怕!
那能改吗?当然能,java支持自定义的序列化与反序列化,最简单的方式就是重写readResolve()方法
1 | package top.huyuxin.singleton; |
结果就是可以了,即使反序列化也是同一个国王.完美!完美??呸这和之前的用volatile修饰单例对象有什么区别?不过话说回来,静态内部类方式是推荐的单例实现方案.
枚举单例模式
我们在序列化对象的时候我们会发现在除了基础数据类型之外,String,数组,Enum,或者实现Serializable接口可以被序列化,从java的序列化部分实现代码就可以看出
1 | private void writeObject0(Object obj, boolean unshared) throws IOException { |
而我们知道Enum(枚举)这种类型比较有意思,就是他就像类一样可以有自己的字段以及方法,那么就用枚举来实现序列化不是可以防止出现反序列化new新的单例对象的问题?是的.
1 | /** |
简洁,线程安全.即使反序列化也是同一个对象,问题就是里面的字段和方法得是静态的.而且因为是单继承,Enum(枚举)类型是已经默认继承了Enum.所以不能继承其他类.日!!!!!!!!!!!!!!!!!!!!!!!