java 设计模式-单例模式4种写法
单例模式通常有四种写法:
饿汉写法
懒汉写法
内部类写法
枚举写法
那么这些写法具体有什么区别呢,给大家分享下我自己的理解
1.饿汉模式:
ok ,为什么恶汉模式这样写呢,其实他是通过jvm类加载机制来保证在类加载的时候只加载一次这个实例 的 ,不需要程序员去保证是否单例。这里 的构造方法必须是私有的,但是这种单例模式也有可能通过反射去调用构造方法,所以如果要保证绝对的单例,还需要加上一个判断。类似这样
其实java.lang下面的runtime就是饿汉模式写法:
2.懒汉模式:
懒汉模式有几点需要注意:1.需要构造方法私有 2.需要双重校验 3.加锁 4.防止cpu或者JIT指令重排序
懒汉一开始其实只需要这样写 保证需要的时候加载就ok
但是当多线程并行执行的时候就存在问题如下:
当两个线程同时走到这个place 的时候就会产生多例。ok 这样写确实能保证单例了,但是如果将synchronized加载方法上又会产生低效率的代码 ,我们只需要在need 产生对象的时候去new 对象,所以改造后如下:
那么此时是不是绝对单例呢,并不是,当两个线程同时并行执行,就会产生两个线程都获取锁成功,两个线程都获取到了锁资源,都从阻塞状态转为待运行状态,如下
所以还需要加上一层校验,如下:
最后还需要注意一点:cpu或者JIT导致的指令重排序,简单演示一下指令重排:比如:
当我们做一个new 对象的时候jvm 指令会做那些事情呢。我们用javap反汇编看下具体的过程 如下:
我们可以看到反汇编后的指令。第一步 是new 也就是分配空间 第二步是复制操作数栈 第三步是初始化 第四步是赋值
那么在多核cpu下并行执行两个线程就会出现指令重排的情况 比如:分配空间->初始化->赋值 指令重排后可能变成
分配空间->赋值->初始化。此时
当A线程来的时候,B线程可能还没进行初始化就拿到这个线程,就会有问题,所以我们需要加上关键字volatile来保证防止指令重排,如下
ok 这就算是一个完整的懒汉了。但其实这还有反射攻击问题。
3.静态内部类
这个就比较简单了
个人理解是通过jvm 类加载机制来保证的单例,和饿汉很像。如果要保证完全单例,还需要和上面的饿汉一样加上防止反射攻击的判断
4.枚举类
枚举类是比较标准的写法,虽然枚举的语法有点反人类,但还是好用的
枚举类其实即能防止反射攻击,也能防止因为序列化和反序列化导致的产生多例问题
这里的序列化和反序列化指的是下面这个场景:
比如我把通过getInstance取出来的对象序列化的磁盘上,在从从磁盘上取出来的时候你就会发现并不是一个对象,那么这个时候也就不是单例了,怎么防止这个问题呢,打开serializable这个类我们可以看到
他已经帮我们想好办法了,
如果不是采取枚举写法,你需要在你的类重写serializable 的一个方法readResolve()就可以保证被序列化后的类对象是一个了。
但是如果你采用枚举写法就不会用这个问题,因为枚举已经帮你实现了。
总结一下:如果你想偷懒用饿汉就可以了,但是需要加上防止反射的判断以及重写序列化的方法才能完全保证安全。如果想要完全高级且安全建议枚举写法。