单例模式线程安全问题引发的思考

前言

说到面试,肯定少不了并发编程、多线程这些啦,这部分相对来说有一定的难度,而且学了就忘,让人头大,因此我打算好好捋一捋这部分,还是那句话,99%的东西是不需要智力的,惟手熟尔,多看几遍写几遍就好啦。

概述

首先需要理解多线程编程的一些由来。其实无非就是改善单线程的速度,一定能改善吗?也不尽然,在单核机器上,如果当前线程一直阻塞在IO,那么此时多线程肯定是有必要的,不然一直等着他IO也不是个事儿啊;如果面对的是一些计算任务,每个任务都需要计算资源,假如CPU1秒钟处理一个,单线程的话10秒处理10个,此时若是换成多线程,处理时间不会表还是10,可能还要在加个几秒钟的线程切换开销,此时多线程反而就慢了。

当然现代计算机基本都是多核,多线程可以更好地利用资源,同时很多业务要求异步完成,也可以使用多线程。 一旦引入多线程,就会出现线程通信的安全问题,比如单例模式在多线程下的应用。

线程安全问题之单例模式

public class SingletonDemo {

    private static SingletonDemo singletonDemo;

    public static SingletonDemo getSingletonDemo(){
        if(singletonDemo==null){
            singletonDemo=new SingletonDemo();
            System.out.println("创建单例");
        }
        return singletonDemo;
    }

    public static void main(String[] args) {
        ExecutorService executor= Executors.newFixedThreadPool(5);
        Thread task=new Thread(new Runnable() {
            @Override
            public void run() {
                SingletonDemo.getSingletonDemo();
            }
        });
        for(int i=0;i<100;i++) executor.execute(task);
    }
}
创建单例
创建单例

这是所谓的懒汉式单例,即需要时才加载。可以看到,多线程下这种单例模式被创建了两次,这其实就是可能同一时间两个线程进入了 if(singletonDemo==null),即两个懒汉去抢食,所以就创建了两次。在这里我们可以通过Synchronized/volatile等实现线程安全,这二者在多线程中也是相当基础且非常重要的。

Synchronized解决线程安全
public static synchronized  SingletonDemo getSingletonDemo(){
        if(singletonDemo==null){
            singletonDemo=new SingletonDemo();
            System.out.println("创建单例");
        }
        return singletonDemo;
    }

其实就是在方法前加上了synchronized ,这个方法是static方法,所以synchronized 在这里的语义就是锁上了这个类,每次只有一个线程能访问这个类,这也就保证了不会出现两个懒汉同时来抢食的情况,即保证了临界资源的互斥访问,进而保证了线程安全。

synchronized 的语义就是加锁,用法如下

  • 指定加锁对象,进入同步代码前要获得给定对象锁
  • 直接作用实例方法 ,相当于给当前实例加锁
  • 直接作用静态方法 给当前类加锁

这里怎么理解实例锁和类锁呢?

实例锁和类锁(static synchronized/synchronized)
实例锁

我理解的是,如果加的是实例锁,即每个实例有每个实例对应的锁,实例之间是不影响的。此时的锁只对实例自身有效。

public class SingletonDemo {

    public synchronized  void sys(){
        System.out.println("eeeee");
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public synchronized void ycy(){
        System.out.println("zzzzzz");
    }
   
    public static void main(String[] args) {
        SingletonDemo singletonDemo1=new SingletonDemo();
        SingletonDemo singletonDemo2=new SingletonDemo();
        Thread task1=new Thread(new Runnable() {
            @Override
            public void run() {
                singletonDemo1.sys();
            }
        });
        Thread task2=new Thread(new Runnable() {
            @Override
            public void run() {
                singletonDemo2.sys();
                singletonDemo1.ycy();
            }
        });
        task1.start();
        task2.start();
    }
}
eeeee
eeeee
// 中间 sleep 3s
zzzzzz

代码很好理解,这段代码中是synchronized,即对应实例锁。可以看到singletonDemo1先执行sys()进入它的实例锁,并睡眠把持住实例锁,此时singletonDemo2并没有被阻塞,也进入了sys()方法,验证了我们说的实例之间是不影响的。singletonDemo1的ycy()方法被自身的sys()方法阻塞,即实例锁只对实例自身有效。

类锁

如果加的是类锁,此时对应static synchronized
那么不同实例之间是共享同一把锁的,此时一个实例进入了static synchronized后,代表该实例获得了锁,其他实例不能再获得锁,即其他实例不能再访问该类中的static synchronized方法,这也是我们的单例模式可以起作用的原因。

public class SingletonDemo {

    public static synchronized  void sys(){
        System.out.println("eeeee");
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    
    public static  synchronized  void ycy(){
        System.out.println("zzzzz");
    }
    
    public static void main(String[] args) {
        Thread task1=new Thread(new Runnable() {
            @Override
            public void run() {
                SingletonDemo.sys();
            }
        });
        Thread task2=new Thread(new Runnable() {
            @Override
            public void run() {
                SingletonDemo.ycy();
            }
        });
        task1.start();
        task2.start();
    }
}
eeeee
// sleep 3s
zzzzz

这里使用的是static synchronnized,我们前边说所有实例之间共享一把锁,代码中其实可以把Singleton换成两个实例,结果是一样的(已测试),其实也很显然的。所有实例同一把锁,所以第一个实例进入sys()方法sleep后把持住锁,第二个实例就不能再获得该锁进入ycy,只能等待了。

说完了这两个锁,再回到我们的单例模式,我们可以发现,如果此时系统中有别的static synchronized,那么获取单例时也会将相对应的别的资源锁住,同时单例模式方法内部所有代码在一把锁中,锁的粒度也有点大,为了解决这些问题,我们引入了volatile。

volatile解决线程安全问题
private static volatile SingletonDemo singletonDemo;

public static  SingletonDemo getSingletonDemo(){
    if(singletonDemo==null){
        synchronized (SingletonDemo.class) {
        if(singletonDemo==null){
           singletonDemo = new SingletonDemo();
           System.out.println("创建单例");
        }
    }
    return singletonDemo;
}

借助volatile,我们可以进一步细化锁的粒度,同时注意到此时一旦单例创建成功,后续获取单例操作都不需要再进入锁,这会大大减小系统开销。这一切都要归功于我们增加了volatile。

说volatile之前,我们先来了解下重排序。现代编译器为了编译效率等种种因素考虑,并不是按行编译指令的,而是对指令在不改变单线程程序语义的情况下进行了重排序,看明白了吧,单线程中没啥影响,多线程就不一定了。如果我们不使用volatile,可能会发生如下过程:

memory=allocate();    // 分配对象内存空间
ctorInstance(memory);  // 初始化对象
singleton=memory;   // 将singleton指向刚分配好的空间

这是正常的执行顺序,可能就会被重排序为如下

1 emory=allocate();    // 分配对象内存空间
2 singleton=memory;   // 将singleton指向刚分配好的空间
3 ctorInstance(memory);  // 初始化对象

如果说线程A执行到2,此时线程B执行if(singletonDemo==null),就会判定singleton不为空,接着 return singleton,return了一个空的值,Omg!这显然不可行。所以我们需要避免重排序,volatile登场了。

每个线程都有其对应的本地缓存,volatile变量修饰的变量在写的时候会被立即刷新到主内存,其他线程的该变量此时都置为无效,其他线程若是想读取该变量,就只能去主内存读取最新的。这也就解决了上述问题,所以可以保证线程安全。关于volatile,限于篇幅,留到下篇吧。

总结

通过单例模式回顾了下synchronized的相关场景用法,注意static synchronized 是全局锁,sunchronized事是实例锁,关于volatile,下篇见吧~

公众号 程序员二狗

每日原创文章 一起交流学习

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值