Java单例模式实现
学习枚举类的时候看到了一篇文章, 其中大致总结了几种单例模式的写法, 正好我也抽空自己整理一下
简介
什么是单例模式? 其实是一种最常使用的设计模式, 也就是确保某个类只有一个实例, 而且这个类能自行实例化并向整个系统提供这个实例.
应用在: 线程池, 缓存, 日志对象, 对话框对象等.
实现
饿汉式
1 | public class SingletonHungry { |
比较简单的一种写法
在类加载的时候就会创建对象, 但是有一个问题就是如果这个类依赖于很多资源, 那么创建必定比较耗时. 所以我们希望他能够延迟加载, 减少初始化负载, 从而就有了懒汉式单例实现
懒汉式
1 | public class SingletonLazy { |
这样写, 就具备了懒加载的特点. 并且为了能够在多线程中更好的工作, 加入了 synchronized 关键字. 优点自然是可以更好的同步, 缺点就是因为 synchronized, 效率会变低.
volatile 关键字打算再另一篇里面写, 简单描述一下就是:
第一层语义是可见性,可见性是指在一个线程中对该变量的修改会马上由工作内存(Work Memory)写回主内存(Main Memory),所以其它线程会马上读取到已修改的值,关于工作内存和主内存可简单理解为高速缓存(直接与CPU打交道)和主存(日常所说的内存条),注意工作内存是线程独享的,主存是线程共享的。volatile的第二层语义是禁止指令重排序优化,我们写的代码(特别是多线程代码),由于编译器优化,在实际执行的时候可能与我们编写的顺序不同。编译器只保证程序执行结果与源代码相同,却不保证实际指令的顺序与源代码相同,这在单线程并没什么问题,然而一旦引入多线程环境,这种乱序就可能导致严重问题。volatile关键字就可以从语义上解决这个问题,值得关注的是volatile的禁止指令重排序优化功能在Java 1.5后才得以实现,因此1.5前的版本仍然是不安全的,即使使用了volatile关键字。
为了优化这个缺点, 在单线程的情况下, 去掉 synchronized 关键字.
双重检查锁
1 | public class Singleton { |
双重检查锁进行了两次 null 检查. 因为在 getSinleton 调用的时候并没有进入同步代码, 外面的检查是并发的, 并且过滤了绝大多数的 null 检查. 当出现 null 的时候进入同步代码再进行一次检查.
这样极大提升了并发度, 也提升了性能.
但是有一个问题, 就是 volatile 关键字是 1.5 才实现的禁止指令重排, 所以可以使用静态内部类
静态内部类
1 | public class SingletonInner { |
我们把Singleton实例放到一个静态内部类中,这样可以避免了静态实例在Singleton类的加载阶段(类加载过程的其中一个阶段的,此时只创建了Class对象)就创建对象,毕竟静态变量初始化是在SingletonInner类初始化时触发的,并且由于静态内部类只会被加载一次,所以这种写法也是线程安全的
共同缺点
序列化可能会破坏单例模式. 因为每次反序列化一个序列化的对象会创建一个新的实例, 解决方案:
1
2
3
4
5
6
7
8
9
10
11public class Singleton implements java.io.Serializable {
public static Singleton INSTANCE = new Singleton();
protected Singleton() {
}
//反序列时直接返回当前INSTANCE
private Object readResolve() {
return INSTANCE;
}
}使用反射强行调用私有构造器. 解决方案: 修改构造器, 让他在创建第二个实例的时候抛异常
1
2
3
4
5
6
7
8
9
10public static Singleton INSTANCE = new Singleton();
private static volatile boolean flag = true;
private Singleton(){
if(flag){
flag = false;
}else{
throw new RuntimeException("The instance already exists !");
}
}
优化
上面的四类模式有了, 共同的缺点也能解决了, 但是代码复杂度也上去了, 更高效的方法就是枚举单例.
1 | public enum SingletonEnum { |
无比简洁!
使用枚举单例我们还不需要考虑序列化和反射的问题, 因为枚举序列化是由jvm保证的,每一个枚举类型和定义的枚举变量在JVM中都是唯一的,在枚举类型的序列化和反序列化上,Java做了特殊的规定:在序列化时Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.Enum的valueOf方法来根据名字查找枚举对象。同时,编译器是不允许任何对这种序列化机制的定制的并禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法,从而保证了枚举实例的唯一性
这里要深入了解可以查看 Enum 类的 valueOf 方法.
总结就是: 创建枚举实例只有编译器能做到
单例模式的几个重点需要我们一直注意:
- 线程安全
- 延迟加载
- 序列化与反序列化安全
- 反射安全