前言
三种让线程等待
和唤醒
的方法如下:
- 方式一:使用 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 {
// 三个线程交替打印ABC
public 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 {
// 三个线程交替打印ABC
public 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 {
// 三个线程交替打印ABC
public 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 方法时
- 它会增加一个凭证,但凭证最多只能有一个,无法累加。