线程等待唤醒机制
三种让线程等待和唤醒的方法:
方式一:使用 Object 中的 wait() 方法让线程等待,使用 Object 中的 notify() 方法唤醒线程
方式二:使用 JUC 包中 Condition 的 await() 方法让线程等待,使用 signal() 方法唤醒线程
方式三:LockSupport 类可以阻塞当前线程以及唤醒指定被阻塞的线程。
1、Object 类中的 wait 和 notify 实现线程等待和唤醒
1)、正常情况下代码实现
/**
* @auther hepingfly
* @date 2021/1/18 10:39 下午
*/
public class LockSupportDemo {
static Object obj = new Object(); // 使用同一把锁
public static void main(String[] args) {
new Thread(() -> {
synchronized (obj) {
System.out.println(Thread.currentThread().getName() + "进入");
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "被唤醒");
}
},"线程1").start();
new Thread(() -> {
synchronized (obj) {
obj.notify();
System.out.println(Thread.currentThread().getName() + "通知");
}
},"线程2").start();
}
// ---------------上面是正常情况,打印出来的结果为:---------------------------------------
线程1进入
线程2通知
线程1被唤醒
复制代码
2)、异常情况一(去掉 synchronized 关键字)
/**
* @auther hepingfly
* @date 2021/1/18 10:39 下午
*/
public class LockSupportDemo {
static Object obj = new Object();
public static void main(String[] args) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "进入");
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "被唤醒");
},"线程1").start();
new Thread(() -> {
obj.notify();
System.out.println(Thread.currentThread().getName() + "通知");
},"线程2").start();
}
}
复制代码
运行直接报异常:
结论:
Object 类中的 wait、notify、notifyAll 用于线程等待和唤醒的方法,都必须在 synchronized 内部执行。
3)、异常情况二(将 notify 方法放在 wait 方法前面)
/**
* @auther hepingfly
* @date 2021/1/18 10:39 下午
*/
public class LockSupportDemo {
static Object obj = new Object();
public static void main(String[] args) {
new Thread(() -> {
try {
Thread.sleep(3000); // 这里让线程1先睡上 3 秒,这样线程2就会先执行
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (obj) {
System.out.println(Thread.currentThread().getName() + "进入");
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "被唤醒");
}
},"线程1").start();
new Thread(() -> {
synchronized (obj) {
obj.notify();
System.out.println(Thread.currentThread().getName() + "通知");
}
},"线程2").start();
}
}
复制代码
执行结果为程序无法结束一直阻塞
结论:
必须要先 wait 后 notify 或者 notifyAll ,等待中的线程才会被唤醒,否则无法唤醒。
2、Condition 接口中的 await 和 signal 方法实现线程等待和唤醒
1)、正常情况下代码实现
/**
* @auther hepingfly
* @date 2021/1/18 10:39 下午
*/
public class LockSupportDemo {
static Lock lock = new ReentrantLock();
static Condition condition = lock.newCondition();
public static void main(String[] args) {
new Thread(() -> {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "进入");
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
System.out.println(Thread.currentThread().getName() + "被唤醒");
},"线程1").start();
new Thread(() -> {
lock.lock();
try {
condition.signal();
System.out.println(Thread.currentThread().getName() + "通知");
} finally {
lock.unlock();
}
},"线程2").start();
}
// ---------------上面是正常情况,打印出来的结果为:---------------------------------------
线程1进入
线程2通知
线程1被唤醒
复制代码
2)、异常情况一(去掉 lock 关键字)
/**
* @auther hepingfly
* @date 2021/1/18 10:39 下午
*/
public class LockSupportDemo {
static Object obj = new Object();
static Lock lock = new ReentrantLock();
static Condition condition = lock.newCondition();
public static void main(String[] args) {
new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + "进入");
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
}
System.out.println(Thread.currentThread().getName() + "被唤醒");
},"线程1").start();
new Thread(() -> {
try {
condition.signal();
System.out.println(Thread.currentThread().getName() + "通知");
} finally {
}
},"线程2").start();
}
// 去掉 lock 和 unlock 关键字,仅仅保留 await() 和 signal() 方法,发现会直接报错
复制代码
运行直接报异常:
结论:
Condition 接口中的 await、signal、signalAll 用于线程等待和唤醒的方法,都必须在 lock 块内部执行
3)、异常情况二(将 signal 方法放在 await 方法前面)
/**
* @auther hepingfly
* @date 2021/1/18 10:39 下午
*/
public class LockSupportDemo {
static Object obj = new Object();
static Lock lock = new ReentrantLock();
static Condition condition = lock.newCondition();
public static void main(String[] args) {
new Thread(() -> {
try {
Thread.sleep(3000); // 这里让线程1睡上 3 秒钟
} catch (InterruptedException e) {
e.printStackTrace();
}
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "进入");
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
System.out.println(Thread.currentThread().getName() + "被唤醒");
},"线程1").start();
new Thread(() -> {
lock.lock();
try {
condition.signal();
System.out.println(Thread.currentThread().getName() + "通知");
} finally {
lock.unlock();
}
},"线程2").start();
}
复制代码
执行结果为程序无法结束一直阻塞
结论:
必须要先 await 后 signal 或者 signalAll ,等待中的线程才会被唤醒,否则无法唤醒。
3、LockSupport 类中的 park 等待和 unpark 唤醒
我们主要是通过 park 和 unpark 方法来实现阻塞和唤醒线程的操作
LockSupport 类使用了一种名为 Permit (许可)的概念来做到阻塞和唤醒线程的功能,每个线程都有一个许可(permit),permit 只有两个值 1 和 0,默认是 0
阻塞
park()/park(Object blocker)
permit 默认是 0,所以一开始调用 park() 方法,当前线程就会阻塞,直到别的线程将当前线程的 permit 设置为 1 时,park 方法会被唤醒,然后会将 permit 再次设置为 0 并返回。
阻塞当前线程/阻塞传入的具体线程
唤醒
unpark(Thread thread)
调用 unpark(Thread thread)
方法后,就会将 thread 线程的许可 permit 设置为 1(注意多次调用 unpark 方法,不会累加,permit 值还是 1)会自动唤醒 thread 线程,即之前阻塞中的 LockSupport.park()方法会立即返回
唤醒处于阻塞状态的指定线程
案例 1:先 park()
后 unpark()
/**
* @auther hepingfly
* @date 2021/1/18 10:39 下午
*/
public class LockSupportDemo {
/**
* 这个好处就是我可以单纯的去阻塞唤醒线程
* 而不需要去加一些锁块之类的代码
* @param args
*/
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "进入");
LockSupport.park(); // 类似于 wait() 方法,被阻塞,等待通知,要放行的话需要许可证
System.out.println(Thread.currentThread().getName() + "被唤醒");
}, "线程1");
thread1.start();
// 暂停几秒钟
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
/**
* 3秒钟以后,线程2运行,线程2要去唤醒线程1,相当于要给线程1发放许可证,线程1才能被放行
*/
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "通知");
LockSupport.unpark(thread1);
},"线程2").start();
}
}
// -----------------------------运行结果
线程1进入
线程2通知
线程1被唤醒
复制代码
案例 2:先 unpark()
后 park()
public class LockSupportDemo {
// 先 unpark 后 park 也是可以运行成功的,不像 wait 和 notify 会直接报错
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
// 暂停几秒钟
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "进入");
LockSupport.park(); // 如果先 unpark 后 park , 那么这行会直接不起作用
System.out.println(Thread.currentThread().getName() + "被唤醒");
}, "线程1");
thread1.start();
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "通知");
LockSupport.unpark(thread1);
},"线程2").start();
}
}
//-------------------运行结果
线程2通知
线程1进入
线程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),这个凭证最多只有一个。
面试题:
为什么可以先唤醒线程后阻塞线程?
答:先唤醒线程意味着你调用了 unpark() 方法,那么凭证加 1,再去阻塞线程,即调用 park() 方法,这个时候有凭证,所以直接消耗掉凭证然后正常退出
为什么唤醒两次后阻塞两次,但最终结果还会阻塞线程?
答:唤醒两次意味着调用了两次 unpark() 方法,但是凭证无法累加最多只有 1,然后阻塞两次,即调用两次 park() 方法,需要消费 2 张凭证才能正常退出,但是只有 1 张凭证,所以凭证不够,阻塞。
更多有意思的内容可以关注我的视频号
每天更新一个视频,给你分享一个干货
评论