设计模式之单例模式(含双重检查锁定详解)

面试常考题

请解释一下什么是单例模式,并给出一个使用场景。

单例模式有哪几种实现?如何保证线程安全?

单例模式有什么好处?写出单例模式代码。

单例模式是什么?

单例模式是一种设计模式,它确保一个类在整个运行过程中只有一个实例,并提供全局访问点来获取该实例。核心思想就是只实例化一次。

单例模式有哪几种实现方式?手写单例模式

单例模式的实现方式主要有饿汉式和懒汉式。

饿汉式就是指实例在类加载的时候就创建,线程安全。缺点就是不用的话浪费资源,启动速度慢。

懒汉式指实例在首次访问时创建(需要保证线程安全)。

饿汉式代码

public class Singnle{
    private static final Singnle instance = new Singnle();
    private Singnle(){};
    public static Singnle getInstance(){
        return instance;
    }
}

饿汉式天然支持线程安全,当多个线程同时首次访问这个类时(如调用getInstance()),JVM会确保只有一个线程能执行类的初始化(其他线程阻塞等待)。

懒汉式代码

public class Singnle{
    private static Singnle instance;
    private Singnle(){}
    public static Singnle getInstance(){
        if(instance==null){
            instance = new Singnle();
        }
        return instance;
    }
}

但是应该考虑线程安全问题,这样写不是线程安全的,万一多线程同时进来就会创建多个实例。我们应在返回实例的时候加锁。然后就有了下面的代码:

//其他代码省略,这里只改动get方法
public static synchronized Singnle getInstance(){
        if(instance == null){
           instance = new Singnle(); 
        }
        return instance;
}

看完上面的代码你可能不会觉得有什么问题,如果要使用这个实例的话怎么办?一般是在main方法里面调用这个静态方法,比如:

public class Main {
    public static void main(String[] args) {
        // 获取单例对象
        Singleton singleton = Singleton.getInstance();
    }
}

这里就会产生一个问题,假设我的单例模式实现的是一个日志管理器,那我在每个类中每次使用这个实例对象的话都要调用这个getInstance()方法,每次调用这个方法都会加锁,但是加锁的初衷是创建这个实例的时候防止线程竞争,显然在方法上加锁就非常影响性能了。所以我们应该在第一次创建实例的时候加锁,修改代码如下:

public class Singnle{
    private static volatile Singnle instance;
    private Singnle(){}
    public static Singnle getInstance(){
        if(instance == null){
            synchronized(Singnle.class){
                if(instance == null){
                    instance == new Singnle();
                }
            }
        }
       return instance;
    }
}

 注意到上面这段代码,修改的部分有:instance前面加了一个volatile修饰,之前的一个if现在变为了两个if。首先我们先讲一下为什么要加volatile,volatile是java中的一个关键字,它在此处的作用是保证这个共享变量的可见性和有序性。可见性就是该变量被一个线程修改之后能立刻被其他线程感知(立刻刷新到主内存)。还有一个作用就是禁止指令的重排序。学过JVM的我们知道,创建对象的过程并不是原子性的,它其实可以被划为三步操作:

1,在堆中分配内存空间

2,初始化对象实例(实例化)

3,在虚拟机栈的栈帧中的局部变量表中的对象引用(reference)赋值(该值就是在堆中的内存地址)

了解了对象创建的过程我们就能理解了这里为什么要用volatile,如果没加volatile的话,cpu会进行重排序(最大程度的提高性能但不会影响结果),但是如果这里进行重排序的话,比如说顺序改为了1-》3-》2,那么同时呢在别的线程访问这个变量的时候,比如调用instance的其他方法,由于这个指令重排序了,这个instance就不是null了,但是还没进行完对象的实例化,就会导致这个对象还没完全准备好呢就访问它,从而导致错误。

还有一个问题,为什么这里使用了双重检查锁定呢?我们可以详细的一步一步地推导多线程的执行步骤

假设有很多线程使用Singnle.getInstance(),然后多个线程就会同时进入这个方法。由于此时实例还没有创建,所有在红色箭头处,所有的线程判断结果都是一样的,都会进入这个if,然后都到了这个绿色箭头处。再往下执行的话就有synchronized关键字了,所以只会有一个线程进入,去创建这个实例。其他线程卡在绿色箭头处。然后实际工作的线程在执行完new Singnle()的代码后,就会解锁然后return。这里就会出现问题,之前跟它一起竞争的线程还卡在synchronized之外呢,不管他是cas自旋也好,阻塞也好,那个执行创建实例的线程解锁后,其他线程会重新竞争进入锁,!!!,此时如果没有这个第二次判断,那么,其他的线程就会又一次的执行这个创建实例的代码,显然就违背单例模式了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值