java单例模式中,留意几个关键点:构造方法私有化,单例对象变量需要static修饰,借助公有方法获取单例对象。另外也需要考虑线程是否安全,是否为延迟加载。
饿汉模式
public class Hungry{
private final static Hungry instance=new Hungry();
private Hungry(){}
public static Hungry getInstance(){
return instance;
}
}
饿汉模式可以保证线程安全,但是却不是延迟加载。这里扩展说一下延迟加载:我们知道类加载包括加载,链接和初始化阶段,而链接又分为:验证,准备和解析,对于静态变量,在准备阶段会赋值默认值,这里就是为instance赋予null,而在初始化阶段会赋值用户设定的值,这里就是new Hungry()。也就是说,一旦这个Hungry类被加载了(使用new关键字获取,反射forName()等都会触发类加载),那么内存就会分配一个空间存放new Hungry()对象,而更多地时候,我们并不是为了使用这个Hungry对象才加载这个类的,这就会占用额外的内存空间。所以引入延迟加载概念就是将初始化instance放到需要用到这个对象的时候。下面介绍的懒汉模式就可以实现延迟加载。
懒汉模式
public class Lazy{
private static Lazy instance=null;
private Lazy(){}
public static Lazy getInstance(){
if(instance==null)instance=new Lazy();
return instance;
}
}
懒汉模式解决了延迟加载,但是却没有考虑到线程安全,多个线程可能会同时进入if语句,导致各个线程获得的对象不同。下面列出两个改进方法:
public class Lazy{
private static Lazy instance=null;
private Lazy(){}
//使用synchronized关键字修饰
public static synchronized Lazy getInstance(){
if(instance==null)instance=new Lazy();
return instance;
}
}
线程是安全了,但是也要保证效率,synchronized是互斥锁,而且还需要操作系统切换线程上下文,得不偿失。进一步优化:
/*
* 假设线程A、B
* Step1:A进入同步代码块
* Step2:B进入getInstance(),发现instance==null,进入if,锁被占用,阻塞
* Step3:A执行完同步代码块,释放锁
* Step4:B获得锁,发现instance已实例化,退出
*
* 如果不进行第二次判定,B线程进入后会再次执行instance=new DoubleLock() 导致获取不同的实例
*/
public class Lazy {
private static Lazy instance = null;
private Lazy() {}
public static Lazy getInstance() {
if (instance == null) {
synchronized (Lazy.class) {
if (instance == null)
instance = new Lazy();
}
}
return instance;
}
}
对于DCL(双重检查锁),其实存在着不少的争执,在《Java并发编程实战》中文版一书中说道:
DCL的真正问题在于:当在没有同步的情况下读取一个共享对象时,可能发生的最糟糕事情只是看到一个失效值(在这种情况下是一个空值),此时DCL方法将通过在持有锁的情况下再次尝试来避免这种风险,然而,实际情况远比这种情况糟糕——线程可能看到引用的当前值,但对象的状态值却是失效的,这以为这线程可以看到对象处于无效或错误的状态。
由于bo主技术有限,如果想要深入理解,可以参考这篇博文。
延迟初始化占位类模式(瞬间高大上)
说白了,就是将单例对象放入一个私有静态内部类中,上代码:
public class Outer {
private Outer(){}
private static class Inner{
private static Outer instance=new Outer();
}
public static Outer getInstance(){
return Inner.instance;
}
public static void other(){}
}
当我们调用Outer类其他方法时,并不会加载Inner类,起到延迟初始化的作用,当且仅当我们调用getInstance()时,需要返回Inner类的静态变量,这时候类加载器才会加载我们的Inner类。这种方法是线程安全的。
枚举类型
这种方法最为简洁,同时也保证了延迟加载和线程安全。
enum EnumModel {
INSTANCE;
private EnumModel(){}
}
需要调用时,直接EnumModel.INSTANCE
方便快捷。