线程间通信-wait/notify/join/ThreadLocal/线程状态

  在将某一任务拆份成多个子任务,使用多线程执行这些子任,就涉及到线程通信,才能使用多子任变成一个整体。

  线程间通信后,系统之间的交互性会更强大,在提高 CPU 利用率时还有助于对各线程任务的把控和监督。

wait/notify

while(true)轮询

while 语句轮询机制检测某一个条件,如果轮询间隔小,就非常浪费 CPU 资源,如果轮询间隔大,就可能取不到最新的数据。

等待 / 通知

排队等待叫号就是典型的 等待 / 通知 模式。

  1. wait() 方法

    wait() 的作用是使当前执行代码的线程进入等待,该方法是 Object 类的方法,是将当前线程置入 预执行队列 中,并且在 wait() 所在的代码处停止执行,直到被通知或中断为止。

    在调用 wait() 方法之前,线程必须获得该对象的对象级别锁,即只能在同步方法或同步代码块中调用 wait() 方法。在执行 wait() 方法后,当前线程释放锁。

    在从 wait() 返回前,线程与其他线程竞争重新获得锁。如果调用 wait() 没有持有适当的锁,则抛出 IllegalMonitorStateException,它是 RuntimeException 的一个子类,因此,不需要 try-catch 语句进行捕抓异常。

    wait() 方法可以使调用该方法的线程释放共享资源的锁,然后从运行状态退出,进入等待队列,直到被再次唤醒。

  2. notify() 方法

    notify() 方法也要在同步方法或同步代码块中调用,即在调用前,线程必须获得该对象的对象级别锁。如果调用 notify() 方法没有持有适当的锁,也会抛出 IllegalMonitorStateException 。

    该方法用来通知那些可能等待该对象的对象锁的其他线程,如果有多个线程等待,则由线程规划器随机挑选一个呈现 waite 状态的线程,对其发出通知(notify),并使它等待获取该对象的对象锁。

    notify() 方法唤醒一个因调用了 wait 操作而处于阻塞状态中的线程,使其进入就绪状态。

    notify() 方法可以随机唤醒等待队列中等待同一共享资源(锁)的 一个 线程,并使该线程退出等待队列,进入可运行状态,即 notify() 方法仅通知一个线程。

    注意:在执行 notify() 方法后,当前线程不会马上释放该对象锁,呈 wait 状态的线程也并不能马上获取该对象锁,要等到执行 notify() 方法的线程将程序执行完,也就是退出 synchronized 代码块后,当前线程才会释放锁。而呈 wait 状态的线程才可以获取该对象锁。

    当第一个获得了该对象锁的 wait 线程运行完毕后,它会释放掉该对象锁,此时如果该对象没有再次使用 notify 语句,则即便该对象已经空闲,其他 wait 状态的线程由于没有得到该对象的通知,还会继承阻塞在 wait 状态,直到这个对象发出一个 notify 或 notifyAll 。

  3. notifyAll()

    notifyAll() 方法可以使所有正在等待队列中等待同一共享资源的 全部 线程从等待状态退出,进入可运行状态。

    此时,优先级最高的线程最先执行,但也可能随机执行,这取决于虚拟机的实现。

总结:wait 使线程停止运行,而 notify 使停止的线程继续运行。

wait 被 interrupt

当线程呈 wait() 状态时,调用线程对象的 interrupt() 方法会出现 InterruptedException 异常。

  1. 执行完同步代码块就会释放对象的锁。
  2. 在执行同步代码块的过程中,遇到异常而导致线程终止,锁也会被释放。
  3. 在执行同步代码块的过程中,执行了锁所属对象的 wait() 方法,这个线程会释放对象锁,而此线程对象会进入线程等待池,等待被唤醒。

只通知一个线程

notify() 方法仅随机唤醒一个线程。

当多次调用 notify() 方法时,会随机将等待 wait 状态的线程进行唤醒。

notifyAll 唤醒所有线程

当 notify() 方法的唤醒次数小于线程对象的数量,会出现有部分线程对象无法被唤醒的情况。

为了唤醒所有线程,可以使用 notifyAll() 方法。

通知过早

如果 notify() 方法 比 wait() 更早执行,就存在通知过早问题,会打乱程序正常的运行逻辑,这样,wait() 方法永远不会被通知。

wait(long) 超时唤醒

带一个参数的 wait(long) 方法的功能是等待某一时间内是否有线程对锁进行唤醒,如果超过这个时间则自动唤醒。

wait() 条件被修改

wait() 方法的条件发生变化,也容易造成程序逻辑的混乱。

例如:if 判断 List 元数,里面调了 wait() 方法,外面执行了删除 List 元素操作,在多线程环境下,一个线程已删除,其它线程再次删除同位置的元素,就会出现索引溢出的异常。可以将 if 判断改为 while 判断来解决。

注意:wait() 方法是释放执行,且会释放锁的;而 sleep() 方法只释放执行,并不释放锁,因此肯定能唤醒的。

生产者/消费者模式实现

等待 / 通知 是典型的 生产者 / 消费者模式,其原理都是基于 wait / notify 的。

多生产与多消费:操作值-假死

当出现多生产与多消费时,通知调的是 notify() 方法,是无法保证 notify 唤醒的是异类,也许是同类,比如 生产者 唤醒 生产者消费者 唤醒 消费者 的情况。如果按此运行,积少成多,就会导致所有的线程都不能继承运和下去,大家都在等待,呈 WAITING 状态,程序最后也就呈 假死 状态,不能继续运行。

要解决此问题,将 notify() 方法改为 notifyAll() 方法即可。原理就是不只通 知同类线程,也包括异类,即通知所有线程。

管道流进行线程通信

在 JAVA 语言中提供了各种各样的输入 / 输入流 Stream ,可以通过 字节流字符流 实现线程通信。

  • 字节流:PipedInputStream,PipedOutputStream
  • 字符流:PipedReader,PipedWriter

字节流和字符流通信,重点是同一应用不同的线程的输出输入流需要建立连接,输出流有一个 connect(input) 方法连接输入流,先启动输入流,没有输入就会阻塞直到有输入;睡眠 2 秒,再启动输出流,写出数据,输入流接收到数据就实现了不同线程通信。

join() 方法

在很多情况下,如果主线程创建并启动子线程,如果子线程中要进行大量的耗时运算,主线程往往将早于子线程结束之前结束。这时,如果主线程想等待子线程执行完后再结束,比如子线程处理一个数据,主线程要取得这个数据中的值,就要用到 join() 方法。

join 作用

join() 的作用是等待线程对象销毁。

方法 join 的作用是使所属的线程对象 x (子线程)正常执行 run() 方法中的任务,而使当前线程 z (父线程)无限期阻塞,等待线程 x (子线程)销毁后,再继承执行线程 z (父线程)后面的代码。

方法 join 具有使线程排队运行的作用,有些类似同步的运行效果。

join 与 synchronized 的区别是:join 在内部使用 wait() 方法进行等待;而 synchronized 关键字使用的是 对象监视器 原理做为同步。

join 与异常

join() 方法与 interrupt() 方法如果彼此遇到,则会出现异常。如果子线程里还嵌套了子子线程,子子线程继承正常运行。

join(long)的使用

join(long) 的参数是设定等待时间。

join(long) 与 sleep(long)

  • **join(long)**:内部是使用 wait(long) 方法来实现,所以 join(long) 方法具有释放锁的特点,有锁的概念即可控制线程顺序。
  • **sleep(long)**: 不能释放锁,并不能控制线程顺序。

ThreadLocal

变量值的共享可以使用 public static 变量的形式,所有的线程都使用同一个 public static 变量。

如果想实现每一个线程都有自己的共享变量,可以使用 JDK 中提供的类 ThreadLocal 来实现。

类 ThreadLocal 主要解决的就是每个线程绑定自己的值,可以将 ThreadLocal 类比喻成全局存放数据的盒子,盒子中可以存储每个线程的私有数据。

ThreadLocal 作用

类 ThreadLocal 解决的是变量在不同线程间的隔离性,也就是不同线程拥有自己的值。不同线程中的值是可以放入 ThreadLocal 类中进行保存的。

解决 get() 返回 null 问题

如果 ThreadLocal 没有设置值,get() 方法返回值是 null,可以通过自定义类继承 ThreadLocal ,重写 initialValue() 方法,方法里返回的值即是默认值。

InheritableThreadLocal 类

InheritableThreadLocal 类继承自 ThreadLocal,可以在子线程中取得父线程继承下来的值,即子线程从父线程中取得值。

使用 InheritableThreadLocal 时注意,如果子线程在取得值的同时,主线程将 InheritableThreadLocal 中的值进行更改,子进程取到的值还是旧值。

线程状态切换

线程对象在不同的运行时间有不同的状态,状态信息就存在于 Thread.State 枚举类中。

线程有 6 种状态,分别是:新建状态,可运行状态(就绪状态),运行状态,阻塞状态(暂停状态),等待状态,死亡状态。

Thread State

每个锁对象都有两个队列,一个是就绪队列,一个是阻塞队列。就绪队列存储了将要获得锁的线程,阻塞队列存储了被阻塞的线程。

一个线程被唤醒后,才会进入就绪队列,等待 CPU 调度;反之,一个线程被 wait 后,就会进入阻塞队列,等待下一次唤醒。

NEW

创建但尚未启动的线程的状态。即实例化后,还未执行 start() 方法。

RUNNABLE-就绪

Runnable 可运行线程的线程状态,也称为就绪状态。可运行状态中的线程在Java虚拟机中执行,但它可能等待来自操作系统(如处理器)的其他资源。

  • 新建一个线程,调用 start() 方法,系统会为此线程分配 CPU 资源,使其处于 Runnable(可运行) 状态。
  • 调用 sleep() 方法后经过的时间超过了指定的休眠时间。
  • 线程调用的阻塞 IO 已经返回,阻塞方法执行完毕。
  • 线程成功地获得了试图同步的监视器。
  • 线程正在等待某个通知,其他线程发出了通知。
  • 处于挂起状态的线程调用了 resume 恢复方法。

RUNNING-运行

  • 线程调度从就绪状态线程队列中选择一个线程交予 CPU 资源。

WAITING-等待

  • 调用了 wait() 方法后线程状态的枚举值,等待其它线程做出一些特定动作(通知或中断)。

TIMED_WAITING

  • 调用了 sleep(long time) 方法后线程状态的枚举值。

BLOCKED-阻塞

  • 线程调用了阻塞式 IO 方法,在该方法返回前,该线程阻塞。
  • 线程阻塞等待线他持有锁的线程释放锁(试图获得一个同步监视器,但该同步监视器正被其他线程所持有)。
  • 线程等待某个通知。
  • 线程调用了 suspend 方法将线程挂线。此方法容易导致死锁,尽量避免使用此方法。

TERMINATED-销毁

  • 线程 run() 方法运行结束后进入销毁阶段,整个线程执行完毕。

线程间通信-wait/notify/join/ThreadLocal/线程状态

http://blog.gxitsky.com/2019/06/12/Java-Thread-05-thread-notify/

作者

光星

发布于

2019-06-12

更新于

2022-06-17

许可协议

评论