> 文章列表 > 【面试】Java线程阻塞和唤醒的几种方式?

【面试】Java线程阻塞和唤醒的几种方式?

【面试】Java线程阻塞和唤醒的几种方式?

文章目录

  • 前言
  • 一、Object类自带的方法
  • 二、Condition接口
  • 三、LockSupport
  • 四、相关面试题
  • 总结:

前言

三种让线程等待唤醒的方法如下:

  • 方式一:使用 Object 中的 wait() 方法让线程等待,使用 Object 中的 notify() 方法唤醒线程
  • 方式二:使用 JUC 包中 Condition 的 await() 方法让线程等待,使用 signal() 方法唤醒线程
  • 方式三:LockSupport 类可以阻塞当前线程以及唤醒指定被阻塞的线程。

下面分别来介绍一下,希望对大家看完有帮助!

一、Object类自带的方法

使用wait()方法来阻塞线程,使用notify()和notifyAll()方法来唤醒线程。

  • 调用wait()方法后,线程将被阻塞,wait()方法将会释放当前持有的监视器锁(monitor),直到有线程调用notify/notifyAll()方法后方能继续执行。

  • notify/notifyAll()方法只是解除了等待线程的阻塞,并不会马上释放监视器锁,而是在相应的被synchronized关键字修饰的同步方法或同步代码块执行结束后才自动释放锁。

默认使用非公平锁,无法修改。

缺点:

  • 使用几个方法时,必须处于被synchronized关键字修饰的同步方法或同步代码块中,否则程序运行时,会抛出IllegalMonitorStateException异常。
  • 线程的唤醒必须在线程阻塞之后,否则,当前线程被阻塞之后,一直没有唤醒,线程将会一直等待下去(对比LockSupport)
public class SynchronizedDemo {// 三个线程交替打印ABCpublic static void main(String[] args) {Print print = new Print();new Thread(() -> {while (true) {print.printA();}}, "A").start();new Thread(() -> {while (true) {print.printB();}}, "B").start();new Thread(() -> {while (true) {print.printC();}}, "C").start();}
}class Print {Object object = new Object();int num = 1;public void printA() {synchronized (object) {try {while (num != 1) {object.wait();}for (int i = 0; i < 5; i++) {System.out.println(Thread.currentThread().getName() + "==>A");}num = 2;object.notifyAll();} catch (InterruptedException e) {e.printStackTrace();}}}public void printB() {synchronized (object) {try {while (num != 2) {object.wait();}for (int i = 0; i < 10; i++) {System.out.println(Thread.currentThread().getName() + "==>B");}num = 3;object.notifyAll();} catch (InterruptedException e) {e.printStackTrace();}}}public void printC() {synchronized (object) {try {while (num != 3) {object.wait();}for (int i = 0; i < 15; i++) {System.out.println(Thread.currentThread().getName() + "==>C");}num = 1;object.notifyAll();} catch (InterruptedException e) {e.printStackTrace();}}}
}

二、Condition接口

使用 JUC 包中 Condition 的await()方法来阻塞线程,signal()/singnalAll()方法来唤醒线程。
需要使用lock对象的newCondition()方法获得Condition条件对象(可有多个)。
可实现公平锁,默认是非公平锁
缺点:

  • 必须被Lock包裹,否则会在运行时抛出IllegalMonitorStateException异常。
  • 线程的唤醒必须在线程阻塞之后
  • Lock的实现是基于AQS,效率稍高于synchronized
public class ConditionDemo {// 三个线程交替打印ABCpublic static void main(String[] args) {Print print = new Print();new Thread(() -> {while (true) {print.printA();}}, "A").start();new Thread(() -> {while (true) {print.printB();}}, "B").start();new Thread(() -> {while (true) {print.printC();}}, "C").start();}
}class Print {private Lock lock = new ReentrantLock();private Condition condition1 = lock.newCondition();private Condition condition2 = lock.newCondition();private Condition condition3 = lock.newCondition();private int num = 1;public void printA() {lock.lock();try {while (num != 1) {condition1.await();}for (int i = 0; i < 5; ++i) {System.out.println(Thread.currentThread().getName() + "==>A");}num = 2;condition2.signal();} catch (InterruptedException e) {e.printStackTrace();} finally {lock.unlock();}}public void printB() {lock.lock();try {while (num != 2) {condition2.await();}for (int i = 0; i < 10; ++i) {System.out.println(Thread.currentThread().getName() + "==>B");}num = 3;condition3.signal();} catch (InterruptedException e) {e.printStackTrace();} finally {lock.unlock();}}public void printC() {lock.lock();try {while (num != 3) {condition3.await();}for (int i = 0; i < 15; ++i) {System.out.println(Thread.currentThread().getName() + "==>C");}num = 1;condition1.signal();} catch (InterruptedException e) {e.printStackTrace();} finally {lock.unlock();}}
}

三、LockSupport

使用park()来阻塞线程,用unpark()方法来唤醒线程。
这里有一个许可证的概念,许可不能累积,并且最多只能有一个许可,只有1和0的区别。
特点:

  • 使用灵活,可以直接使用
  • 线程唤醒可在线程阻塞之前,因为调用unpark()方法后,线程已经获得了一个许可证(但也只能有一个许可证),之后阻塞时,可以直接使用这个许可证来通行。
  • 效率高
public class LockSupportDemo {// 三个线程交替打印ABCpublic static void main(String[] args) throws Exception {Print print = new Print();Thread threadA = new Thread(() -> {while (true) {print.printA();}}, "A");Thread threadB = new Thread(() -> {while (true) {print.printB();}}, "B");Thread threadC = new Thread(() -> {while (true) {print.printC();}}, "C");threadA.start();threadB.start();threadC.start();while (true) {LockSupport.unpark(threadA);LockSupport.unpark(threadB);LockSupport.unpark(threadC);}}
}class Print {private int num = 1;public void printA() {while (num != 1) {LockSupport.park();}for (int i = 0; i < 5; i++) {System.out.println(Thread.currentThread().getName() + "==>A");}num = 2;}public void printB() {while (num != 2) {LockSupport.park();}for (int i = 0; i < 10; i++) {System.out.println(Thread.currentThread().getName() + "==>B");}num = 3;}public void printC() {while (num != 3) {LockSupport.park();}for (int i = 0; i < 15; i++) {System.out.println(Thread.currentThread().getName() + "==>C");}num = 1;}
}

四、相关面试题

  • 为什么可以先唤醒线程后阻塞线程?

    答:先唤醒线程意味着你调用了 unpark() 方法,那么凭证加1,再去阻塞线程,即调用 park() 方法,这个时候有凭证,所以直接消耗掉凭证然后正常退出

  • 为什么唤醒两次后阻塞两次,但最终结果还会阻塞线程?

    答:唤醒两次意味着调用了两次 unpark() 方法,但是凭证无法累加最多只有 1,然后阻塞两次,即调用两次 park() 方法,需要消费 2 张凭证才能正常退出,但是只有 1 张凭证,所以凭证不够,阻塞。

总结:

LockSupport 是用来创建锁和其他同步类的基本线程阻塞原语。

LockSupport 是一个线程阻塞工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞,阻塞之后也有对应的唤醒方法。归根结底,LockSupport 调用的是 Unsafe 类中的 native 方法。

LockSupport 提供 park() 和 unpark() 方法实现阻塞线程和解除线程阻塞的过程

LockSupport 和每个使用它的线程都有一个许可(permit)关联。 permit 默认是 0。

  • 调用一次 unpark 就加 1 变成 1
  • 调用一次 park 会消费 permit ,也就是将 1 变成 0,同时 park 立即返回。
  • 如果再次调用 park 会变成阻塞(因为 permit 为 0 会阻塞在这里,一直到 permit 为 1),这时候调用 unpark 会把 permit 置为 1
  • 每个线程都有一个相关的 permit,permit 最多只有一个,重复调用 unpark 不会积累凭证。

简单来说:

线程阻塞需要消耗凭证(permit),这个凭证最多只有一个。

  • 当调用 park 方法时
    • 如果有凭证,则会直接消耗掉这个凭证然后正常退出
    • 如果没有凭证,就必须阻塞等待凭证可用
  • 当调用 unpark 方法时
    • 它会增加一个凭证,但凭证最多只能有一个,无法累加。