懒汉式,跟饿汉式相反,类加载的时候并不会被实例化,而是在第一次被调用的时候被实例化。
懒汉式单例奥义
- 构造器私有!!!
- 暴露getInstance()方法!!!
- 在第一次被调用的时候进行初始化
写法1(存在线程安全问题喔)
public class LazySimpleSingleton {
private static LazySimpleSingleton lazy = null;
/**
* 构造器私有!
*/
private LazySimpleSingleton(){}
/**
* 对外暴露的实例化接口
* @return
*/
public static LazySimpleSingleton getInstance(){
if(lazy == null){
lazy = new LazySimpleSingleton();
}
return lazy;
}
}
自己多线程跑一下,打印一下就知道不是一个对象了。
改进1(synchronized搞他)
public synchronized static LazySimpleSingleton getInstance(){
if(lazy == null){
lazy = new LazySimpleSingleton();
}
return lazy;
}
再多线程跑一下,好像解决了一样,其实并不是解决的很好,在更多线程的情况下,会产生大量阻塞,性能会下降。
改进2(双重检查锁)
/**
* 双重检查锁
* @return
*/
public synchronized static LazySimpleSingleton getInstance3(){
// 第一重检查
if(lazy == null){
// 锁class
synchronized (LazySimpleSingleton.class){
// 第二重检查
if(lazy == null){
lazy = new LazySimpleSingleton();
}
}
}
return lazy;
}
把锁的范围变小了,只会在多个线程抢着去new才会产生点点的阻塞,所以只要锁的内容不多,调用者感知不强。
- 第一重检查好说,就是看是不是为null,需不需要进行new
- 第二重则是因为可能很多个线程同时通过了第一重检查,想象一下去掉第二重会发生什么事情(只要经过第一重,都会new一次),所以需要第二重检查。
隐患
看上去该改进2又单例,性能也不差,然而还隐藏着一个问题:指令重排序
正常new的时候干了什么?
- 分配内存空间
- 初始化对象
- 将对象指向刚分配的内存空间
然而有的编译器为了提高性能,会将2和3对调: - 分配内存空间
- 将对象指向刚分配的内存空间
- 初始化对象
这样就会产生一个巨大的问题,有可能在第一次new的时候,只new到第二步指向内存,还没执行真正的初始化,这时候其他线程会经过第一重检查直接到return,然后拿到还没初始化的对象进行调用。
改进3 (双重检查锁——volatile防止指令重排序)
private volatile static LazySimpleSingleton lazy = null;
对的,就改一句,volatile可以防止指令重排序,所有的写操作都将发生在读操作之前,这是完整的懒汉式双重检查锁单例;
改进4 (静态内部类)
虽然说双重检查锁性能已经不差了,但是能不能再提高性能一点呢?从类初始化的角度考虑:
这里主要是用到了一点:内部类要在方法调用之前被初始化,意味着它能巧妙避开线程安全问题,而且代码简单。
public class LazyInnerClassSingleton {
/**
* 构造器私有
*/
private LazyInnerClassSingleton() {
}
public static LazyInnerClassSingleton getInstance() {
return LazyHolder.LAZY;
}
/**
* 默认不加载该内部类
*/
private static class LazyHolder {
private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
}
}
改进5(不法分子用反射破坏单例)
我们知道反射可以破解private关键字,所以能够通过反射调用构造方法,具体如下:
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Class<LazyInnerClassSingleton> clazz = LazyInnerClassSingleton.class;
Constructor<LazyInnerClassSingleton> c = clazz.getDeclaredConstructor(null);
c.setAccessible(true);
LazyInnerClassSingleton lazy1 = c.newInstance();
LazyInnerClassSingleton lazy2 = LazyInnerClassSingleton.getInstance();
System.out.println(lazy1 == lazy2);//false
}
那解决调用构造器来搞事情的反射也很简单,只要在构造方法加个判断就行:
private LazyInnerClassSingleton() {
if (LazyHolder.LAZY!=null){
throw new RuntimeException("不准搞事");
}
}
重新执行上面的测试代码,可以发现反射搞不了事了。
不算改进(序列化破坏单例)
有时候(很少,我几乎没见过,当做没有)会把对象序列化写到磁盘,到时候拿出来用,这样做如果是单例对象,就破坏单例了。
太长不写...
注册式单例(两种:枚举、容器)
枚举单例模式
public enum SingletonEnum {
/**
* 单例
*/
INSTANCE;
private Object data;
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public static SingletonEnum getInstance() {
return INSTANCE;
}
}
进行测试:
- 调用getInstance()方法,把单例对象塞到data里面
- 序列化枚举对象
- 反序列化枚举对象
- 比较是否同一个对象
结果是同一个对象
为何?
反编译发现INSTANCE被静态代码块初始化了,是一种饿汉式