对于线程执行最大的问题就是随机调度,抢占式执行,对于程序猿来讲,是不喜欢这种随机性的,程序猿喜欢确定的东西,于是就有了一些方法,可以控制线程之间的执行顺序,虽然线程在内核里调度是随机的,但我们可以通过一些 api 让线程主动阻塞等待,主动放弃 CPU 给其他线程让路呀!
就比如说,在地铁上,张三看到一位老人上地铁了,主动让座,老人坐了一会,起身对小伙说,我还有一站就到了,你来坐着吧,我站一会就下车了。
这是不是就像线程1正在占用 CPU 资源了,突然线程2开始工作了,于是线程1就让线程2先去工作,等线程2工作差不多了,在通知线程1可以工作了。
在实际开发中,很多时候线程之间是需要相互配合的。
比如篮球哥喜欢打篮球,篮球里,一个队伍五个人,小前锋,大前锋,中锋,后卫,分位,这 5 个角色就像 5 个线程,如果这 5 个人都争这一个球,那这个的队伍就没有配合性,必定会输球。
如果这 5 个人打好配合,先谁持球,然后接着执行什么战术,有合理的战术安排,此时球就能很好的在这 5 个人的手里运作起来,进球的概率也就大大提升。
再比如,球员a 先持球过半场,传球给球员b,球员b接球就投,球进了!
此时是不是就需要 a 先拿球过半场啊,等 a 过了半场,在传球给 b ,在 a 没有传球之前 b 是不能拿到球的!
也就是线程1没有执行到一定阶段,线程2是不能工作的!
对于完成上述的配合操作,主要涉及到三个方法:
wait() / wait(long timeout)
notify / notiryAll()
此处的方法都是 Object 类中的方法,Object 类是所有类的父类,所以所有对象都有上述方法。
后续的内容也是围绕上述方法进行展开。
当某个对象调用 wait 方法时,wait 会做如下三件事:
wait 使当前执行代码的线程进行等待(把线程放到阻塞队列中)
释放当前的锁
满足一定条件时被唤醒,重新尝试获取这个锁
由于 wait 执行时会释放当前的锁,所以调用 wait 的时候需要先获取到锁,即 wait 需要搭配 synchronized 使用。
wait 的结束条件(满足一个即可):
其他线程调用该对象的 notify 方法
wait 超时等待(wait 方法提供一个带有 timeout 参数的版本, 来指定等待时间)
其他线程调用该线程的 interrupted 方法,导致 wait 抛出 InterruptedException 异常
public static void main(String[] args) {
Object object = new Object();
Thread t1 = new Thread(() -> {
synchronized (object) {
System.out.println("开始等待!!!");
try {
object.wait();
System.out.println("等待结束!!!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
}
显然上述这个代码是一个 "死等",因为没有触发上述 wait 结束条件的任意一个,所以 t1 线程会无止境的等待下去:
通过 jconsole 工具也能发现,t1 线程始终处于 WAITING 状态!
如何让 wait 结束,那么只要满足上述所说的三个 wait 结束条件即可。
notify 的作用是唤醒等待的线程
notify 这个方法也要在同步代码块或同步方法中执行(被synchronized 修饰),notify 用于通知哪些可能等待该对象锁的其他线程,并使他们重新获取该对象的锁。
如果有多个线程等待该对象的锁, 则由线程调度器随机挑选出一个呈 wait 状态的线程,并不会采取先来后到的机制。
notify 方法后,当前线程不会马上释放该对象锁,要等到执行完 notify 所处被 synchronized 修饰的代码块执行结束,才能释放对象锁!
注意,通过指定对象调用 wait() 进入 WAITING 状态的线程,只有指定对象调用 notify 唤醒后(特殊情况除外),该线程才能尝试获取锁,接着往下执行!
notify 就好像一个妈妈(指定对象),妈妈手上拿着一块小蛋糕,有三个小朋友在桌子旁边坐着等(妈妈.wait),妈妈随机喊了一个小朋友,让他来吃蛋糕(妈妈.notify),但是妈妈并没有把蛋糕放下(没有结束对应代码块,也就是还未释放锁),当妈妈把蛋糕放在桌子上(锁被释放),这个小朋友才能去吃蛋糕(获取到锁)。
此时有了上述知识,我们就可以实现下上述图中吃蛋糕的场景了(为了代码简洁,我们只设定两个线程来等待被唤醒):
public static void main(String[] args) throws InterruptedException {
Object object = new Object();
Thread t1 = new Thread(() -> {
synchronized (object) {
System.out.println("张三进入 WAITING 状态");
try {
object.wait();
System.out.println("张三吃到蛋糕了!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread t2 = new Thread(() -> {
synchronized (object) {
System.out.println("李四进入 WAITING 状态");
try {
object.wait();
System.out.println("李四吃到蛋糕了!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
t2.start();
Thread.sleep(10); // 保证两个线程都进入到 WAITING 状态
synchronized (object) {
object.notify();
}
}
可能大家多次测试上述代码后,发现一直都是张三吃到了蛋糕啊,但是其实这个是随机的,因为 CPU 就是随机调度的,这个咱们就没必要钻牛角尖了,实在要钻,可以创建线程池(后续讲),搞一堆线程进行测试即可。
此时问题来了,当释放锁了之后,也就是妈妈把蛋糕放在桌子上了,此时被唤醒的线程是可以去拿蛋糕的,但是有没有可能释放锁的瞬间,被其他处在 RUNNABLE 状态的线程给劫持了呢?(其他线程也来竞争这把锁) 也就是突然冲进来了一条小狗,把蛋糕给抢到了,其实是有这种情况的:
public static void main(String[] args) throws InterruptedException {
Object object = new Object();
Thread t1 = new Thread(() -> {
synchronized (object) {
System.out.println("张三进入 WAITING 状态");
try {
object.wait();
System.out.println("张三吃到蛋糕了!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread t2 = new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (object) {
System.out.println("小狗把蛋糕抢走了!");
while (true) {} // 吃蛋糕
}
});
t1.start();
t2.start();
Thread.sleep(1000); // 保证两个线程都进入到 WAITING 状态
synchronized (object) {
object.notify();
Thread.sleep(1000); // 唤醒 t1 但并没有立即释放锁, 休眠 1s 再释放
}
}
上述代码 main 线程等待 1000 毫秒后唤醒 t1 线程,此时 t1 被唤醒,就会重新尝试获取 object 对象锁,但是 t2 休眠了 1000 毫秒后,也想获取 object 对象锁。
唤醒 t1 之后,过了 1000 毫秒,也就意味着锁被释放,此时 t1 和 t2 都想获取到 object 对象锁,那究竟谁能获取到呢?这完全是随机的!比如下面的测试结果:
所以是有可能别半路截胡的,罪魁祸首还是因为随机调度,抢占式执行呀!所以以后在写多线程代码的时候一定要多多注意,要让每种执行顺序得到的结果都是一样的,这才是好的代码!
关于 notifyAll :
notifyAll 和 notify 非常相似,假设 5 个线程等待 object 对象唤醒,然后 object.notifyAll(),就会将这 5 个线程全部唤醒,然后这 5 个线程竞争 object 对象锁,没竞争到的,就继续进入 WAITING 状态。
一定要弄清楚是谁在等被谁唤醒!
如果 t1 里面调用 o1.wait(),那么只有其他线程调用了 o1.notify() 才能唤醒 t1,如果是其他线程调用 o2.notify(),是不能唤醒 t1 的!因为 t1 线程是在等 o1 唤醒!
而 o1 也只能唤醒在等他的线程,比如 t3 在等 student 唤醒,那调用 o1.notify() 是不能唤醒 t3 的,只能调用 studnet.notify() 才能唤醒 t3。
归根到底,我们一定要弄清楚,线程在等谁,也要弄清楚,这个对象,有哪些线程在等他的唤醒!
这里举两个例子来演示一下:
public static void main(String[] args) throws InterruptedException {
Object o1 = new Object();
Object o2 = new Object();
Thread t = new Thread(() -> {
synchronized (o1) {
try {
o1.wait();
System.out.println("t 线程被 main 线程唤醒!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
Thread.sleep(10); // 保证 t 线程进入 WAITING 状态
synchronized (o2) {
o2.notify();
System.out.println("执行完 o2.notify!");
}
}
这段代码,t 线程在等待 o1 对象唤醒,所以 main 线程中 o2.notify() 是在唤醒等待 o2 的线程,显然没有线程在等待 o2 唤醒,所以空打一枪,然而 t 线程仍然处在 WAITING 状态。
如果对应对象 notify 的时候,没有线程在等待这个对象唤醒呢?那么就是无效唤醒,也没有什么副作用,所以我们以后写代码的时候还是要尽量保证先执行 wait 在执行 notify 才是有意义的,也就是在 notify 的时候,有线程在等待这个对象唤醒!
wait 的带参数版本,指定了最大的等待时间,看起来和 sleep 有点像,但是还是有本质区别的。
notify 唤醒 wait 的时候,是不会有任何异常的(正常的业务逻辑)
interrupt 唤醒 sleep 的时候,则是会抛出一个异常(表示逻辑出现了问题)
其实从理论上,wait 和 sleep 是没得比的,wait 是线程之间的通信,互相配合,而 sleep 是单纯让线程阻塞一段时间,唯一的相同点就是都可以让线程放弃 CPU 的调度一段时间而已。
wait 是需要搭配 synchronized 使用的,sleep 则不需要
wait 是 Object 的方法,而 sleep 是 Thread 的静态方法
下期预告:【多线程】单例模式