一、wait 和 notify
wait notify 是两个用来协调线程执行顺序的关键字,用来避免"线程饿死"的情况。
wait 和 notify 其实都是 Object 这个类的方法,而 Object这个类是所有类的"祖宗类",也就是说明,任何一个类,都可以调用wait 和notify这两个方法。

Java标准库中,涉及到阻塞的方法,都可能抛出InterruptedException


让我们来观察一下这个异常的名字。
Illegal:非法的,不正确的,不合理的(而不是违反法律的)、
Monitor:监视器/显示器(电脑的显示器,英文不是screen,而是Monitor)此处的Monitor指的是synchronized,synchronized在JVM里面的底层实现,就被称为"监视器锁"(JVM源码,变量名是Monitor相关的词)
所以这个异常的意思是,当前处于非法的锁状态。
众所周知,锁一共有两种状态,一种是加锁,一种是解锁。

wait方法内部做的第一件事情,就是释放锁。
而我们必须要先得到锁,才能去谈释放锁。因此,wait必须放到synchronized代码块内部去进行使用。

此处的阻塞会持续进行,直到其他线程调用notify把该线程进行唤醒。

此处的阻塞会持续进行,直到其他线程调用notify,将该线程进行唤醒。
java
package Thread;
import java.util.Scanner;
public class demo28 {
// 将 object 变量移到类内部并添加 static 修饰符
public static Object object = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
synchronized (object) {
System.out.println("t1 wait之前");
try {
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t1 wait之后");
}
});
Thread t2 = new Thread(() -> {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入任意内容 尝试唤醒t1");
scanner.next();
synchronized (object) {
object.notify();
System.out.println("t2 notify之后");
}
});
t1.start();
t2.start();
}
}
输出:


图上这四处地方的锁,必须是同一个对象。

假设notify后面又有一堆别的逻辑,此时,这个锁就会再多占有一会。
【总结】wait要做的事情:
1、使当前执行代码的线程进行等待(把线程放到等待队列中去)
2、释放当前的锁
3、满足一定条件的时候被唤醒,并且重新尝试获取这把锁
(这三个步骤是同时进行的)
使用wait的时候,阻塞其实是有两个阶段的:
1、WAITING的阻塞:通过wait 等待其他线程的通知
2、BLOCKED阻塞:当收到通知之后,就会重新尝试获取这把锁。重新尝试获取这把锁,很可能又会遇到锁竞争
wait进行阻塞之后,需要通过notify唤醒。默认情况下,wait的阻塞也是死等。
这样子是不合理的,因此,我们在工作中需要设定等待时间上限。(超过时间)


括号里的等待时间是毫秒
java
package Thread;
import java.util.Scanner;
public class demo29 {
public static Object locker = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
synchronized (locker) {
System.out.println("t1 wait之前");
try {
locker.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t1 wait之后");
}
});
Thread t2 = new Thread(() -> {
synchronized (locker) {
System.out.println("t2 wait之前");
try {
locker.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t2 wait之后");
}
});
Thread t3 = new Thread(() -> {
synchronized (locker) {
System.out.println("t3 wait之前");
try {
locker.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t3 wait之后");
}
});
Thread t4 = new Thread(() -> {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入任意内容 尝试唤醒t1、t2、t3");
scanner.next();
synchronized (locker) {
System.out.println("t4 notify之前");
locker.notify(); // 唤醒一个在 locker 上等待的线程,这里是 t1
System.out.println("t4 notify之后");
}
});
t1.start();
t2.start();
t3.start();
t4.start();
}
}
输出:

可以看出,当前只是将t1唤醒了
再次尝试

唤醒的仍然是t1
咱们在多线程中谈到的"随机"其实不是数学上概率均等的随机,这种随机的概率是无法预测的。取决于调度器如何去调度。调度器里面,其实不是"概率均等的唤醒",调度器内部也是有一套规则的。这套规则,对于程序员是"透明的",程序员做的,就是不能依赖于这里的状态。
mysql的时候,select查询一个数据,得到的结果集,是按照怎样的顺序的呢?(是按照id的顺序,时间的顺序,排列的顺序的吗?)都不是,mysql就没有这样的承诺。必须加上orderby
notifyAll可以唤醒全部:

如果没有任何对象在wait,那么直接调用notify / notifyAll 会发生什么?
不会发生任何事情,直接凭空调用notify是没有任何副作用的

经典面试题:
请你谈一谈sleep 和 wait 的区别
1.wait 的设计就是为了提前唤醒。超时时间,是"后手"(B计划)
sleep 的设计就是为了到达时间再进行唤醒。虽然也可以通过Interrupt()进行提前唤醒,但是这样的唤醒是会产生异常的。(此处的异常表示:程序出现不符合预期的情况,才称为"异常")
2.wait需要搭配锁来时进行使用,wait执行时会先释放锁
sleep不需要搭配锁进行使用,当把sleep放到synchronized内部的时候,不会释放锁(抱着锁睡觉)
综上所述,在实际开发中,wait比sleep用的更多。
二、单例模式
单例模式是一种设计模式,校招中最常考到的设计模式之一。
为了使得新手的代码下线也能够有所保证,大佬们研究出了一些"设计模式",用来解决一些固定的场景问题,这些问题有着固定的套路。
如果按照设计模式写,能够得到一个较为靠谱的代码,属于是一种软性要求。
设计模式有很多很多种类,不仅仅有23种。
单例模式 :单例,也就是单个实例(单个对象)。虽然一个类,在语法角度来说,是可以无限创建实例的,但是在实际的场景当中,可能有时候我们只希望这个类只有一个实例(例如JDBC)
那么,在Java代码中,如何实现单例模式呢?------有很多种实现方式,其中最主要的有两种模式:
1、饿汉模式
创建实例的时机是非常紧迫的。
由于此处的Instance是一个static 成员,创建时机,就是在类加载的时候。也就是说,程序一启动,实例就被创建好了。

java
package Thread;
class Singleton{
private static Singleton instance = new Singleton();
public static Singleton getInstance(){
return instance;
}
//做了一个"君子协定",让其他类不能new这个类,只能通过getInstance()方法获取这个类的实例。
private Singleton(){
}
}
public class demo33 {
public static void main(String[] args) {
Singleton instance1 = Singleton.getInstance();
Singleton instance2 = Singleton.getInstance();
System.out.println(instance1 == instance2);
}
}
做了一个"君子协定",让其他类不能new这个类,只能通过getInstance()方法获取这个类的实例。
2、懒汉模式

第一次使用这个实例的时候,才会创建这个实例,创建的时机更晚
上述两份代码,哪一份是线程安全的,哪一份是线程不安全的呢?
而懒汉模式容易因此下述问题:

最终只创建了一个实例!
实际开发中,单例类的构造方法可能是一个非常重量的方法。我们之前,代码中也有单例模式的使用。当时通过单例类,管理整个服务器程序所以来的所有数据(100G)。这个实例创建的过程,就会从硬盘上把100G的数据加载到内存当中。
那么,如何解决该问题呢?
我们可以通过加锁,将操作打包成原子的来解决该问题。

但是,这串代码仍然存在问题:逻辑上来看,我们只是在第一次调用的时候,才会涉及到线程安全问题,只要对象创建完毕,后序都是直接return了,就不涉及修改了。但是,此时这个代码,锁是每次调用都会加上的。明明已经线程阿耐庵了,但是还要再进行加锁,这并不合理。

图中,一摸一样的条件连续写了两遍。以前都是一个线程,这个代码执行下来,第一次判定和第二次判定,结论是一定相同的。
而现在是多线程,第一次判定和第二次判定,结论可以不一样。因为再第一次和第二次判定之间,可能有另外一个线程,修改了instance。多线程,打破了以前的认知。而后面学习的网络,EE进阶里面的框架,也会打破以前的认知。
在多线程当中,指令重排序容易引起线程安全问题。指令重排序是编译器优化的一种手段,这是编译器在确保逻辑一致的情况下,为了提高效率,调整代码的顺序,就可以让效率变高了。然而,指令重排序在遇见多线程就又出现问题了。

此处涉及到的指令是非常多的,为了简化这个模型,我们将他抽象成三个步骤:
1、申请内存空间
2、在内存空间上进行初始化(构造方法)
3、内存地址,保存到引用变量当中

在多线程中,由于指令重排序,容易引起上述问题。
Instance里面还没有任何的属性方法,但是已经被线程2拿去使用了!
那么,如何避免指令重排序的问题呢?

只需要加上volatile这个关键字。
volatile的意思是,针对这个变量的读写操作,不要触发优化。
