今天不学习,明天变垃圾!
本文主要内容是:多线程案例中的【单例模式】。
饿汉模式:类加载的同时, 创建实例。
懒汉模式:类加载的时候不创建实例, 第一次使用的时候才创建实例。
懒汉和饿汉模式,谁才是线程安全的?
答:懒汉模式线程不安全,饿汉模式线程安全。
① 考虑某个模式是否线程安全,本质上是在考虑多个线程同时调用getInstance的时候是否会有问题。
② 饿汉模式获取实例getInstance的操作只是单纯的“读数据”,不涉及到修改,因为饿汉模式在类加载的时候就已经创建好实例对象了。
③ 懒汉模式获取实例getInstance的操作既涉及到读,又涉及到修改,此时线程就是不安全的。
那么如何修改懒汉模式的代码使得该模式下线程安全呢?
答:加锁,把多个操作打包成一个原子操作。【注意锁对象:该类的类对象.class】
我们主要是想要t2线程的读数据load操作在t1线程new实例之后,所以加锁是加在if之外。但是这种直接加锁的方式可能会导致后续线程安全的时候还在继续加锁;而加锁的开销其实还是挺大的,加锁可能会涉及用户态到内核态之间的切换,这里的切换成本是比较高的。
那么加锁如何在在需要的时候加,在不需要的时候就不加呢?
答: 实例没有创建之前是线程不安全的,需要加锁;而在实例创建之后线程就是安全的,不需要加锁。
因此:就在加锁的外层再加一层判定条件,符合条件才加锁。
new操作本质上大致可以分为三个步骤:
① 申请内存,得到内存首地址;
② 调用构造方法来初始化实例
③ 把内存首地址赋值给instance引用
- 在单线程的角度下,②③操作的顺序是可以进行调换的,执行谁效果都一样
- 假设此处触发了指令重排序,并且按照① ③ ②的顺序执行,那就有可能在线程1在执行了①和③之后、执行②之前,线程2调用了getInstance方法,此时线程就是不安全的:因为调用了①③的时候其实并没有真正初始化实例对象,得到的是不完全对象,只是有内存,但是内存上的数据无效;而在线程2拿到实例的时候就会以为是已经创建了,实例对象为非空,并且可能会针对instance对象解引用操作来使用里面的属性/方法,这是不行的。
——其实上述问题就是指令重排序造成的问题
那么如何解决指令重排序带来的问题呢?
答:办法就是:禁止指令重排序。那么如何禁止呢?就是使用volatile关键字,既能保证内存可见性(读、修改线程并发,但是其实细想是不会存在的:因为每个线程有各自工作的一套CPU寄存器,有各自的上下文),又能禁止指令重排序(避免得到不完全对象,内存数据无效)。
【经典面试题】
① synchronized锁加在哪里?
② 两层if是什么意思?
③ 为什么volatile不能少?
理解双重 if 判定 / volatile:
① 加锁 / 解锁是一件开销比较高的事情. 而懒汉模式的线程不安全只是发生在首次创建实例的时候。
因此后续使用的时候, 不必再进行加锁了。
② 外层的 if 就是判定下看当前是否已经把 instance 实例创建出来了。
③ 同时为了避免 “内存可见性” 导致读取的 instance 出现偏差, 于是补充上 volatile 。
④ 当多线程首次调用 getInstance, 大家可能都发现 instance 为 null, 于是又继续往下执行来竞争锁, 其中竞争成功的线程, 再完成创建实例的操作。
⑤ 当这个实例创建完了之后, 其他竞争到锁的线程就被里层 if 挡住了, 也就不会继续创建其他实例。
⑥ 这样会降低操作开销。
【单例的延伸】如何让单例模式反射安全以及序列化安全?(冷门八股,可以自行了解)
其实面试题答案大多可以在参考代码中找到。
Demo1-2
注意要看每次提交所修改的!!不单只是看代码,看每次提交过程的代码!