单例模式的五种实现方式和可能出现的问题

最简单的两种实现模式:饿汉式和懒汉式

饿汉式
public class Hungry {
    private Hungry() {
    }
    private final static Hungry instance = new Hungry();
    public static Hungry getInstance() {
        return instance;
    }
}

即私有化构造器,并将实例作为静态变量,在类初始化时加载完成,线程安全,但是单例在还没有使用到的时候,初始化就已经完成了,这就造成了不必要的资源浪费。

懒汉式
public class Lazy {
    private static Lazy instance;
    private Lazy(){}
    public static synchronized Lazy getInstance(){
        if(instance==null){
            instance=new Lazy();
        }
        return instance;
    }
}

线程安全 ,做到了延时加载,但是有一个很严重的问题,就是外部每次调用getinstance()方法时都要先获取synchronized锁,也就说访问效率很低。为了改进该方法,衍生出了所谓DCL( Double Check Lock )懒汉

DCL( Double Check Lock )懒汉式
public class DCLLazy {
    private DCLLazy() {
    }
    private static DCLLazy instance;

    public static DCLLazy getInstance() {
        if (instance == null) {
            synchronized (DCLLazy.class) {
                if (instance == null) {
                    instance = new DCLLazy();
                }
            }
        }
        return instance;
    }
}

这样写与上面写有一个显著的好处,只有在单例还没有实例化的时候需要获取类锁,如果单例已经实例化,之后的访问只需要不会进入if句块。保证了线程的安全性,又符合了懒加载,只有在用到的时候,才会去初始化,调用效率也比较高,但是这种写法在极端情况还是可能会有一定的问题。

Java在new 对象的过程不是原子性操作,至少会经过三个步骤:

  1. 分配内存
  2. 执行构造方法
  3. 指向地址

    步骤2和步骤3不存在数据依赖关系.而且无论重排前还是重排后程序执行的结果在单线程中并没有改变,因此这种重排优化是允许的。但是指令重排只会保证串行语义的执行一致性(单线程) 并不会关心多线程间的语义一致性

所以当一条线程访问instance不为null时,由于instance实例未必完成初始化,也就造成了线程安全问题 。

具体来说,就是由于指令重排,导致A线程执行instance = new DCLLazy();的时候,可能先执行了第三步(还没执行第二步),此时线程B又进来了,发现instance已经不为空了,直接返回了instance,并且后面使用了返回的instance,由于线程A还没有执行第二步,导致此时instance还不完整,可能会有一些意想不到的错误。

改进方法: 将instance使用volatile进行修饰;

private static volatile LazyLoad instance = null;
静态内部类(饿汉式改进)
public class Holder {
    private Holder() {
    }
    public static Holder getInstance() {
        return InnerClass.holder;
    }
    private static class InnerClass {
        private static final Holder holder = new Holder();
    }
}

这种方式是第一种饿汉式的改进版本,同样也是在类中定义static变量的对象,并且直接初始化,不过是移到了静态内部类中,内部类没有被调用时不会被加载,满足了懒加载。而且由于所有访问都是通过外部类来实现,所以内部类只会被Holder访问,保证了线程的安全性。

但是这个方法有一个很严重的问题,如果使用反射进行对象的创建,它可以无视private修饰的构造方法,可以直接在外面newInstance()。

验证代码如下:

public static void main(String[] args) {
        try {
            LazyMan lazyMan1 = LazyMan.getInstance();
            Constructor declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
            declaredConstructor.setAccessible(true);
            LazyMan lazyMan2 = declaredConstructor.newInstance();
            System.out.println(lazyMan1.hashCode());
            System.out.println(lazyMan2.hashCode());
            System.out.println(lazyMan1 == lazyMan2);
        } catch (Exception e) {
            e.printStackTrace();
       }
}

两个对象哈希值不相等,解决方法如下

public class LazyMan {
    private LazyMan() {
        synchronized (LazyMan.class) {
            if (lazyMan != null) {
                throw new RuntimeException("不要试图用反射破坏单例模式");
            }
        }
    }

    private volatile static LazyMan lazyMan;

    public static LazyMan getInstance() {
        if (lazyMan == null) {
            synchronized (LazyMan.class) {
                if (lazyMan == null) {
                    lazyMan = new LazyMan();
                }
            }
        }
        return lazyMan;
    }
}

在私有的构造函数中做一个判断,如果lazyMan不为空,说明lazyMan已经被创建过了,如果正常调用getInstance方法,是不会出现这种事情的,所以直接抛出异常

但是这种写法还是有问题:

上面我们是先正常的调用了getInstance方法,创建了LazyMan对象,所以第二次用反射创建对象,私有构造函数里面的判断起作用了,反射破坏单例模式失败。但是如果破坏者干脆不先调用getInstance方法,一上来就直接用反射创建对象,我们的判断就不生效了,如下:

public static void main(String[] args) {
        try {
            Constructor declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
            declaredConstructor.setAccessible(true);
            LazyMan lazyMan1 = declaredConstructor.newInstance();
            LazyMan lazyMan2 = declaredConstructor.newInstance();
            System.out.println(lazyMan1.hashCode());
            System.out.println(lazyMan2.hashCode());
        } catch (Exception e) {
            e.printStackTrace();
        }
}

防止这种反射破坏,在这里,我定义了一个boolean变量flag,初始值是false,私有构造函数里面做了一个判断,如果flag=false,就把flag改为true,但是如果flag等于true,就说明有问题了,因为正常的调用是不会第二次跑到私有构造方法的,所以抛出异常。

看起来很美好,但是还是不能完全防止反射破坏单例模式,因为可以利用反射修改flag的值。

public class LazyMan {
    private static boolean flag = false;
    private LazyMan() {
        synchronized (LazyMan.class) {
            if (flag == false) {
                flag = true;
            } else {
                throw new RuntimeException("不要试图用反射破坏单例模式");
            }
        }
    }
    private volatile static LazyMan lazyMan;
    public static LazyMan getInstance() {
        if (lazyMan == null) {
            synchronized (LazyMan.class) {
                if (lazyMan == null) {
                    lazyMan = new LazyMan();
                }
            }
        }
        return lazyMan;
    }
}
枚举(最推荐)
public Enum EnumSingleton {
    instance;
    public EnumSingleton getInstance(){
        return instance;
    }
}

足够简单,不需要开发自己保证线程的安全,同时又可以有效的防止反射破坏我们的单例模式 ,因为newinstance源码中会检查是否为enum对象。

 @CallerSensitive
    public T newInstance(Object ... initargs)
        throws InstantiationException, IllegalAccessException,
               IllegalArgumentException, InvocationTargetException
    {
        if (!override) {
            if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
                Class caller = Reflection.getCallerClass();
                checkAccess(caller, clazz, null, modifiers);
            }
        }
        if ((clazz.getModifiers() & Modifier.ENUM) != 0)
            throw new IllegalArgumentException("Cannot reflectively create enum objects");//看此处对enum做了判断
        ConstructorAccessor ca = constructorAccessor;   // read volatile
        if (ca == null) {
            ca = acquireConstructorAccessor();
        }
        @SuppressWarnings("unchecked")
        T inst = (T) ca.newInstance(initargs);
        return inst;
    }

你可能感兴趣的:(设计模式,java,后端,单例模式)