• 并发编程——LockSupport工具和Condition接口


    本文参考于《Java并发编程的艺术》

    开始学习吧⬇️

    LockSupport工具

    当需要阻塞或唤醒一个线程的时候,都会使用LockSupport工具类来完成。

    LockSupport定义了一组park开头的方法用来阻塞当前线程,unpark的方法来唤醒被阻塞的线程。

    方法源码如下: 

    1. //阻塞当前线程,如果调用unpark方法或者当前线程被中断,才能从park返回
    2. public static void park(Object blocker) {
    3. Thread t = Thread.currentThread();
    4. setBlocker(t, blocker);
    5. UNSAFE.park(false, 0L);
    6. setBlocker(t, null);
    7. }
    8. //阻塞当前线程,最长不超过nanos纳秒
    9. public static void parkNanos(long nanos) {
    10. if (nanos > 0)
    11. UNSAFE.park(false, nanos);
    12. }
    13. //阻塞当前线程,直到deadline时间
    14. public static void parkUntil(long deadline) {
    15. UNSAFE.park(true, deadline);
    16. }
    17. //唤醒处于阻塞状态的线程
    18. public static void unpark(Thread thread) {
    19. if (thread != null)
    20. UNSAFE.unpark(thread);
    21. }

    有同学去LockSupport类中去研究源码,就发现你这不对啊,源码里面的parkNanos(Object blocker, long nanos) 参数还多一个blocker呢,你都不说,及其敷衍,不看了!

    所以,为了留住读者们,那我必然提一嘴👄

    Java 6之后,LockSupport就赠加了3个方法,不仅是parkNanos(Object blocker, long nanos) ,还有park(Object blocker)和parkUntil(Object blocker, long deadline)。这时候就有人发现了,没错,都增加了blocker这个对象。这3个方法都是用于实现阻塞当前线程的功能,其中blocker就是用来标记当前线程在等待的对象,其实也就是阻塞对象。

    blocker对象的用途是什么呢?

    问得好!主要用户问题的排查和系统监控。

    在Java 5之前,当前阻塞线程在一个对象上时,通过线程dump能够查看该线程的阻塞对象,但是,Lock的工具类给忘了!!忘了!!

    所以Java 6马上就安排上了含有阻塞对象的park方法,代替之前的park方法。

    来个对比图看看区别:

    通过对比图 可以看到,都是阻塞10秒,但是有阻塞对象的parkNanos就可以给我们传递更多的线程信息。

    Condition接口

    任意一个Java对象,都拥有一组监视器方法,主要包括wait()、wait(long timeout)、notify()和notifyAll()方法,这些方法与synchronized配合可以实现等待/通知模式。

    Condition接口也提供了类似的监视器方法,与Lock配合可以实现等待/通知模式。

    二者的区别如下: 

    接口示例

    Condition是依赖Lock对象的。Condition对象是由Lock对象创建出来的。

    Condition定义了等待/通知两种类型的方法,当前线程调用这些方法时,需要提前获取到Condition对象关联的锁。

    具体使用方式如下:

    1. Lock lock = new ReentrantLock();
    2. Condition condition = lock.newCondition();
    3. public void conditionWait() throws InterruptedException{
    4. lock.lock();
    5. try {
    6. condition.await();
    7. }finally {
    8. lock.unlock();
    9. }
    10. }
    11. public void conditionSignal() throws InterruptedException{
    12. lock.lock();
    13. try{
    14. condition.signal();
    15. }finally {
    16. lock.unlock();
    17. }
    18. }

    当调用await方法后,当前线程会释放锁并在此等待,而其他线程调用Condition对象的signal方法,通知当前线程后,当前线程才会从await方法返回,并且在返回前已经获取了锁。

    获取一个Condition必须通过Lock的newCondition()方法。

    Condition定义的部分方法如下,更多方法可以去源码查看。

    1. //当前线程进入等待状态直到被通知或中断
    2. void await() throws InterruptedException;
    3. //当前线程进入等待状态直到被通知
    4. void awaitUninterruptibly();
    5. //当前线程进入等待状态直到被通知,中断或超时。返回值表示剩余时间
    6. long awaitNanos(long nanosTimeout) throws InterruptedException;
    7. //当前线程进入等待状态直到被通知,中断或到某个时间。
    8. //如果没有到指定时间就被通知的,方法返回true,否则,表示到了时间,返回false
    9. boolean awaitUntil(Date deadline) throws InterruptedException;
    10. //唤醒一个登载线程
    11. void signal();
    12. //唤醒所有等待线程
    13. void signalAll();

    实现分析

    ConditionObject是同步器AbstractQueuedSychronizer的内部类,因为Condition的操作都需要获取相关联的锁。每个Condition对象都包含着一个队列,该队列是Condition对象实现等待/通知功能的关键。

    等待队列

    等待队列是一个FIFO的队列,在队列中的每个节点都包含了一个线程引用,该线程就是在Condition对象上等待的线程,如果一个线程调用了Condition.await()方法,那么该线程将会释放锁,进入等待状态。

    Condition拥有首节点和尾节点。等待队列的基本结构如下图

    如图, 新增节点只需要将尾节点nextWaiter指向它,并更新尾节点即可。由于await方法是必定获取了锁的线程,因此是线程安全的。

    等待

    调用Condition的await()方法或者其他以await开头的方法,会使当前线程进入等待队列并释放锁,同时状态变为等待状态。当从await()方法返回时,当前线程一定获取了相关锁。

    1. public final void await() throws InterruptedException {
    2. if (Thread.interrupted())
    3. throw new InterruptedException();
    4. //当前线程加入等待队列
    5. Node node = addConditionWaiter();
    6. //释放锁
    7. int savedState = fullyRelease(node);
    8. int interruptMode = 0;
    9. while (!isOnSyncQueue(node)) {
    10. LockSupport.park(this);
    11. if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
    12. break;
    13. }
    14. if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
    15. interruptMode = REINTERRUPT;
    16. if (node.nextWaiter != null) // clean up if cancelled
    17. unlinkCancelledWaiters();
    18. if (interruptMode != 0)
    19. reportInterruptAfterWait(interruptMode);
    20. }

    我们来看一下源码。首先还是老习惯,先看注释

    实现可中断条件等待。

    1. 如果当前线程被中断,则抛出InterruptedException。
    2. 保存getState返回的锁定状态
    3. 以保存的状态作为参数调用release,如果失败,将引发IllegalMonitorStateException。
    4. 获取锁,直到发出信号或中断。
    5. 通过调用的专用版本重新获取acquire,保存状态作为参数。
    6. 如果在步骤4中被阻止时中断,则抛出InterruptedException。 

    当等待队列中的节点被唤醒,则唤醒节点的线程开始尝试获取同步状态。如果不是通过其他线程调用 Condition.signal()方法唤醒,而是对等待线程进行中断,则会抛出InterruptedException异常。

    通知

    调用Condition.signal()方法,将等待时间最长的线程(如果存在)从该条件的等待队列移动到所属锁的等待队列。

    1. public final void signal() {
    2. //判断是否获取了锁的线程
    3. if (!isHeldExclusively())
    4. throw new IllegalMonitorStateException();
    5. Node first = firstWaiter;
    6. if (first != null)
    7. doSignal(first);
    8. }

    如果isHeldExclusily返回false,则会抛出IllegalMonitorStateException。调用该方法的条件是当前线程必须获取了锁。接着获取等待队列的首节点,将其移动到同步队列并使用Support唤醒节点中的线程。

    节点从等待队列移动到同步队列的过程如图所示,

    通过调用同步器的enq方法,等待队列中的头节点线程安全的移动到同步队列。当节点移动到同步队列后,当前线程再使用LockSupport唤醒该节点的线程。被唤醒后的线程,将从awit()方法中的while循环中退出,也就是 isHeldExclusily返回true。然后调用同步器的acquireQueued()方法加入到获取同步状态的竞争。成功获取锁后,被唤醒的线程将从await()方法返回。

    直观看一下就是

    1. 调用同步器的enq(头节点移动到同步队列)
    2. 使用LockSupport唤醒该节点的线程(从awit方法退出循环)
    3. 调用同步器的acquireQueued()获取锁 
    4. 线程将从await方法返回

    Condition的signalAll()方法相当于等待队列中的每一个节点执行一次signal方法,效果就是将等待队列中所有节点全部移动到同步队列,并唤醒每个节点的线程。

     

  • 相关阅读:
    forwardRef, useImperativeHandle实现父组件调用子组件方法(个人笔记,非教程)
    RIP解决不连续子网问题
    Vm软件安装_链接相机
    JAVA计算机毕业设计在线辅导答疑系统Mybatis+源码+数据库+lw文档+系统+调试部署
    dubbo以xml方式操作和新版dubbo-admin安装
    POJ1287联网题解
    el-form简单封装一个列表页中的搜索栏
    《深入浅出Vue.js》打卡Day1
    docker使用详解
    【408数据结构与算法】—希尔排序 Donald Shell(十七)
  • 原文地址:https://blog.csdn.net/Aguai229/article/details/126949623