单例模式五种写法与线程安全
单例模式五种写法与线程安全
懒汉式
是否 Lazy 初始化:是
是否多线程安全:可以实现
实现难度:易
优点:第一次调用才初始化,避免内存浪费。
缺点:必须加锁 synchronized 才能保证单例,但加锁会影响效率。
单线程:
public class LazyMan {
//线程不安全
private LazyMan(){
}
private static LazyMan instance;
public static LazyMan getInstance(){
if (instance==null){
instance = new LazyMan();
}
return instance;
}
}
多线程:
public class LazyManSafe {
//线程安全,但是锁占用资源
private LazyManSafe(){
}
private static LazyManSafe instance;
public synchronized static LazyManSafe getInstance(){
if (instance==null){
instance = new LazyManSafe();
}
return instance;
}
}
饿汉式
是否 Lazy 初始化:否
是否多线程安全:是
实现难度:易
优点:没有加锁,执行效率会提高。
缺点:类加载时就初始化,浪费内存。
public class HungryMan {
//线程安全,但是浪费资源空间
private HungryMan(){
}
//注意加上final
private static final HungryMan instance = new HungryMan();
public static HungryMan getInstance(){
return instance;
}
}
它基于 classloader 机制避免了多线程的同步问题,不过,instance 在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用 getInstance 方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化 instance 显然没有达到 lazy loading 的效果。
双重锁/双重校验锁
JDK 版本:JDK1.5 起
是否 Lazy 初始化:是
是否多线程安全:是
实现难度:较复杂
public class DoubleCheck {
private DoubleCheck(){
}
//注意加上volatile
private volatile static DoubleCheck instance;
public static DoubleCheck getInstance(){
if (instance==null){
synchronized (DoubleCheck.class){
if (instance==null){
instance = new DoubleCheck();
}
}
}
return instance;
}
}
1.双重判断的原因:
第一次:避免不必要的线程等待,相当于对多线程懒汉式的优化,提高效率
第二次:避免多次实例化,单例模式的基本检查
2.使用volatile的原因:
主要是禁止重排序,初始化一个实例(SomeType st = new SomeType())在java字节码中会有4个步骤:
1、申请内存空间
2、初始化默认值(区别于构造器方法的初始化)
3、执行构造器方法
4、连接引用和实例
这4个步骤后两个有可能会重排序,1234或1243都有可能,造成未初始化完全的对象发布。volatile可以禁止指令重排序,从而避免这个问题。
3.为什么是JDK1.5起?
(待补充,大致是因为JVM虚拟机、volatile在jdk1.5前后有改变)
4.为什么synchronized保证有序性,却还要用volatile?
关于这个问题,可以参考知乎Java synchronized 能防止指令重排序吗?
静态内部类
是否 Lazy 初始化:是
是否多线程安全:是
实现难度:一般
//静态内部类
public class Holder {
private Holder(){
}
static class InnerHolder{
private static final Holder instance = new Holder();
}
public static Holder getInstance(){
return InnerHolder.instance;
}
}
只有第一次调用getInstance方法时,虚拟机才加载 内部类 并初始化 instance ,只有一个线程可以获得对象的初始化锁,其他线程无法进行初始化,保证对象的唯一性。目前此方式是所有单例模式中最推荐的模式,但具体还是根据项目选择。
如果实例化 instance 很消耗资源,所以想让它延迟加载,另外一方面,又不希望在 Singleton 类加载时就实例化,因为不能确保 Singleton 类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化 instance 显然是不合适的。这个时候,这种方式相比第 3 种方式就显得很合理。
枚举
JDK 版本:JDK1.5 起
是否 Lazy 初始化:否
是否多线程安全:是
实现难度:易
public enum Singleton {
INSTANCE;
}
默认枚举实例的创建是线程安全的,并且在任何情况下都是单例。实际上
- 枚举类隐藏了私有的构造器。
- 枚举类的域 是相应类型的一个实例对象
那么枚举类型日常用例是这样子的:
public enum Singleton {
INSTANCE
//doSomething 该实例支持的行为
//可以省略此方法,通过Singleton.INSTANCE进行操作
public static Singleton get Instance() {
return Singleton.INSTANCE;
}
}
这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。 这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。不过,由于 JDK1.5 之后才加入 enum 特性,用这种方式写不免让人感觉生疏,在实际工作中,也很少用。 不能通过 reflection attack 来调用私有构造方法。
结论:
一般情况下,不建议使用第 1 种懒汉方式,建议使用第 2 种饿汉方式。只有在要明确实现 lazy loading 效果时,才会使用第 4 种静态内部类方式。如果涉及到反序列化创建对象时,可以尝试使用第 5 种枚举方式。
对于第3种双重锁单例模式,本人认为加了volatile后应该解决了问题,可以算的上是一个线程安全、高效、懒加载的单例模式,但是搜索网上评论后大多都没有推荐这种方式,如有读者知道原因,请在评论区指出。
上一篇: Python面向对象编程基础解析(一)