java并发系列(3)——线程协作:wait,notify,join

接上一篇:《java并发系列(2)——线程共享,synchronized与volatile

2.4 线程协作/通信

2.4.1 wait/notify

首先要注意的是:wait,notify,notifyAll 这三个方法跟 sleep,yield,join,interrupt,suspend,resume,stop 这些不一样,后者是 Thread 特有的方法,前者是 Object 的方法。

2.4.1.1 monitor 回顾

在 wait,notify 之前,先回顾一下 monitor。前面在讲 synchronized 时提到 monitor 计数器,以及 monitor 上标记的当前持有锁的线程。这里会再增加一个 wait set。

monitor 持有的信息:

  • 计数器:获得锁或重入锁时,计数器 +1,退出时计数器 -1,计数器为 0 意味着锁没有被任何线程获得;
  • 线程:记录了当前锁被哪个线程所持有;
  • wait set:调用了 wait 方法的线程,会被记录在这里。(每个 Object 都有自己的 monitor,在哪个 Object 上调用 wait,就会被记录到哪个 Object 的 monitor 的 wait set。)
2.4.1.2 wait/notify 的作用

概况地说,wait 方法会使当前线程进入阻塞状态,直到被其它线程使用 notify 或 notifyAll 唤醒。

具体行为如下:

wait:

  • 进入 wait 时:线程释放 monitor 锁,进入阻塞状态,线程被记录到 wait set;
  • 被 notify 时:线程从 wait set 中被清除,进入就绪状态等待线程调度,获得 cpu 使用权开始执行后先竞争 monitor 锁(行为与 synchronized 一样,并且不会比其它线程更优先竞争到锁),所有同步状态恢复到调用 wait 方法之前,然后从 wait 方法正常返回;
  • 被 interrupt 时:线程行为与被 notify 相同,区别是从 wait 方法异常返回(同时 interrupt 状态会被清除);
  • 意外醒来:wait 中的线程有极低的概率在没有超时,没有被 notify,没有被 interrupt 的情况下醒来,这是操作系统导致的,被称为“欺骗性唤醒”。

notify:如果 wait set 中有线程,唤醒 wait set 中的一个线程(具体唤醒哪个线程不可控,JVM 可自由实现);如果 wait set 中没有线程就忽略。

notifyAll:唤醒 wait set 中所有的线程。

2.4.1.3 wait/notify 的标准使用范式
//wait
synchronized (object) {
    while (<condition>) {
        object.wait();
        //object.wait(timeout);
    }
    //...
}

//notify
synchronized (object) {
    //...
    object.notify();
    //object.notifyAll();
}
  • wait,notify,notifyAll 方法都需要先获得锁(在哪个对象上执行方法,就需要获得哪个对象的锁);
  • wait 方法要放在循环里面执行(当被唤醒时检查是否满足放行条件,不满足就继续 wait),防止被意外唤醒;
  • notify 方法尽量在最后执行(要保证 notify 执行完后尽快释放锁),因为 wait 线程被唤醒后需要获得锁,notify 线程如果不释放锁,即使唤醒了 wait 线程,wait 线程也还是会阻塞。

wait,notify 示例代码见《利用wait/notify模拟消息队列》

2.4.1.4 wait,sleep,yield

三者异同:

  • 都会导致线程阻塞(如果 yield 没有被线程调度器忽略);
  • wait 和 sleep 都能被 interrupt;
  • wait 会释放锁,sleep 和 yield 不会(顺便一提,suspend 也不会);
  • wait 除了超时苏醒,还能被唤醒,sleep 只能超时苏醒;
  • wait 由于必须先获得锁,因此会伴随着线程工作内存的刷新,而 sleep 和 yield 都没有规定必须刷新内存。
2.4.1.5 wait 阻塞,sleep 阻塞与 synchronized 阻塞

从操作系统层面讲,都是阻塞状态,阻塞状态的线程都不会被调度。

在 Java 层面,wait 对应的线程状态是 Thread.State.WAITING 或者 Thread.State.TIMED_WAITING,取决于是否设置了超时。

sleep 阻塞对应的线程状态是 Thread.State.TIMED_WAITING,因为 sleep 必须设置超时时间。

synchronized 阻塞对应的线程状态是 Thread.State.BLOCKED,这个状态的阻塞是因为锁竞争而导致的。

所以,wait 可能会产生两种阻塞状态,在醒来之前是 WAITING 或 TIMED_WAITING,醒来之后会竞争锁,如果没有竞争到锁则可能会进入 BLOCKED 状态。

另外,synchronized 未必会导致线程阻塞,即使没有竞争到锁。Java 为了减少线程切换开销,在没有竞争到锁的情况下有可能让线程空跑(自旋状态,类似于跑一个空的死循环)而非进入阻塞状态。

2.4.1.6 notify 与 notifyAll

使用 notify 不会出现问题的情况下尽量使用 notify,而不是 notifyAll。

因为 notifyAll 会唤醒多个线程,唤醒的线程都要竞争锁,必然只有一个线程得到锁,其它被唤醒的线程又会重新进入阻塞状态,增加了不必要的线程切换开销。

适合使用 notify 的情况:

Thread a = new Thread(() -> {
    synchronized (monitor) {
        while (conditionA()) {
            monitor.wait();
        }
        System.out.println("A");
    }
});
Thread b = new Thread(() -> {
    synchronized (monitor) {
        while (conditionA()) {
            monitor.wait();
        }
        System.out.println("A");
    }
});

这两个线程,醒来之后做的事情是一样的,唤醒谁都无所谓,使用 notify 就可以。

如果再加一个这样的线程:

Thread c = new Thread(() -> {
    synchronized (monitor) {
        while (conditionB()) {
            monitor.wait();
        }
        System.out.println("B");
    }
});

这个线程醒来后干的事情不一样,那么使用 notify 唤醒的线程可能不符合预期,这时候就只能用 notifyAll,不符合预期的线程因为不满足退出 while 循环条件会重新 wait。

当然,如果不在乎这点线程切换开销,反而更担心代码运行出错,那么全都 notifyAll 也不是不行。

2.4.2 join

join 方法的作用可以概括为“线程插队”,即阻塞当前线程,让其它线程先执行,等其它线程执行完或超时,当前线程再恢复执行。

join 方法是用 wait 方法实现的,所以 wait 方法可以被 interrupt,join 方法自然也可以。

当然 join 方法也必须获得线程对象的锁。

可以看下它的实现:

public final synchronized void join(long millis)
    throws InterruptedException {
        long base = System.currentTimeMillis();
        long now = 0;

        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (millis == 0) {
            while (isAlive()) {
                wait(0);
            }
        } else {
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
    }

核心逻辑也就是下面这三行代码(else 里面增加了对超时时间的控制,本质一样):

            while (isAlive()) {
                wait(0);
            }

wait(0) 等同于 wait() ,即没有超时时间。

这三行代码的意思就是: 如果 join 进来的线程还活着(即已经调了 start 方法但还没有执行完成),就一直等。

不用担心这里的 wait 会永远等待下去,因为线程终止的时候,会调用 this.notifyAll 方法。

可以写几行代码测试一下:

package per.lvjc.concurrent.waitnotify;

import java.util.concurrent.TimeUnit;

public class ThreadTerminatedTest {

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            System.out.println("thread begin");
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("thread end");
        });
        thread.start();
        synchronized (thread) {
            thread.wait();
        }
        System.out.println("main end");
    }
}

在 Thread 对象上 wait。跑一下可以发现 thread 执行完之后,wait 是会被唤醒的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值