是否 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后应该解决了问题,可以算的上是一个线程安全、高效、懒加载的单例模式,但是搜索网上评论后大多都没有推荐这种方式,如有读者知道原因,请在评论区指出。
