zoukankan      html  css  js  c++  java
  • 并发中的单例模式

    一、单例模式简介

         单例模式,是一种常用的软件设计模式。在它的核心结构中只包含一个被称为单例的特殊类。通过单例模式可以保证系统中,应用该模式的类一个类只有一个实例。即一个类只有一个对象实例。在java代码中,通常new关键字创造出来的对象,对系统的开销一般都挺大的。所以在某些情况下,单例的实现也是应对系统优化的一种解决办法。

    二、单例模式的实现

       常见的单例有这几种实现

      1、饿汉式

      2、饱汉式

      3、双重校验

      4、静态内部类

         1、饿汗式

           先来介绍饿汉式,饿汉式,顾名思义,就是一进入这个类,该类的实例就被初始化完成了。接下来来看下代码。

      

    1 public class Demo {
    2     
    3     private static Demo h = new Demo();
    4     private Demo(){}
    5     
    6     public static Demo getInstance(){
    7         return h;
    8     }
    9 }

        代码也和简单,就是直接构造一个私有的构造器,然后在建立一个成员变量,顺便实例化该类,在调用该类的getInstance方法,当然前面也说过是一进入这个类,

    该类的实例就被创建完成,所以也可以利用类的加载顺序来编写这个代码。比如 A类是B类的子类,在初始化A类的实例的时候,会先去父类B中去,看看有没有静态块和静态成员变量(静态方法只有被调用时才会加载,且只会被加载一次),若有则先去加载B类的静态块和静态成员变量,再加载A类的。之后会去调用B的构造器,最后才会调用本类对应的构造器。(这个不多讲,具体可以去网上搜搜)。

    所以我们可以在静态代码块中实例化。如下代码

     1 public class Demo {
     2     
     3     private static Demo h = null;
     4     private Demo(){}
     5     static{
     6         h = new Demo();
     7     }
     8     public static Demo getInstance(){
     9         return h;
    10     }
    11 }

    该种实现的单例是线程安全的。当然由于它会提前初始化,所以会提前占用一些系统资源。

         2、饱汉式

              饱汉式的构造实例的时候与饿汗式相反,它只有在第一次需要的时候才会去构造实例。具体实现代码如下 

     1 public class Demo {
     2     
     3     private static Demo h = null;
     4     private Demo(){}
     5     public static Demo getInstance(){
     6         if(h == null){  //1
     7             h = new Demo();  //2
     8         }
     9         return h;
    10     }
    11 }

            饱汉式最常见的的编写方式就是上述代码,对于刚了解单例模式的人来说,饱汉式就写完了,不过在单线程环境也确实可以说是写完了,A线程在获取实例,第一次获取时,看见为null,进行初始化,第二次,不是null,直接返回。这也是一种很理想的流程。但是值得注意的是,在多线程下,它就值得推敲了。比如看下面例子

           线程A:嘿嘿!我已经走到了2,这初始化的好处我就要独占了,想想都鸡冻,我要去初始化Demo类的实例了,啦啦啦。

           线程B:哈哈!线程A那个SD,我都走到了1,它竟然还没发现我,还想独占Demo的实例化,没门!!

           旁白:线程A还完成了对该类实例的初始化,线程B也进入了对该实例的构造中,

           因此,线程A和线程B都同时初始化了该实例,这也不满足单例的条件。

       于是有人很自然的想到,加锁。如下

           

     1 public class Demo {
     2     
     3     private static Demo h = null;
     4     private Demo(){}
     5     public synchronized static Demo getInstance(){
     6         if(h == null){
     7             h = new Demo();
     8         }
     9         return h;
    10     }
    11 }

      这样确实可以防止多线程环境造成多个实例。但的缺点是每一次获取都去加锁,会对性能有一定的损失。所以有了双重校验锁。

        3、双重校验

               双重校验,就是在获取单例的时候,对加锁的方式进行了改变,它不在方法上加锁,它对代码块进行加锁,这样的效率比饱汉式要高。具体代码如下

     1 public class Demo {
     2     
     3     private static Demo h = null;
     4     private Demo(){}
     5     public static Demo getInstance(){
     6         if(h == null){  //1
     7             synchronized (new Object()) {//2
     8                 if(h == null){
     9                     h = new Demo();
    10                 }
    11             }
    12         }
    13         return h;
    14     }
    15 }

      也许有人看了以上代码后会有疑问,要加上两个if判断干嘛?加一个不行吗,比如如下

     1 public class Demo {
     2     
     3     private static Demo h = null;
     4     private Demo(){}
     5     public static Demo getInstance(){
     6         if(h == null){//1
     7             synchronized (new Object()) {//2
     8                 h = new Demo();
     9             }
    10         }
    11         return h;
    12     }
    13 }

      这样不也对进行实例化的时候加锁了吗?也可以保证线程安全啊!

             看这个例子

             线程A:嘿嘿!我已经走到了2,咦!竟然还有锁,更好了,这初始化的好处我就要独占了,想想都鸡冻,我要去初始化Demo类的实例了,啦啦啦。

        线程B:哈哈!线程A那个SD,我都走到了1,它竟然还没发现我,还想独占Demo的实例化,没门!!卧槽,静态被线程A那个SD上锁了,哎等等吧!

             线程B:咦!锁的钥匙又回来了,哎,没希望了,希望能给我喝点汤。n秒后,B处于惊讶中,没想到我还能初始化。哈哈哈。

          为啥双重会被认为是线程安全的。看这个例子

         线程A:嘿嘿!我已经走到了2,咦!竟然还有锁,更好了,这初始化的好处我就要独占了,想想都鸡冻,我要去初始化Demo类的实例了,啦啦啦。

         线程B:哈哈!线程A那个SD,我都走到了1,它竟然还没发现我,还想独占Demo的实例化,没门!!卧槽,静态被线程A那个SD上锁了,哎等等吧!

        线程B:咦!锁的钥匙又回来了,哎,没希望了,希望能给我喝点汤。n秒后,B处于崩溃中,没想到竟然还有if(h == null)这个大门,我进不去了,呜呜呜。

        

         值得注意的是,该种产生单例的方式也会有线程安全的问题,学过java的都知道,java中在new对象的时候,并不是原子操作,它有以下三个大概步骤

         一、分配内存空间

         二、初始化对象

      三、将内存地址赋给引用h

       由于重排序原因(关于重排序的知识,可自行网上搜索),可能在new对象时,第二步和第三步发生了交换,导致错误发生,理由是,此时的h是一个地址,但它

       还没完成初始化,如下例子

          线程A:嘿嘿!我已经走到了2,咦!竟然还有锁,更好了,这初始化的好处我就要独占了,想想都鸡冻,我要去初始化Demo类的实例了,啦啦啦。

         线程B:哈哈!线程A那个SD,我都走到了1,它竟然还没发现我,还想独占Demo的实例化,没门!!卧槽,静态被线程A那个SD上锁了,哎等等吧!

         线程B:咦!锁的钥匙又回来了,哎,没希望了,希望能给我喝点汤。n秒后,B处于崩溃中,没想到竟然还有if(h == null)这个大门。

         线程B:只能认命了,我只能访问Demo对象玩玩,噗噗噗,竟然出错了。。

      其原因就如上述所说,发生了重排序导致的错误发生,当然,这个错误不一定经常发生。所有这时应该想的是怎么防止重排序。

       于是有人就想到了java中的volatile关键字来禁止代码的重排序,当然它还有保证可见性的功能,但不能和synchronized一样还能保证原子性。

       加了volatile关键字后,这样才算真的线程安全,具体代码如下     

     1 public class Demo {
     2     
     3     private volatile static Demo h = null;
     4     private Demo(){}
     5     public static Demo getInstance(){
     6         if(h == null){
     7             synchronized (new Object()) {
     8                 h = new Demo();
     9             }
    10         }
    11         return h;
    12     }
    13 }

      4、静态内部类

            静态内部类就是在该类的内部实现一个静态内部类,内部类里来实现该类的实例化,具体代码如下

             

     1 public class Demo {
     2     
     3     private volatile static Demo h = null;
     4     private Demo(){}
     5     public static Demo getInstance(){
     6         return InnerDemo.h;
     7     }
     8     //内部类
     9     private static class InnerDemo{
    10         private final static Demo h = new Demo();
    11     }
    12 }

             内部类的原理是利用了类加载器classloader机制来保证初始化h时只有一个线程,这样也就保证了线程的安全性 。同时也不像饿汗式一样,一进入该类就触发实例初始化。内部类虽然是static,但只有在return InnerDemo.h时才会触发该内部类的加载,也是懒加载的一种。

         


    以上就是我对单例设计模式的理解,若有不足之处,望指正。

       

              

      

  • 相关阅读:
    团队作业第四次
    团队作业第三次
    团队作业第二次(2)
    团队作业第二次(1)
    团队作业1
    Pillow库
    pyautogui库
    Python文件读取与异常
    元注解
    Java注解
  • 原文地址:https://www.cnblogs.com/qm-article/p/8167150.html
Copyright © 2011-2022 走看看