zoukankan      html  css  js  c++  java
  • 设计模式 之 单列设计模式

    本文章是在学习了 微信公众号   “java后端技术 ” 之后自己的学习笔记  。 其中直接 复制了 相当部分的原作者的原文。

        如果您看到了我的这篇文章, 推荐您 查看原文

    原文连接  :   https://mp.weixin.qq.com/s/CfekzTTT-a066_PyT_n_eA   

    在以前自己也了解过一些设计模式, 这其中就包括了单例模式, 但是 对单例模式只限于 基本的     懒汉式    和    饿汉式 :

     饿汉式代码示例 :

    public DemoSingle{
        //私有化构造器
        private DemoSingle(){}    
        //提前构造好方法
        private static DemoSingle single = new DemoSingle();
        //提供暴露对象的方法
         public DemoSingle getDemoSingle(){
                 return  single;
          }
    }        

    懒汉式代码示例 :

    /**
     * Create by yaoming  on  2018/4/27
     */
    public class DemoSingle {
        //私有化构造方法
        private DemoSingle(){}
        //私有化 本类对象引用
        private static DemoSingle single = null;
        //得到本类方法的引用
        public DemoSingle getDemoSingel(){
            synchronized (DemoSingle.class){
                if(single == null){
                    single = new DemoSingle();
                }
            }
            return single;
        }
    }
    

      

    所谓单列模式就是说, 全局在任何一个地方发使用到的该类对象都是同一个对象,首先要保证 对象一直存在(一直有引用指向对象),所以,使用一个静态引用

    指向该类。 同时要保证 只有一个对象, 所以要私有化 构造方法, 使得只有自己能构造这个对象(而且自己必须构造且之构造一个该对象)。

    饿汉式    是在加载该类的时候就进行了对象的建立,无论我们是否使用到了 这个对象。 其安全有效, 不涉及多线程操作。 但是其造成了资源的浪费。

    懒汉式    在实际情况中我们可能为了性能着想, 往往希望能使用延迟加载的方式来创建对象, 这个就是懒汉式了。

        上面的懒汉式代码,为了考虑多线程的关系, 加了一个同步代码块, 这样虽然解决了 多线程安全问题, 但是却因为每次都会进行一个同步情况下的判断,

        往往使得效率并,并没有增加,  用原文作者的话来说就是 : 使用一个 百分之百的盾 来 阻挡一个 百分之一 的出现的问题。 这显然不合适。

    遂优化 :

    public class DemoSingle {
        //私有化构造方法
        private DemoSingle(){}
        //私有化 本类对象引用
        private static DemoSingle single = null;
        //得到本类方法的引用
        public DemoSingle getDemoSingel(){
            if(single == null){
                synchronized (DemoSingle.class){
                    if(single == null){
                        single = new DemoSingle();
                    }
                }
            }
            return single;
        }
    }
    

     

    这个代码就是原来我对于懒汉式的理解了, 在看了原作者的文章后, 才发现在这个看似完美的代码下面隐藏的问题,

    这里 原作者 谈到了两个概念 : 原子操作 和 指令重排 

    这里是作者原文 :

      

    原子操作:
    简单来说,原子操作(atomic)就是不可分割的操作,在计算机中,就是指不会因为线程调度被打断的操作。比如,简单的赋值是一个原子操作:

     

    m = 6; // 这是个原子操作

    假如m原先的值为0,那么对于这个操作,要么执行成功m变成了6,要么是没执行 m还是0,而不会出现诸如m=3这种中间态——即使是在并发的线程中。

    但是,声明并赋值就不是一个原子操作:

     

    int  n=6;//这不是一个原子操作

    对于这个语句,至少有两个操作:①声明一个变量n ②给n赋值为6——这样就会有一个中间状态:变量n已经被声明了但是还没有被赋值的状态。这样,在多线程中,由于线程执行顺序的不确定性,如果两个线程都使用m,就可能会导致不稳定的结果出现。

    指令重排:
    简单来说,就是计算机为了提高执行效率,会做的一些优化,在不影响最终结果的情况下,可能会对一些语句的执行顺序进行调整。比如,这一段代码:

     

    int a ;   // 语句1
    a = 8 ;   // 语句2
    int b = 9 ;     // 语句3
    int c = a + b ; // 语句4

    正常来说,对于顺序结构,执行的顺序是自上到下,也即1234。但是,由于指令重排
    的原因,因为不影响最终的结果,所以,实际执行的顺序可能会变成3124或者1324。

    由于语句3和4没有原子性的问题,语句3和语句4也可能会拆分成原子操作,再重排。——也就是说,对于非原子性的操作,在不影响最终结果的情况下,其拆分成的原子操作可能会被重新排列执行顺序。

    OK,了解了原子操作和指令重排的概念之后,我们再继续看代码三的问题。

    主要在于singleton = new Singleton()这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情。
      1. 给 singleton 分配内存
      2. 调用 Singleton 的构造函数来初始化成员变量,形成实例
      3. 将singleton对象指向分配的内存空间(执行完这步 singleton才是非 null了)

    在JVM的即时编译器中存在指令重排序的优化。
      
    也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。
      
    再稍微解释一下,就是说,由于有一个『instance已经不为null但是仍没有完成初始化』的中间状态,而这个时候,如果有其他线程刚好运行到第一层if (instance ==null)这里,这里读取到的instance已经不为null了,所以就直接把这个中间状态的instance拿去用了,就会产生问题。这里的关键在于线程T1对instance的写操作没有完成,线程T2就执行了读操作。

    由此可见, 我的第二段 懒汉式代码存在 隐患 , 根据作者思路 将之改为 :

    public class DemoSingle {
        //私有化构造方法
        private DemoSingle(){}
        //私有化 本类对象引用
        private static volatile DemoSingle single = null;
        //得到本类方法的引用
        public DemoSingle getDemoSingel(){
            if(single == null){
                synchronized (DemoSingle.class){
                    if(single == null){
                        single = new DemoSingle();
                    }
                }
            }
            return single;
        }
    }
    

      其实就是加上了一个 volatitle 关键字 ,   这里  volatitle  关键字的作用是禁止 指令重排, 在对 single 进行复制完成之前是不会进行 读操作的。

           (作者原文 注意:volatile阻止的不是singleton = new Singleton()这句话内部[1-2-3]的指令重排,而是保证了在一个写操作([1-2-3])完成之前,不会调用读操作(if (instance == null))。)

      

      这样就解决了传统的 懒汉式单例模式 的多线程安全问题, 除此之外 原作者还提供了 其他两种更为简便的 方式:

      

    静态内部类:

    public class DemoSingle {
        //私有化构造方法
        private DemoSingle(){}
        //静态内部类
        private static class DemoSingleHand{
            private static final DemoSingle DEMO_SINGLE = new DemoSingle();
        }
        //获得该类对象的方法
        public static DemoSingle getDemoSingel(){
            return DemoSingleHand.DEMO_SINGLE;
        }
    }
    

      

    这种写法的巧妙之处在于:对于内部类SingletonHolder,它是一个饿汉式的单例实现,在SingletonHolder初始化的时候会由ClassLoader来保证同步,使INSTANCE是一个真单例。

    同时,由于SingletonHolder是一个内部类,只在外部类的Singleton的getInstance()中被使用,所以它被加载的时机也就是在getInstance()方法第一次被调用的时候。
      
    它利用了ClassLoader来保证了同步,同时又能让开发者控制类加载的时机。从内部看是一个饿汉式的单例,但是从外部看来,又的确是懒汉式的实现

    枚举:

    是不是很简单?而且因为自动序列化机制,保证了线程的绝对安全。三个词概括该方式:简单、高效、安全

    这种写法在功能上与共有域方法相近,但是它更简洁,无偿地提供了序列化机制,绝对防止对此实例化,即使是在面对复杂的序列化或者反射攻击的时候。虽然这中方法还没有广泛采用,但是单元素的枚举类型已经成为实现Singleton的最佳方法。

    原文地址:https://gyl-coder.top/Java%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E5%8D%95%E4%BE%8B%E6%A8%A1%E5%BC%8F/

  • 相关阅读:
    HDU 4435
    、输入某人出生日期(以字符串方式输入,如198741)使用DateTime和TimeSpan类,(1)计算其人的年龄;(2)计算从现在到其60周岁期间,总共多少天。
    NetBeans IDE 7.0 Release Candidate 2 Now Available
    NetBeans 时事通讯(刊号 # 142 Apr 14, 2011)
    B3log Solo & B3log Symphony/Rhythm
    B3log Solo & B3log Symphony/Rhythm
    GAE SDK 1.4.3 发布了!
    NetBeans IDE 7.0 Release Candidate 2 Now Available
    Guice 3.0 正式版发布
    NetBeans 时事通讯(刊号 # 141 Mar 30, 2011)
  • 原文地址:https://www.cnblogs.com/soficircle/p/8953055.html
Copyright © 2011-2022 走看看