设计模式 之 单列设计模式

本文章是在学习了 微信公众号   “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/

 

posted @ 2018-04-27 12:18  阿肯新  阅读(214)  评论(0编辑  收藏  举报