单例模式有几种写法?需要注意什么?
饱汉模式
饱汉,即已经吃饱了,不着急再吃,饿的时候再吃。
大概意思就是,他不先不初始化单例,等下一次使用的时候再初始化,即 “懒加载”。
饱汉的核心就是懒加载,好处是启动速度快,节省内存资源,到实例被获取时才需要被实例化。
1、基础的饱汉模式写法
//饱汉
public class FullSingle {
private static MySingle mySingle;
public static MySingle getMySingle(){
if (null == mySingle){
mySingle = new MySingle();
}
return mySingle;
}
}
基础的写法存在线程安全的问题,大概就是多个并发执行该方法,创建的对象可能都是不同,也就是上面的if条件存在竞争。
2、Synchronized 关键字解决线程安全问题
最简单有效的方法使用 synchronized 关键字修饰 getMySingle 方法,这样能达到绝对的线程安全。
//饱汉
public class FullSingle {
private static MySingle mySingle;
public synchronized static MySingle getMySingle(){
if (null == mySingle){
mySingle = new MySingle();
}
return mySingle;
}
}
这种写法虽然对线程安全有了解决,坏处就是并发的性能极差,可以认为完全退化到了串行。
对象只需创建一次,而创建完之后,后面的每次获取都避免不开 synchronized 锁,从而把这个方法变成串行的操作了。
3、DCL 解决每次都要上锁的问题 1.0 version
DCL (double check lock) 双层检查机制
//饱汉
public class FullSingle {
private static MySingle mySingle;
public static MySingle getMySingle(){
if (null == mySingle){
synchronized (FullSingle.class){
if (null == mySingle){
mySingle = new MySingle();
}
}
}
return mySingle;
}
}
这种写法的核心是 DCL ,在 synchronized 内层又加了一个 if 判断。就是再创建完单例对象之后,可以避免再次走 synchronized 锁。
该写法存在指令重排的问题,导致对象只初始化了一半。解决方法就是 把 单例对象 修饰为 volatile ,就能禁止指令重排,被称为 DCL v2.0写法
private static volatile MySingle mySingle ;
饿汉模式
饿汉很饿,只想着尽早吃到,所以他就在最早的时机,即类加载初始化单例,后面获取的时候直接返回即可。
饿汉模式写法
1.1 基础饿汉写法
//饿汉
public class HungrySingle {
private static MySingle mySingle = new MySingle();
public static MySingle getMySingle(){
return mySingle;
}
}
饿汉的缺点就是没有懒加载,会比饱汉式浪费一些资源,启动慢一些,优点就是不会存在线程安全的问题。
单线程环境下,俄汉与饱汉在性能上没什么差别;但多线程环境下,由于饱汉需要加锁,饿汉的性能反而更优。
1.2 优化版饿汉写法 Holder 模式
我们即希望饿汉模式中的静态变量的方便和线程的安全,又希望通过懒加载避免浪费资源。Holder 模式满足了这两个优点,核心仍是静态变量,即方便又保证了线程安全,通过静态内部类持有真正的实例,间接的使用了懒加载。
//饿汉
//Holder模式
public class HungrySingle {
private static class HungrySingleHolder{
private static final MySingle mySingle = new MySingle();
private HungrySingleHolder(){
}
}
private HungrySingle() {
}
public static MySingle getMySingle(){
return HungrySingleHolder.mySingle;
}
}
相对于基础的饿汉模式写法,Holder 模型增加了静态内部类,性能效果和饱汉的两层校验的写法相当(略优)。
1.3 枚举模式写法
用枚举模式实现单例比较方便,但不存在可读性。
基础枚举实现单例
//将枚举的静态变量作为单例的实例
public enum SingleEnum {
SINGLE;
}
通过反编译打开枚举,就看到了枚举类型的本质,简化如下:
//枚举
public class SingleEnum extends Enum<SingleEnum> {
...
public static final SingleEnum SINGLE = new SingleEnum;
...
}
总结
上面考虑的场景,都忽略了反射和序列化的情况。通过反射和序列化能够访问到私有构造器,这样就能破坏单例对象。只有枚举模式能天然防范这一问题。