单例设计模式重点解析(五要点)

本文五要点:

  1.双重锁单例模式的实现。

  2.双重锁的单例模式是否真的安全以及解决方法。

  3.静态内部类单例模式的实现。

  4.枚举单例模式的实现。

  5.避免反射与反序列化破坏单例。

双重锁单例模式实现:

public class SingletonLock {
    private static SingletonLock singletonLock = null;
    private SingletonLock(){}

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

    public static void main(String[] args) {
        SingletonLock singletonLock1 = SingletonLock.getInstance();
        SingletonLock singletonLock2 = SingletonLock.getInstance();
        System.out.println(singletonLock1==singletonLock2);
    }
}

分析:思考一下,双重锁真的安全吗?其实并不安全,问题在于创建对象的时候。虚拟机在创建一个对象的过程是一下步骤:

  1.在堆空间分配内存(给对象成员分配内存)

  2.执行构造方法进行初始化

  3.将对象指向内存中分配的内存空间,即对象有了内存地址(意味着对象不为null了)

如果按上面步骤正常来说,执行到A线程执行到 singletonLock = new SingletonLock(); 那么对象就成功创建,并在下面返回。但是java编译器可能会对指令进行重排序优化操作,导致上面的步骤变成1,3,2。

此时问题来了

  1.A线程进来创建对象(执行singletonLock = new SingletonLock();),执行了1,3但2还未执行完。

  2.线程B进来执行到if(singletonLock!=null),判断对象singletonLock已经不为null了(此时对象的2步骤构造方法初始化还未执       行),就直接走到return把对象返回出去。

  3.当线程B调用方拿到对象去调用对象的属性使用时,此时对象还未初始化完毕,拿到的属性是为null的,那么就会发生空指针异       常了。

或许这里会有人觉得这3个步骤都是很快不会被其它线程影响,但在编程时可以想想当你的对象进行构造方法初始化时需要执行很多操作或者计算,这种可能就存在了。

问题解决:可以使用jdk提供volatile关键字修饰对象的定义,它的作用可以使java编译器不对指令进行重排序优化操作(它的作用还有高速缓存(工作内存)与主内存那点事,原理是通过内存屏障,感兴趣的建议去看《java并发编程的艺术》这本书,个人建议程序员必须要学习的书),这样就保证执行顺便是1,2,3就不会出现上面这个问题了。

private static volatile SingletonLock singletonLock = null;

来看看下面提供另外2种方式,现在用的比较多的单例模式实现:

静态内部类单例模式实现:

public class InnerClassSingleton {
    private InnerClassSingleton(){}

    private static class SingletonHandler{
        private static InnerClassSingleton innerClassSingleton = new InnerClassSingleton();
    }

    public static InnerClassSingleton getInstance(){
        return SingletonHandler.innerClassSingleton;
    }

    public static void main(String[] args) {
        InnerClassSingleton innerClassSingleton1 = InnerClassSingleton.getInstance();
        InnerClassSingleton innerClassSingleton2 = InnerClassSingleton.getInstance();
        System.out.println(innerClassSingleton1==innerClassSingleton2);
    }
}

此方式是一种懒汉式加载,并且能保证线程安全。原理是,类在没有被访问使用的时候虚拟机是不会去加载的,只有当真正调用时才会去加载并且初始化一些操作。(具体可以去阅读《深入理解java虚拟机》这本书中的第七章节的虚拟机类加载机制,里面有详解,在此也非常强烈建议这本书作为程序员一定要去阅读)。当我们SingletonHandler.innerClassSingleton;虚拟机才会去加载这个类SingletonHandler,并且static保证我们每次去拿的都是同一个。

反射与反序列化都能破坏单例,对于反序列化,因为反序列化时会通过readResolve方法返回一个新的对象。解决方法就是重写readResolve方法,在该方法中直接返回了我们的内部类实例。对于反射,下面讲的枚举方式可以解决。

//通过该方法返回我们内部类的实例就不会去创建新的对象
public Object readResolve(){
    return SingletonHandler.innerClassSingleton;
}

枚举单例模式实现:

public enum SingletonEnum {
    SOURCES;
    private Sources sources = null;
    private SingletonEnum(){
        sources = new Sources();
    }
    public Sources getInstance(){
        return sources;
    }
}
public class Demo {
    public static void main(String[] args) {
        Sources sources1 = SingletonEnum.SOURCES.getInstance();
        Sources sources2 = SingletonEnum.SOURCES.getInstance();
        System.out.println(sources1==sources2);
    }
}

枚举实现单例模式很简单,也是现在大家最为推荐的方式。Sources就是你想要做单例的类了。原理:枚举的特点就是在枚举里永远只有一个实例,而且也能防止反射对单例对象的破坏与操作。

你可能感兴趣的:(设计模式)