线程等待唤醒机制
三种让线程等待和唤醒的方法:
方式一:使用 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 张凭证,所以凭证不够,阻塞。
更多有意思的内容可以关注我的视频号
每天更新一个视频,给你分享一个干货
评论