• 【极客思考】设计模式:你确定你真的理解了单例模式吗?


    摘要:单例模式是创建类型中常用的一种设计模式。该模式下的类有且仅有一个实例。

    什么是单例模式?

    说到单例模式,其实大家应该都不陌生,因为真的太常用了,应该所有开发者接触设计模式的第一个模式。那我这里一句话简单说下为何使用单例:如果你希望你的某个类只需要有一个实例对象,并且全局共享,那么你就使用单例。
    我喜欢的单例模式实现
     
    单例模式是创建类型中常用的一种设计模式。该模式下的类有且仅有一个实例。单例模式常见的实现有懒汉式、饿汉式这两种方式,但是在这里,我不想讨论这两种方式,因为常见所以没有讨论和需要思考的价值。
    让我们来看看以下的几种方式的一些实现机制:

    一、双重校验锁(DCL)

    上代码:
    开发、单例模式
    DCL双重加锁的方式保证每次调用getSingleton方法的时候都是同步的。其实加锁大家都能理解,就是解决多线程同步的问题。但其实这里有个重点,就是这行代码:
    private volatile static Singleton singleton
    为什么要用volatile去修饰呢,这边从两个方面去说明:
     
    1.如果不用volatile修饰会怎么样?
     
    这看起来似乎也是行的通的,但是了解过编译器和程序指令的话就会知道那是不可靠的,具体原因如下:
    1. 编译器优化了程序指令,以加快cpu处理速度。
    2. 多核cpu会动态调整表指令顺序,以加快并行运算能力。
    简单理解,那就是现在都0202年了,一台计算机cpu和内核都是好几个出现的,不在是那个单核的老时代了,所以java文件编译成字节码指令之后,你的编码逻辑确实是串行的,计算机也会根据范式把你编程的逻辑结果给你执行返回,但是具体到cpu去执行指令的时候,为了体现多核的优势,会对一些指令做并行处理,以加快程序运行速度。
     
    我想好奇的你还是想知道,如果不加volatile的话,会在什么时候出现问题,那我给你说说问题出现的顺序:
    1. 线程A,调用方法获取实例,发现对象未实例化,准备开始实例化。
    2. 由于编译器优化了程序的指令,允许对象在构造函数未调用完成前,将共享变量的引用指向部分构造的对象,虽然对象未完全实例化,但是已经不为null了。
    3. 线程B进入也要调用方法获取实例,发现部分构造的对象已经不为null,则直接返回了该对象。
    至于线程B返回之后会发生什么,可想而知,没实例化完,那么就会导致调用部分的方法的时候,就会有空指针的异常,所以就是我上面说的,不可靠。
     
     
    2.volatile作用是啥?
     
    为了解决这个问题,JDK1.6之后的版本提供了该关键字, 其实就是为了让其修饰的变量你能够在线程间可见,而所谓的可见,那就是大家都从主存中获取,至于主存等概念在这里就不展开说明了。
    可以这么理解:在线程B读取volatile变量后,线程A在写这个volatile之前,所有可见的共享变量的值都将立即变得对线程B可见。
    对应上面的问题解决也就是:线程A在未初始化完,singleton变量那就是null,线程B读到的也就是null,那么当线程B再进去想要加锁实例化的时候,发现线程A获取了锁正在实例化,那就阻塞了起来,直到A实例化完释放锁,但是因为实例化完之后B立马又知道该变量不为null了所以在第二个判断的时候,就不用进去new了,返回了。

    二、静态内部类

    上代码:
    静态内部类是一个我比较喜欢的实现方式,当然很明显代码少,逻辑较为简单。这种方式主要是利用了classloader机制来保证初始化singleton的时候只有一个线程,避免了需要再去保证线程同步的问题。同时我们把这种方式实例化有lazy loading的效果,其实主要是因为静态内部类Holder类并不会在Singleton类被装载的时候就被初始化了,只有当Holder类被主动使用,也就是调用了getSingleton方法之后,才会显示的装载Holder类,从而实例化singleton对象。如果singleton对象是一个消耗资源占用比较大的内存的对象的时候,如果你希望延迟加载的话,那么这种方式是个不错的选择。
     
    但是其实静态内部类的方式实际上并没有想象中的那么完美,因为它无法阻挡反射和反序列攻击,你可以利用前面两种方式再去构造新的Singleton的实例,所以不是严格意义上的单例。

    三、枚举

    上代码:
    这种方式是Josh Bloch提倡的,利用枚举的特性,让JVM来保证线程安全和单例的问题,还能防止反序列化和反射,除了大家不怎么常用外,其实这种简单的方式是个很好的方式。
    反编译看一下,其实枚举是在static块中进行的对象的创建:

    单例模式真的有那么好吗?

    优点:
     
    1.提供了唯一实例的受控访问。
    2.因为只有一个实例,节约了系统资源,提高系统性能。
     
    缺点:
     
    1.单例模式没有抽象层,扩展比较困难。
    2.单例类的职责过重,违背了“单一职责原则”。
     
    我的推荐
     
    我们去使用单例基本目标就是为了节省内存资源,而且一般的web项目都会引入Spring框架,通过Spring实现的单例和上面设计模式说的单例有所不同。设计模式的单例是在整个Java应用中只有一个实例,而Spring中的单例是在一个IOC容器中就只有一个单例。但对于web应用来说,web容器(Jetty或tomcat)对用户的每个请求都会创建一个单独的servlet线程去处理请求,Spring框架下的接口每个action也都是单例的,那么其实就保证了我们使用的是一个实例。
     
    同时Spring也支持我们通过注解或者xml进行lazy-init,也可以指定scope确定其是否为全局单例,又或者是多个实例,对于程序来说有了更多的选择。
    当然上面提到的线程安全的问题,其实大多数情况下Spring是没有去保证所有bean的线程安全,所以主动权交给了开发者,我们自己编写程序要保证线程安全的。不过在我们经常使用的数据库dao层的那些dao 的bean对象,Spring通过ThreadLocal对象,区别与我们常用的加锁的方式而是用空间换时间,给每个线程分配了独自的变量副本,从而隔离了多线程访问对数据访问的冲突,保证了线程安全性。至于这个类和这个机制,这里就不展开谈了,谈多了这篇文章就装不下了。
     
     
  • 相关阅读:
    Codeforces 526D Om Nom and Necklace (KMP)
    HDU
    HDU
    Codeforces 219D
    HDU
    HDU
    POJ
    HDU
    HDU
    第二次作业
  • 原文地址:https://www.cnblogs.com/huaweiyun/p/12973585.html
Copyright © 2020-2023  润新知