关于volatile关键字在多线程单例模式下的应用

多线程下单例模式存在的问题

单例模式相信很多人都了解过,不过对于初学者来说,单例模式主要是在单线程中的操作,事实上很多问题只要涉及到了多线程当中,就会暴露出许多问题,同样的,单例模式如果应用到多线程当中,那它就不叫单例模式了,不信?让我们来瞧一瞧。
首先编写一个单例模式对于大家来讲应该是很简单的事了,那么在测试方法中,我们来开启多个线程,来获取我们单例模式new出来的对象回怎样呢?我们来看下面一段代码:

public class SingletonDemo {
    private  static SingletonDemo instance=null;
    private SingletonDemo(){
        System.out.println(Thread.currentThread().getName()+" 我是构造方法SingletonDemo。。。");
    }
    private static SingletonDemo getInstance(){
        if (instance==null){
                         instance = new SingletonDemo();
            }

        return instance;
    }

    public static void main(String[] args) {
        //开启多个线程
        for (int i =1; i <=20; i++) {
            new Thread(() -> SingletonDemo.getInstance(),String.valueOf(i)).start();
        }
    }
}

乍一看代码似乎没什么毛病,我们来运行一下

1 我是构造方法SingletonDemo。。。
2 我是构造方法SingletonDemo。。。
4 我是构造方法SingletonDemo。。。
3 我是构造方法SingletonDemo。。。

Process finished with exit code 0

哎?不是单例模式吗?为什么会有多个构造方法执行了?这就是多线程的神(wu)奇(yv)之处,在多线程环境下,每个线程都是抢着进行,大家都是在并行执行,在上面代码中,首先执行线程一,在判断完实例对象为null时或者正在new一个实例对象的时候,线程二抢到了资源,所以线程二开始执行,而在线程二执行的时候,有可能又会被其他线程抢占资源而执行下去,所以就会造成,构造方法被执行了多次,实例对象被new了多次。
如何解决这种问题呢?
会有人想到在get方法前加synchronized修饰,这肯定是可以的,但是这样加,我们程序的并发性就会降低很多,我们只是因为创建一个实例对象这一句代码而出现的问题,那么我们就要在这句代码上动点文章了。
那么对于这种问题,可以通过DCL(Double Check Lock)双端检锁机制来解决,也就是下面这段代码:

public class SingletonDemo {
    private  static SingletonDemo instance=null;
    private SingletonDemo(){
        System.out.println(Thread.currentThread().getName()+" 我是构造方法SingletonDemo。。。");
    }
    private static  SingletonDemo getInstance(){
        if (instance==null){
                    synchronized (SingletonDemo.class) {
                        if (instance==null) {
                            instance = new SingletonDemo();
                        }
                    }
            }

        return instance;
    }

    public static void main(String[] args) {
        //开启多个线程
        for (int i =1; i <=20; i++) {
            new Thread(() -> SingletonDemo.getInstance(),String.valueOf(i)).start();
        }
    }
}

如果第一次检查实例对象部位null,则返回已实例化的对象,也就不需要执行下面的锁和初始化操作,这样做确实大大减少了性能开销,并且通过加锁,避免了多个线程去实例对象,但是仅仅这样就可以了吗?在大多数情况下,我们写这样的程序出来的结果是符合我们的预期的,但是其实实际上,还是有漏洞的,在我们的程序被java虚拟机编译的时候,是被写为字节码来执行的,而我们new一个实例对象,在我们的程序当中是一行代码。但编译器实现的时候却是这样:
memory = allocate(); 1
ctorInstance(memory); 2
instance = memory; 3
第二步和第三步之间没有数据依赖,所以有可能在编译器上会被重排序,就会导致出现下面这种情况:
memory = allocate(); 1
instance = memory; 3
ctorInstance(memory); 2
第三步被提前了!可是这个时候只是分配了内存地址,我的对象没有被初始化啊!但是现在如果线程二来执行第一步验证,这时候回显示instance!=null,所以B线程就继续往下了,那么他返回的会是一个没有被初始化的对象,这个时候,volatile关键字就来了,因为什么呢?volatile关键字有一个特性,那就是:禁止指令重排。不允许编译器重排序。我们在刚开始为实例null值的instance加上volatile关键字修饰,上面的问题就可以得到解决。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值