两种单例模式详解(内含懒汉式的双重校验锁详解)

两种单例模式详解

对于一个软件系统的某些类而言,我们无须创建多个实例。举个大家都熟知的例子——Windows任务管理器,我们可以做一个这样的尝试,在Windows的“任务栏”的右键弹出菜单上多次点击“启动任务管理器”,看能否打开多个任务管理器窗口?通常情况下,无论我们启动任务管理多少次,Windows系统始终只能弹出一个任务管理器窗口,也就是说在一个Windows系统中,任务管理器存在唯一性。
实际开发中,我们也经常遇到类似的情况,为了节约系统资源,有时需要确保系统中某个类只有唯一一个实例,当这个唯一实例创建成功之后,我们无法再创建一个同类型的其他对象,所有的操作都只能基于这个唯一实例。为了确保对象的唯一性,我们可以通过单例模式来实现,这就是单例模式的动机所在。

模式定义

单例模式定义如下: 单例模式(Singleton Pattern):确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。单例模式是一种对象创建型模式。
单例模式有三个要点:

  1. 某个类只能有一个实例;
  2. 它必须自行创建这个实例;
  3. 它必须自行向整个系统提供这个实例。

结构图

在这里插入图片描述
在单例类的内部实现只生成一个实例,同时它提供一个静态的getInstance()工厂方法,让客户可以访问它的唯一实例;为了防止在外部对其实例化,将其构造函数设计为私有;在单例类内部定义了一个Singleton类型的静态对象,作为外部共享的唯一实例。

单例模式一般分为两种,分别是饿汉式与懒汉式,下面就分别讲解这两种模式,并且详解饿汉式的双重校验锁

饿汉式

对于饿汉式来说,它在定义静态变量的时候实例化单例类,因此在类加载的时候就已经创建了单例对象。而且该单例对象用final修饰,之后每次获取直接返回即可。

public class HungrySingleton {
    private static final HungrySingleton singleton=new HungrySingleton();
    private HungrySingleton(){

    }
    public static HungrySingleton getInstance(){
        return singleton;
    }
}

懒汉式(双重校验锁)

懒汉式单例在第一次调用getInstance()方法时实例化,在类加载时并不自行实例化,这种技术又称为延迟加载(Lazy Load)技术,即需要的时候再加载实例。
先看一下在单线程情况下的懒汉式单例:

public class LazySingleton {
    private static LazySingleton singleton=null;
    private LazySingleton(){

    }
    public static LazySingleton getInstance(){
        if(singleton==null){
            singleton=new LazySingleton();
        }
        return singleton;
    }
}

可以看到,在获取实例的时候,会先判断一下当前的静态变量singleton是否为null,如果为null,则为其初始化。如果不为null,则直接返回。
但是这样的程序在多线程环境下会出现问题!!!
在多线程环境下这样的代码会出现问题,因此我们考虑给getInstance()方法加上同步锁,防止多个线程同时访问getInstance()方法,如下。

public class LazySingleton {
    private static LazySingleton singleton=null;
    private LazySingleton(){

    }
    public static synchronized LazySingleton getInstance(){
        if(singleton==null){
            singleton=new LazySingleton();
        }
        return singleton;
    }
}

但是这样一来,每次调用getInstance()时都需要进行线程锁定判断,在多线程高并发访问环境中,将会导致系统性能大大降低。那么如何解决该问题呢?有人提出了双重校验锁
在加锁之前先判断一下该静态变量是否为null。如果不为null就不需要再加锁进行初始化了。这样在高并发的环境下就不会出现频繁获取锁的情况了。

public class LazySingletonSyn {
    private static LazySingletonSyn singleton=null;
    private LazySingletonSyn(){

    }
    public static LazySingletonSyn getInstance(){
        if(singleton==null){
            synchronized (LazySingletonSyn.class){
                if(singleton==null){
                	// 标记点 1
                    singleton=new LazySingletonSyn();
                }
            }
        }
        return singleton;
    }
}

现在程序是完美的了吗?
依然不是!问题出现在singleton=new LazySingletonSyn();语句
由于现代的处理器大多采用指令级并行技术。为了提高指令的执行效率,在指令执行的阶段可能会出现
重排序
的现象。
我们看上面代码的标记点1 singleton=new LazySingletonSyn();
该语句在执行的时候会被分解为三条(伪)指令:

memory=allocate(); 			//1.分配对象的内存空间
ctorInstance(memory);        //2. 初始化对象
instance=memory;				//3. 设置instance指向刚分配的内存地址

在上面的伪代码中,其中2与3可能会被重排序。

这里涉及到了as-if-serial概念。as-if-serial概念是指不管怎么重排序,单线程程序的执行结果不能被改变。而在该语句中,将2,3的执行顺序改变之后,在单线程的情况下该程序的运行结果不会被改变。因此2,3可能重排序

一旦被重排序,就可能出现以下的结果:
如果线程A,B按照下图的时间执行,那么B线程将会得到一个还没有被初始化的对象!!
在这里插入图片描述
问题就出现在2,3的重排序!那么我们只需要禁止2,3重排序即可。

双重校验锁实现懒汉式
我们只需要对上面的代码进行很小的改动(将singleton声明为volatile),就可以实现线程安全的懒汉式单例。

public class LazySingletonSyn {
    private volatile static LazySingletonSyn singleton=null;
    private LazySingletonSyn(){

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

解释如下:
由于singleton被volatile修饰,那么为了实现volatile的内存语义(保证singleton在多线程环境下对共享内存的可见性),编译器在生成字节码的时候,会在指令序列中插入内存屏障来禁止特定的指令重排序。
由于singleton=new LazySingletonSyn();是一个写操作,在该操作的指令之后,JMM(Java内存模型)会插入一个storeload内存屏障。
此时的内存指令变成:

memory=allocate(); 			//1.分配对象的内存空间
ctorInstance(memory);        //2. 初始化对象
instance=memory;				//3. 设置instance指向刚分配的内存地址
storeload;							//4. storeload内存屏障

该指令(storeload)会保证在屏障之前的所有内存访问指令全部完成之后,再执行该屏障的之后的语句。因此当线程B再尝试singleton==null的时候,线程A的singleton=new LazySingletonSyn();以及全部执行完成了。因此就不会再出现上面的由于指令2,3重排序导致的问题了。
其实就是一句话:
JMM会禁止volatile写与其之后可能存在的volatile读/写重排序。因此不会存在上面的图出现的情况!

  • 6
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值