设计模式之懒汉式单例模式

设计模式之单例模式

定义

保证一个类仅有一个实例,并提供一个全局访问点。java

类型

建立型设计模式

适用场景

想确保任何状况下都绝对只有一个实例。安全

优势

  • 在内存中只有一个实例,减小内存开销。特别是一个对象在使用时须要频繁建立和销毁同时建立和销毁性能没法优化时。
  • 能够避免对资源的多重占用。好比我在对一个文件进行写操做,使用单例能够避免同时对这个文件进行写操做。
  • 设置全局访问点,严格控制访问。

缺点

没有接口,拓展困难。若是想要拓展就必需要修改代码。多线程

下面开始看代码。咱们首先实现如下懒汉式。懒汉式的单例模式注重的是延时加载,只有在引用的时候才会加载。jvm

public class LazySingleton {
    private static LazySingleton lazySingleton = null;
 
    public static LazySingleton getInstance(){
        if(lazySingleton == null){
            lazySingleton = new LazySingleton();
        }
        return lazySingleton;
    }
}    
复制代码

这个方式写的在单线程下是没有什么问题的。 可是在多线程的环境就会出现问题,假设其中一个线程到 lazySingleton = new LazySingleton();这一行时尚未return出去。 此时又有一个线程进入这个方法。 由于此时尚未return出去。 因此在进行判断时lazySingleton依旧为null。 咱们说 单例模式的优势就是:ide

在内存中只有一个实例,减小内存开销。特别是一个对象在使用时须要频繁建立和销毁同时建立和销毁性能没法优化时。性能

原本咱们但愿这个对象之建立一次。这样的话这个对象2次进入都建立了对象,这样就不能达到减少内存开销的目的。 小伙伴们能够用这段代码debug走一下看看(使用idea在多线程环境下debug记得要切换一下,不会的小伙伴能够百度一下)。优化

咱们看一下这个代码该怎么优化。那位很简单嘛,不就是多线程的问题吗,加个锁不就好了。好咱们加上锁了。idea

public class LazySingleton {
    private static LazySingleton lazySingleton = null;
 
    public synchronized static LazySingleton getInstance(){
        if(lazySingleton == null){
            lazySingleton = new LazySingleton();
        }
        return lazySingleton;
    }
}  

复制代码

如今是咱们在方法上添加了synchronized关键字。这个方法是个静态方法也是说这个所至关于加载类上面。一样的咱们还能够这样写,效果是同样的。spa

public class LazySingleton {
    private static LazySingleton lazySingleton = null;
 
    public static LazySingleton getInstance(){
    synchronized(LazySingleton.class){
         if(lazySingleton == null){
            lazySingleton = new LazySingleton();
        }
    }
        return lazySingleton;
    }
} 

复制代码

这作确实解决了多线程的问题,可是咱们都知道,synchronized是比较耗费资源的加锁方式,并且在使用static方法时锁的是这个class。锁的范围也是很是的大, 性能消耗也是很是明显。下面咱们看看有没有在性能和安全性上可以取得平衡的方案。

咱们可使用doubleCheck双重检查的方式来上实现懒汉式单例。这种方式是兼顾性能和安全的一种实现方式。

public class LazyDoubleCheckSingleton {
    private volatile static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;
    private LazyDoubleCheckSingleton(){

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

复制代码

双重锁如今再也不是对方法进行加锁,咱们如今对代码块进行加锁。此时咱们是锁定了这个类,这个if方法仍是能够进来,这种方式大大的缩短了synchronized锁定的范围,从而减小性能消耗。 至于这里为何要加两个if判断,同窗们对比上面的第二种在类上面加锁的代码,就很明显能够看出了。 而后这种实现方式还有个坑。咱们来解决一下。 咱们来看这个代码, lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();看起来是一行代码,其实这里经历了三个过程:

  1. 分配内存给这个对象
  2. 初始化对象
  3. 设置lazyDoubleCheckSingleton 指向刚分配的内存地址

这里的坑就是2和3操做可能会被重排序,2和3的操做顺序可能会被颠倒。此时执行流程为:

  1. 分配内存给这个对象

. 3. 设置lazyDoubleCheckSingleton 指向刚分配的内存地址

. 2. 初始化对象

此时在执行到3操做是,其实咱们的实例尚未完成实例化的操做,可是在进行空判断的时候,由于已经被分配了内存空间,判断时并不为空,而后直接return一个空对象。若是咱们拿到这个对象进行操做就会报空指针异常了。那为何会进行重排序呢,在单线程中java语言规范中容许2和3进行重排序,由于重排序能够提升程序的执行效率。

为了方便你们理解,你们能够看下面的图,这是一个单线程的执行过程。

对于在多线程中的执行就变成下面的方式了。

如图,对于线程0步骤2和步骤3进行重排序并不会影响最后的结果。可是对于线程1就不同了。当线程0执行步骤3以后线程1进入对象是否为空的判断,此时对象并无完成初始化,可是对象已经被设置了内存空间,因此判断是否为空时不为空,这时候线程1就会执行return操做也就是步骤4,然而拿到的是一个没有初始化的完成的对象。

那么咱们该如何解决这个问题呢? 我能够不容许线程0进行2和3的重排序,或者不让线程1看到2和3的重排序。

咱们使用volatile这个关键字限制线程0进行重排序。完整的代码以下:

public class LazyDoubleCheckSingleton {
    private volatile static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;
    private LazyDoubleCheckSingleton(){

    }
    public static LazyDoubleCheckSingleton getInstance(){
        if(lazyDoubleCheckSingleton == null){
            synchronized (LazyDoubleCheckSingleton.class){
                if(lazyDoubleCheckSingleton == null){
                    lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();

                }
            }
        }
        return lazyDoubleCheckSingleton;
    }
}
复制代码

下面咱们来实现一下不让线程看到操做2和操做3的重排序的方案。 使用静态内部类的方式。

public class StaticInnerClassSingleton {
    private static class InnerClass{
        private static StaticInnerClassSingleton staticInnerClassSingleton = new StaticInnerClassSingleton();
    }
    public static StaticInnerClassSingleton getInstance(){
        return InnerClass.staticInnerClassSingleton;
    }
     private StaticInnerClassSingleton(){
    }
}
复制代码

咱们来讲一下为何这样作可让操做2和操做3对线程1不可见。

类在初始化的阶段:也就是类在加载而且在线程使用以前,jvm在类的初始化阶段会获取一个类初始化的一个锁,这个锁会同步多个线程对一个类的初始化。示意图以下,当线程0在执行时,对于线程1是不会看到的。

上面的方式咱们称之为基于类初始化的延迟加载的单例模式。 根据java语言规范主要如下几种种状况,发生这个类将被当即初始化。

  1. 有个实例被建立
  2. 类中的静态方法被调用
  3. 类中的静态成员被赋值
  4. 类中的静态成员被使用

懒汉式的单例模式咱们就降到这里

相关文章
相关标签/搜索