Object 中的 wait,notify,notifyAll, 可以用于线程间的通信, 核心原理为借助于监视器的入口集与等待集逻辑
通过这三个方法完成线程在指定锁 (监视器) 上的等待与唤醒, 这三个方法是以锁 (监视器) 为中心的通信方法
除了他们之外, 还有用于线程调度, 控制的方法, 他们是 sleep,yield,join 方法, 他们可以用于线程的协作, 他们是围绕着线程的调度而来的
sleep 方法
有两个版本的 sleep 方法, 看得出来, 核心仍旧是 native 方法
非 native 方法只是进行了参数校验, 接着仍旧是调用的 native 方法, 这个情形与 wait 是类似的
接下来仔细看下, native 版本的 sleep
在指定的毫秒数内让当前正在执行的线程休眠(暂停执行), 此操作受到系统计时器和调度程序精度和准确性的影响. 该线程不丢失任何监视器的所属权.
注意:
sleep 不会释放锁, 不会释放锁, 不会释放锁
可以理解为他进入监视器这个房间之后, 在这房间里面睡着了
与 wait 类似的是, sleep 也是可中断方法(从方法签名可以看得出来, 可能抛出 InterruptedException), 也就是说如果一个线程正在 sleep, 如果另外的线程将他中断(调用 interrupt 方法), 将会抛出异常, 并且中断状态将会擦除
所以对于 sleep 方法, 要么自己醒来, 要么被中断后也会醒来
对于 sleep 始终有一个超时时间的设置, 所以, 尽管他是在监视器内睡着了, 但是并不会导致死锁, 因为他终究是要醒来的
如下, 线程休眠 500 毫秒, 主线程 50 毫秒打印一次状态
ps:sleep 方法的调用结果为状态: TIMED_WAITING
借助于 sleep 方法, 可以模拟线程的顺序执行
比如下面示例, 两个阶段, 第二个阶段将在第一个阶段执行之后才会执行
- package test1;
- import java.lang.Thread.State;
- public class T16 {
- public static void main(String[] args) {
- // 模拟执行任务的第一个阶段的执行
- Thread stepOne = new Thread(() -> {
- System.out.println(Thread.currentThread().getName()+": 第一阶段任务开始执行");
- try {
- Thread.sleep(1000);
- System.out.println(Thread.currentThread().getName()+": 第一阶段任务执行结束");
- } catch (InterruptedException e) {
- }
- }, "firstStage");
- stepOne.start();
- // 模拟任务第二个阶段的执行
- Thread stepTwo = new Thread(() -> {
- while (!State.TERMINATED.equals(stepOne.getState())) {
- try {
- Thread.sleep(100);
- System.out.println(Thread.currentThread().getName()+": 我在等待第一阶段任务执行结束");
- } catch (InterruptedException e) {
- }
- }
- System.out.println(Thread.currentThread().getName()+": 第二阶段任务执行结束");
- }, "secondStage");
- stepTwo.start();
- }
- }
另外, 你应该已经注意到 sleep 方法都有 static 修饰, 既然是静态方法, 在 Thread 中的惯例就是针对于: 当前线程, 当前线程, 当前线程
yield 方法
对于 sleep 或者 wait 方法, 他们都将进入特定的状态, 伴随着状态的切换, 也就意味着等待某些条件的发生, 才能够继续, 比如条件满足, 或者到时间等
但是 yield 方法不涉及这些事情, 他针对的是时间片的划分与调度, 所以对开发者来说只是临时让一下, 让一下他又不会死, 就只是再等等
yield 方法将会暂停当前正在执行的线程对象, 并执行其他线程, 他始终都是 RUNNABLE 状态
不过要注意, 可以认为 yield 只是一种建议性的, 如果调用了 yield 方法, 对 CPU 时间片的分配进行了 "礼让", 他仍旧有可能继续获得时间片, 并且继续执行
所以一次调用 yield 并不一定会代表肯定会发生什么
借助于 while 循环以及 yield 方法, 可以看得出来, 也能一定程度上达到线程排序等待的效果
yield 也是静态方法, 所以, 也是针对于当前线程, 当前线程, 当前线程.
join 方法
三个版本的 join 方法
方法的实现过程, 与 wait 也是非常类似, 下面两个版本的方法一个调用 join(0), 一个参数校验后, 调用 join(millis), 所以根本还是单参数版本的 join 方法
在方法深入介绍前先看个例子
一个线程, 循环 5 次, 每次 sleep 1s, 主线程中打印信息
从结果可以看到, 主线程总是在线程执行之后, 才会执行, 也就是主线程在等待我们创建的这个线程结束, 结束了之后才会继续进行
如果调整下顺序 --->start 与 join 的先后顺序, 再次看下情况, 可以发现顺序没有保障了
结论:
主线程 main 中调用启动线程(调用 start), 然后调用该线程的 join 方法, 可以达到主线程等待工作线程运行结束才执行的效果, 并且 join 要在 start 调用后
如何做到的?
从上面源代码可以看得出来, 内部调用了 wait 方法, 所以也能明白为啥 join 也会抛出 InterruptedException 了吧
主线程 main 中调用 thread.join()方法, join 方法相当于 join(0), 也就是
- while (isAlive()) {
- wait(0);
- }
而这个 wait(0)就相当于是 this.wait(0),this 就是我们自己创建的那个线程 thread, 看看方法的签名是不是有一个 synchronized
isAlive()也是 this.isAlive(), 也就是如果当前线程 alive(已经启动, 但是未终止), 那么将持续等待, 等待的临界资源就是我们创建的这个线程对象本身
所以这两行代码的含义就是:
该线程是否还存活? 如果存活, 调用 join 的那个线程将会在这个对象上进行等待(进入该线程对象的等待集)
也就是说调用一个线程的 join 方法, 就是在这个线程是等待, 这个线程对象就是我们的锁对象(不要疑惑, Object 都可以作为锁, Thread 实例对象怎么不可以?)
肯定大家很奇怪, 既然是等待, wait 又不会自己醒来, 那不是出问题了吗?
其实线程结束后, 会调用 this.notifyAll, 所以主线程 main 会被唤醒
如果传递的参数不为 0, 将会走到下面的分支, 会 wait 指定时长, 与上面的逻辑一致, 只不过是有指定超时时长而已
- long delay = millis - now;
- if (delay <= 0) {
- break;
- }
- wait(delay);
- now = System.currentTimeMillis() - base;
手动版本的等待结束
只是将 join 方法换成了同步代码块, 锁对象为那个线程的实例对象 thread, 调用他的 wait 方法
从结果上看, 效果一样
(不过此处没有持续监测 isAlive(), 所以一旦主线程醒来, 即使线程没有结束, 也会继续, 不能百分百确保 main 肯定等待线程结束)
不过要注意: 注释中有说明, 自己不要使用 Thread 类的实例对象作为锁对象, 如果是现在这种场景, 使用 join 即可
为什么? 从我们目前来看, join 方法就是以这个对象为锁, 如果你自己在使用, 又是 wait 又是 notify(notifyAll)的, 万一出现什么隐匿的问题咋办?
所以 join 方法的原理就是: 将指定的 Thread 实例对象作为锁对象, 在其上进行同步, 只要那个线程还活着, 那么就会持续等待(或者有限时长)
线程终止之后会调用自身 this.notifyAll, 以通知在其上等待的线程
简单说, 只要他活着大家就都等着, 他死了会通知, 所以效果就是在哪里调用了谁的 join, 哪里就要等待这个线程结束, 才能继续
为什么要在 start 之后?
如上面所示, 将 join 改造成同步代码块如下所示, 如果这段同步代码在 start 方法之前
看下结果, 没有等待指定线程结束, main 主线程就结束了
因为如果还没有调用 start 方法, 那么 isAlive 是 false(已开始未结束), 主线程根本就不会等待, 所以继续执行, 然后继续到下面的 start, 然后主线程结束
所以, 为什么 join 方法一定要在 start 之前?
就是因为这个 isAlive 方法的校验, 你没有 start,isAlive 就是 false, 就不会同步等待, 所以必须要先 start, 然后才能 join
小结:
对于 join 方法, 有两个关键:
调用的哪个对象的 join?
在哪里调用的?
换一个说法:
join 的效果是: 一个线程等待另一个线程 (直到结束或者持续一段时间) 才执行, 那么谁等待谁?
在哪个线程调用, 哪个线程就会等待; 调用的哪个 Thread 对象, 就会等待哪个线程结束;
状态图回顾
在回顾下之前状态一文中的切换图, 又了解了这几个方法后, 应该对状态切换有了更全面的认识
总结
对于 yield 方法, 比较容易理解, 只是简单地对于 CPU 时间片的 "礼让", 除非循环 yield, 否则一次 yield, 可能下次该线程仍旧可能会抢占到 CPU 时间片, 可能方法调用和不调用没差别
sleep 是静态方法, 针对当前线程, 进入休眠状态, 两个版本的 sleep 方法始终有时间参数, 所以必然会在指定的时间内苏醒, 他也不会释放锁, 当然, sleep 方法的调用非必须在同步方法 (同步代码块) 内
join 是实例方法, 表示等待谁, 是用于线程顺序的调度方法, 可以做到一个线程等待另外一个线程, join 有三个版本, 指定超时时间或者持续等待直到目标线程执行结束, join 也无需在同步方法 (同步代码块) 内
sleep 和 join 都是可中断方法, 被其他线程中断时, 都会抛出 InterruptedException 异常, 并且会醒来
join 方法底层依赖 wait, 我们对比下 wait 与 sleep
wait 和 sleep 都会使线程进入阻塞状态, 都是可中断方法, 被中断后都会抛出异常
wait 是 Object 的方法, sleep 是 Thread 的方法
wait 必须在同步中执行, sleep 不需要(join 底层依赖 wait, 但是不需要在同步中, 因为 join 方法就是 synchronized 的)
wait 会释放锁, sleep 不会释放锁
wait(无超时设置的版本)会持续阻塞, 必须等待唤醒, 而 sleep 必然有超时, 所以一定会自己醒来
wait 实例方法(Object), 在对象上调用, 表示在其上等待; sleep 静态方法, 当前线程
来源: https://www.cnblogs.com/noteless/p/10443446.html