目录
前言
1、 wait和notify
1.1、wait()方法
1.2、notify()方法
1.3、wait和sleep 的对比
2、单例模式
2.1、饿汉模式
2.2、懒汉模式
2.3、上述懒汉模式和饿汉模式在多线程情况下是否安全
2.3.1、解决懒汉模式多线程不安去问题
前言
- 这里补充一下上一个博客中的volatile和内存可见性的知识点。网上有些资料在说内存可见性的时候,会说t1线程在频繁读取主内存,效率比较低,就被优化成直接读自己的工作内存,t2修改了主内存的结果,由于t1没有读主内存,导致修改不能被识别到。上述中的工作内存就相当于我们说的CPU寄存器,主内存就是内存。
- 主内存和内存这一说法来自于jvm规范文档(官方说法),为什么要这么说,而不使用我们采用的说法,原因在于Java的跨平台性使Java能够1.兼容多种操作系统,2.兼容多种硬件设备,尤其是CPU。不同的硬件设备之间的差别很大,就以CPU为了,以前的CPU上面只有寄存器,而现在的CPU上不仅有寄存器还有缓存(现在常见的CPU都是3级缓存 L1,L2,L3).
- 所以工作内存准确来说,表示的就是CPU寄存器+缓存(CPU内部存储数据的空间)。
- 了解一下缓存的知识点:
- 缓存有三个( L1,L2,L3),L1最小,但是L1读数据的速度最快(比寄存器慢);L3最大,但是读数据的速度最慢(比内存快很多)
- 缓存读取数据的速度介于寄存器和内存之间。(寄存器>缓存>内存>硬盘)。
- 实际上CPU尝试读一个内存数据的步骤为:
- 先看看寄存器里有没有
- 寄存器中没有,再看L1里有没有,
- L1中没有,再看L2中有没有
- L2中没有,再看L3中有没有
- L3中没有,再看内存中有没有。
这样做的目的就是为了让程序更快,上述虽然读了很多次,但是寄存器读取数据的速率是内存的几千倍,几万倍,缓存也是同样的道理,所以比起读内存还是快很多。
1、 wait和notify
方法 | 说明 |
wait() | 就是让某个线程暂停下来,等一等 |
wait(timeout) | 参数表示等待最长时间,到这个时间了就会自动唤醒 |
notify() | 唤醒等待的线程 |
notifyAll() | 唤醒等待同一个对象的多个线程 |
❗❗❗注意:wait、notify和notifyAll都是Object类的方法。只要是一个类对象(不是内置类型),都可以使用wait/notify.
由于线程的调度是无序的,随机的,但是有些场景需要线程是有序执行的,我们的wait和notify就是控制线程执行顺序的一种方式,就像之前的join也是一种控制线程执行顺序的一种方式。
1.1、wait()方法
wait主要做三件事
- 使当前执行代码的线程阻塞等待(把线程放到等待队列中)
- 释放当前的锁
- 满足一定条件是被唤醒,重新尝试获取锁。
❗❗❗注意:wait要搭配synchronized来使用,脱离了synchronized使用wait会直接抛出异常。
1️⃣代码没有加锁,直接使用wait方法,代码抛异常
public class ThreadDemo15 {
public static void main(String[] args) throws InterruptedException {
Object object = new Object();
System.out.println("wait 之前");
object.wait();
System.out.println("wait 之后");
}
}
上述代码就会抛出非法的锁状态异常
2️⃣正确的写法就是wait搭配synchronized使用。
public class ThreadDemo15 {
public static void main(String[] args) throws InterruptedException {
Object object = new Object();
System.out.println("wait 之前");
synchronized(object){//加锁的锁对象必须和调用wait的对象是同一个。
object.wait();
}
System.out.println("wait 之后");
}
}
❗❗❗注意:使用synchronized(加锁)的时候锁对象必须和调用wait的对象是同一个,notify也是同理,要放在synchronized中
❗❗❗wait结束等待的条件:
- 其他线程调用该对象的notify方法
- wait等待时间超时(wait方法提供一个带有timeout参数的版本,来指定等待时间)
- 其他线程调用该等待线程的interrupted方法,导致wait抛出 interruptedException异常。
1.2、notify()方法
notify方法是唤醒等待的线程
如果有多个线程等待同一个对象,notify会随机唤醒其中一个线程。
public class ThreadDemo16 {
public static void main(String[] args) throws InterruptedException {
Object locker = new Object();
Thread t1 = new Thread(()->{
try {
System.out.println("wait 开始");
synchronized (locker) {
locker.wait();
}
System.out.println("wait 结束");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t1.start();
Thread.sleep(1000);//表示的意思是让t1线程先执行,主线程休眠1s.
Thread t2 = new Thread(()->{
synchronized (locker){
System.out.println("notify 开始");
locker.notify();
System.out.println("notify 结束");
}
});
t2.start();
}
}
可以看到t1先执行,执行到wait了,就阻塞了,1s之后t2开始执行,执行到notify就会通知t1线程唤醒。(notify是在synchronized内部的,就需要t2线程释放了锁,t1才能继续往下执行。)所以wait在被唤醒的时候,会存在阻塞,需要等待t2线程将所释放了,t1才会被唤醒。
在上述的代码中,虽然t1是先执行的,但是可以通过wait notify控制t2先执行一些逻辑。t2执行完之后,notify唤醒t1,t1在继续向下执行。
❗❗❗总结
1️⃣使用wait,阻塞等待会让线程进入Waiting状态
2️⃣唤醒操作,还有一个 notifyAll, 可以有多个线程,等待同一个对象
比如在 t1 t2 t3 中都调用 object.wait ,此时在 main 中调用了 object.notify ——————会随机唤醒上述的一个线程(另外两个仍然是 waiting 状态)
如果是调用了 object.notifyAll, 此时就会把上述三个线程都唤醒,此时这三个线程就会重新竞争锁,然后依次执行
1.3、wait和sleep 的对比
wait有一个带参数的版本,用来体现超市时间,这个时候感觉好像和sleep差不多
1️⃣相同点:都可以提前唤醒(wait可以通过notify提前唤醒,sleep可以通过interrupt提前唤醒)
2️⃣最大的区别是初心不同:wait解决的是线程之间的顺序控制 ,sleep单纯是让当前线程休眠一会
3️⃣使用上存在明显的区别:wait要搭配锁使用,sleep不需要
2、单例模式
单例模型是校招中最常考的设计模型之一。(1、单例模式,2、工厂模式)
单例模式:是指某个类,在进程中只有唯一的一个实例。
设计模型
设计模型就好比象棋中的"棋谱"。马走日字,象走田字,这只是单纯的知道每个棋子该怎样走,而棋谱就是用来教会我们象棋的一些套路,这些套路不能让我们无敌,但是可以让我们将象棋玩的不是很差,而我们这里说到的设计模型和棋谱同理。
单例:表示单个实例,也就是单个对象。表示一个程序中,某个类只能创建出唯一一个实例(对象),不能创建多个对象。
Java中的单例模式,借助Java语法,保证某个类,只能够创建出一个实例,而不能new多次
✨在Java语法中,实现单例模式有很多种写法,我们这里了解两种写法
- 饿汉模式(体现的是急迫)
- 懒汉模式(体现的是从容):效率更高
🎇我们通过这样一个例子来了解懒汉和饿汉:
打开一个硬盘上的文件,读取文件内容,并显示出来。
- 饿汉:把文件所有内容都读到内存中,并显示。
- 懒汉:只把文件读一小部分,把当前屏幕填充上,如果用户翻页了,再读其他文件内容,如果不翻页,就省下了。
假设文件非常大,饿汉模式将文件一次性从硬盘读到内存中,文件打开可能要卡好长时间,但是懒汉模式就可以快速打开,一页的文件占用的内存很小。
2.1、饿汉模式
饿汉模式:在创建类的时候,直接通过new创建出来。
//把这个类设置为单例模式
class Singleton{
//唯一实例的本体
private static Singleton instance = new Singleton();
//被static修饰,该属性是类的属性(被static修饰,属于类对象),JVM中,每个类的类对象只有唯一一
//份,类对象里的这个成员自然也就是唯一一份了。
//获取到实例的方法
public static Singleton getInstance(){
return instance;
}
private Singleton(){
}
}
public class ThreadDemo17 {
public static void main(String[] args) {
//此时s1和s2是同一个对象
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
//机制外部new实例,这时候new的这个对象是不成功的,会编译报错。
Singleton s3 = new Singleton();
}
}
饿汉方式创建单例模式,确实可以实现,但是饿汉模式在创建唯一实例的本体时机太早,只要类一加载就会常见这个实例,但是如果后面我们没有用到这个实例,就会浪费。
2.2、懒汉模式
我们更多的会使用懒汉模式来写单例模式
懒汉模式:核心思想,非必要,不创建。什么时候需要什么时候创建实例。
class SingletonLazy{
private static SingletonLazy instance;
public static SingletonLazy getInstance(){
if(instance == null){
instance = new SingletonLazy();
}
return instance;
}
private SingletonLazy(){ }
}
public class ThreadDemo18 {
public static void main(String[] args) {
SingletonLazy s1 = SingletonLazy.getInstance();
SingletonLazy s2 = SingletonLazy.getInstance();
System.out.println(s1 == s2);
}
}
2.3、上述懒汉模式和饿汉模式在多线程情况下是否安全
根据多线程不安去的原因去分析:
- 多线程的抢占式执行,多线程之间的调度充满随机性。
- 多个线程同时修改同一个变量
- 针对变量的操作不是原子的
- 内存可见性问题
- 指令重排序问题
1️⃣饿汉模式
Thread在主方法中创建实例多线程t1,t2,t3都调用getInstance()方法,他们在并发执行中是否会产生bug?
答案是不会,因为在饿汉模式中getIntance()只存在读取操作,并没有修改的操作。所以饿汉模式在多线程中是线程安全的。
2️⃣懒汉模式
懒汉模式是多线程不安全的。
因为在懒汉模式的代码中getInstance()方法中即存在判断也存在修改操作。
存在的安全性问题一:
存在这个执行过程会导致创建多个SingletonLazy对象,产生上述问题的原因就是没有保证两条语句的原子性。 我们可以通过使用synchronized代码块来包裹这两个语句。
public static SingletonLazy getInstance(){
synchronized (SingletonLazy.class){
if(instance == null){
instance = new SingletonLazy();
}
}
return instance;
}
众所周知加锁会减缓我们程序的运行速度,所以非必要不加锁。加锁之后的代码还存在一点问题,我们在第一次创建SingletonLazy对象时,需要加锁,但是在我们将对象创建好之后,就不需要再加锁了,可以直接读取对象的引用即可。所以在getInstance()方法中还需要再加入一个if判断。
public static SingletonLazy getInstance(){
//这个条件,判定是否要加锁,如果对象已经有了,就不必加锁了,此时直接返回instance
if(instance == null){
synchronized (SingletonLazy.class){
if(instance == null){
instance = new SingletonLazy();
}
}
}
return instance;
}
❓❓❓看到上述代码,有的老铁经过分析,认为两个if判断条件一摸一样,为什么要这样写?将内层的if去掉好像代码也能成立?
❗❗❗可定时不能去掉内层的if的。
- 第一个原因在于,这两个if的作用不一样,设计最外层的if是用来判断是否需要加锁,内层的if是用来判断是否需要创建实例。
- 第二个原因在于,这两个代码的执行时机相差很大,第二个if在锁内,多线程中如果一个线程将外层的if执行完了,但是走到synchronized代码块位置,由于锁竞争,别的线程在执行锁内程序,当前线程就需要阻塞等待,等到当前的线程拿到锁,并执行锁内的代码的时候,其他线程可能已经将SingletonLazy对象已经创建好了,这个时候通过内层的if进行判断,这样就保证不会创建多的SingletonLazy对象。
存在的第二个安全性问题:
getInstance()方法中的instance= new SingletonLazy();会触发指令重排序。
这个操作可以分为三步:
- 创建内存(买房子)
- 调用构造方法/初始化(装修)
- 把内存地址,赋给引用(拿到钥匙)
我们这里2和3的操作顺序是可以颠倒的,先装修在拿钥匙,你拿到的是精装修房。先拿钥匙,在装修你拿到的是毛坯房。
现在有两个线程t1和t2,t1线程执顺序为1,3,2。t1线程刚好将3执行完,还没有执行2(初始化对象),此时t2线程执行了外层的if判断,判断结果为instance不为空,直接拿到了instance引用。t2线程拿到的对象就相当于是一个毛坯房,然后再调用这个对象中的方法进行操作时,产生的结果就会越来越离谱了。
针对第二个问题我们可以对私有变量instance加一个volatile关键字,禁止他进行指令重排序。
volatile private static SingletonLazy instance;
2.3.1、解决懒汉模式多线程不安去问题
这里只展示一下代码,存在的安全问题上面已经解决完了。
package threading;
class SingletonLazy{
volatile private static SingletonLazy instance;
public static SingletonLazy getInstance(){
//这个条件,判定是否要加锁,如果对象已经有了,就不必加锁了,此时直接返回instance
if(instance == null){
synchronized (SingletonLazy.class){
if(instance == null){
instance = new SingletonLazy();
}
}
}
return instance;
}
private SingletonLazy(){ }
}
❗❗❗总结:
单例模型,多线程安全问题。
- 饿汉模型:天然就是安全的,getInstance中只是读操作
- 懒汉模型:不安全,getInstance中不仅存在读操作,还有写操作。
解决懒汉模式的线程不安全的做法,分三步:
- 加锁,把if和new变成原子操作
- 双重if,减少不必要的加锁操作
- 使用volatile禁止指令重排序,保证后续线程肯定拿到完整的对象。