ReentrantLock Condition的使用和实现原理(不留死角!!!)

1. 概述

本文是分析ReentrantLock源码的第三篇博客,介绍Condition的使用和分析源码的实现细节。之前的两篇博客链接如下,有兴趣的读者不妨看看。

通过这篇博客可以学习到Condition当中5种await方法,signal和signalAll方法的使用和源码的实现细节。

2. 初识Condition

在介绍方法的使用和分析源码之前,先来了解一下Condition是什么。

可以把Condition看作是Object监视器的替代品。众所周知,Object有wait()和notify()方法,用于线程间的通信。并且这两个方法只能在synchronized同步块内才可以调用,所有线程的等待和唤醒都需要关联到监视器对象的WaitSet集合。

Condition同样可以实现上面的线程通信。不同点在于,synchronized锁对象关联的监视器对象仅有一个,所以等待队列也只有一个。而一个ReentrantLock可以有多个Condition,这样可以根据不同的业务需求,在使用同一个lock锁对象的基础上使用多个等待队列,让不同性质的线程加入到不同的等待队列当中。

AQS当中Condition的实现类是ConditionObject,它是AQS的内部类,所以无法直接实例化。可以配合ReentrantLock来使用。

ReentrantLock中有newCondition()的方法,来实例化一个ConditionObject对象,因此可以调用多次newCondition()方法来得到多个等待队列。

3. 5种await方法的使用

3.1 await()

先来看一个比较简单的例子,一个线程,拿到锁由于某些条件无法满足,调用condition.await()方法

@Slf4j(topic = "s")
public class AwaitTest1 {
    
    

    static ReentrantLock lock = new ReentrantLock();
    static Condition condition = lock.newCondition();

    public static void main(String[] args) {
    
    
        new Thread(() -> {
    
    
            try {
    
    
                lock.lock();
                log.debug("因为某些条件无法满足,进入等待");
                condition.await();
                log.debug("条件满足了被唤醒,开始工作");
            } catch (Exception e) {
    
    
                e.printStackTrace();
            } finally {
    
    
                lock.unlock();
            }
        }, "t1").start();
    }
}

控制台输出如下,仅仅打印出了一句话,说明调用await方法之后,该线程就不会继续往下执行代码了。这就和Object的wait方法很像,需要另一个线程调用notify来唤醒。不过此处的方法名字不叫做notify,而是signal
在这里插入图片描述
修改上面的测试代码如下:

@Slf4j(topic = "s")
public class AwaitTest1 {
    
    

    static ReentrantLock lock = new ReentrantLock();
    static Condition condition = lock.newCondition();

    public static void main(String[] args) throws InterruptedException {
    
    
        new Thread(() -> {
    
    
            try {
    
    
                lock.lock();
                log.debug("因为某些条件无法满足,进入等待");
                condition.await();
                log.debug("条件满足了被唤醒,开始工作");
            } catch (Exception e) {
    
    
                e.printStackTrace();
            } finally {
    
    
                lock.unlock();
            }
        }, "t1").start();

        TimeUnit.SECONDS.sleep(4);

        lock.lock();
        condition.signal();
        lock.unlock();
    }
}

此时的结果如下,4秒后,主线程将t1线程唤醒,t1线程就继续执行后面的逻辑啦,打印了开始工作。
在这里插入图片描述
上面的就是await/signal最基本的使用例子。由两个线程来协作完成,一个线程等待,另一个线程负责唤醒。

3.2 awaitUninterruptibly()

这一节来看看awaitUninterruptibly()和await()方法有什么样的不同。

从名字上看,该方法多了Uninterruptibly,不可打断的意思。那么就写一个测试代码,打断一下正在等待的线程看看有什么区别。

先来试一下调用了await()方法的线程被打断的结果,测试代码如下:

@Slf4j(topic = "s")
public class AwaitTest2 {
    
    
    static ReentrantLock lock = new ReentrantLock();
    static Condition condition = lock.newCondition();
    public static void main(String[] args) throws InterruptedException {
    
    
        Thread thread = new Thread(() -> {
    
    
            try {
    
    
                lock.lock();
                log.debug("因为某些条件无法满足,进入等待");
                condition.await();
                log.debug("条件满足了被唤醒,开始工作");
            } catch (Exception e) {
    
    
                e.printStackTrace();
            } finally {
    
    
                lock.unlock();
            }
        }, "t1");
        thread.start();
        TimeUnit.SECONDS.sleep(4);
        thread.interrupt();//打断t1线程
    }
}

控制台的输出如下,打断t1线程之后,t1线程会抛出中断异常。
在这里插入图片描述
那么调用awaitUninterruptibly()的结果呢?测试代码如下,仅仅将await替换成awaitUninterruptibly。

@Slf4j(topic = "s")
public class AwaitTest2 {
    
    

    static ReentrantLock lock = new ReentrantLock();
    static Condition condition = lock.newCondition();

    public static void main(String[] args) throws InterruptedException {
    
    
        Thread thread = new Thread(() -> {
    
    
            try {
    
    
                lock.lock();
                log.debug("因为某些条件无法满足,进入等待");
                condition.awaitUninterruptibly();//仅修改此处
                log.debug("条件满足了被唤醒,开始工作");
            } catch (Exception e) {
    
    
                e.printStackTrace();
            } finally {
    
    
                lock.unlock();
            }
        }, "t1");
        thread.start();
        TimeUnit.SECONDS.sleep(4);
        thread.interrupt();
    }
}

控制台输出如下,并不会抛出中断异常。
在这里插入图片描述
总结

  • 调用了await方法的线程会因为中断抛出异常。
  • 调用了awaitUninterruptibly方法的线程不会因为中断抛出中断异常。

底层是如何实现的呢?第4部分会分析,继续往下看。

3.3 awaitNanos(long nanosTimeout)

上面的两个方法在不发生异常的情况下,会一直在等待被其他线程唤醒。接下来的三个方法,都是带有时间的等待,在一个时间范围内等待,超过这个时间范围,那么就会自己醒来。

测试代码如下:

@Slf4j(topic = "s")
public class AwaitTest3 {
    
    

    static ReentrantLock lock = new ReentrantLock();
    static Condition condition = lock.newCondition();

    public static void main(String[] args) throws InterruptedException {
    
    

        Thread thread = new Thread(() -> {
    
    
            try {
    
    
                lock.lock();
                log.debug("因为某些条件无法满足,进入等待");
                condition.awaitNanos(5000000000l);//5秒
                log.debug("条件满足了被唤醒,或超时,开始工作");
            } catch (Exception e) {
    
    
                e.printStackTrace();
            } finally {
    
    
                lock.unlock();
            }
        }, "t2");
        thread.start();
    }
}

控制台输出结果如下,等待5秒后,t2线程自己醒来了,继续执行代码。
在这里插入图片描述
参数的意思是一个纳秒时间,截止的时间是当前时间+纳秒时间。在截止时间之前,t2线程可以被其他线程叫醒(signal)或者中断(抛出中断异常)。如果超过截止时间,则t2线程自己醒来执行下面的代码。

提问:超过截止时间,t2线程醒来后是立马执行接下来的代码吗?

下面再写一个测试例子看看结果如何:

@Slf4j(topic = "s")
public class AwaitTest3 {
    
    

    static ReentrantLock lock = new ReentrantLock();
    static Condition condition = lock.newCondition();

    public static void main(String[] args) throws InterruptedException {
    
    
        // t2线程 因为某条件不满足 进入等待队列
        Thread thread = new Thread(() -> {
    
    
            try {
    
    
                lock.lock();
                log.debug("因为某些条件无法满足,进入等待");
                condition.awaitNanos(5000000000l);//5秒
                log.debug("条件满足了被唤醒,开始工作");
            } catch (Exception e) {
    
    
                e.printStackTrace();
            } finally {
    
    
                lock.unlock();
            }
        }, "t2");
        thread.start();
        TimeUnit.MILLISECONDS.sleep(100);
        lock.lock();
        // 创建5个线程,因为拿不到锁都进入阻塞队列
        for (int i = 0; i < 5; i++) {
    
    
            int finalI = i;
            new Thread(() -> {
    
    
                try {
    
    
                    log.debug("t" + (finalI + 3) + "线程拿不到锁 进入阻塞队列");
                    lock.lock();
                    log.debug("t" + (finalI + 3) + "线程拿到锁,开始工作");
                    TimeUnit.SECONDS.sleep(2);//模拟工作时间2秒
                } catch (Exception e) {
    
    
                    e.printStackTrace();
                } finally {
    
    
                    lock.unlock();
                }
            }, "t" + (i + 3)).start();
        }
        TimeUnit.MILLISECONDS.sleep(100);//确保t3 - t7 5个线程都进入阻塞队列
        lock.unlock();
    }
}

先说明一下代码的意图,首先t2线程因为不满足某些条件而调用awaitNanos()方法进入等待队列。之后主线程拿锁,for循环创建5个线程,这5个线程由于拿不到锁会进入阻塞队列,至于为什么,之前的博客已经说明过了,这里不再赘述。

我们来看看结果,看看t2线程是在什么时候执行工作的。控制台输出如下:
在这里插入图片描述
可以看到在10秒的时候,它进入了等待队列,但是在20秒的时候,他才继续工作。期间相差的这10秒中,恰好是5个线程,每个线程工作2秒的时间总和。

所以这里可以猜想,t2醒来后它跑到了阻塞队列当中,到底是不是这样的呢?第4部分源码分析的时候再证明。

3.4 awaitUntil(Date deadline)

该方法也是一个规定时间的等待,在截止时间之前,线程可以被其他线程叫醒(signal)或者中断(抛出中断异常)。如果超过截止时间,则线程自己醒来执行下面的代码。

只是这里传入的参数直接是一个截止时间,不再像上面一样需要计算一个截止时间。

测试代码如下:

@Slf4j(topic = "s")
public class AwaitTest4 {
    
    
    static ReentrantLock lock = new ReentrantLock();
    static Condition condition = lock.newCondition();

    public static void main(String[] args) throws InterruptedException {
    
    
        // t1线程 因为某条件不满足 进入等待队列
        Thread thread = new Thread(() -> {
    
    
            try {
    
    
                lock.lock();
                log.debug("因为某些条件无法满足,进入等待");
                Calendar calendar = Calendar.getInstance();
                calendar.add(Calendar.SECOND, 5);
                condition.awaitUntil(calendar.getTime());//5秒
                log.debug("条件满足了被唤醒,或超时,开始工作");
            } catch (Exception e) {
    
    
                e.printStackTrace();
            } finally {
    
    
                lock.unlock();
            }
        }, "t1");
        thread.start();
    }
}

控制台输出如下:
在这里插入图片描述

3.5 await(long time, TimeUnit unit)

该方法传入等待的时间,和时间单位,相比较于awaitNanos(long nanosTimeout)方法更加的灵活。时间和时间单位进行配合。计算一个截止时间,作用和上面两个方法一样。

测试代码如下:

@Slf4j(topic = "s")
public class AwaitTest5 {
    
    
    static ReentrantLock lock = new ReentrantLock();
    static Condition condition = lock.newCondition();

    public static void main(String[] args) throws InterruptedException {
    
    
        // t1线程 因为某条件不满足 进入等待队列
        Thread thread = new Thread(() -> {
    
    
            try {
    
    
                lock.lock();
                log.debug("因为某些条件无法满足,进入等待");
                condition.await(5, TimeUnit.SECONDS);//5秒
                log.debug("条件满足了被唤醒,或超时,开始工作");
            } catch (Exception e) {
    
    
                e.printStackTrace();
            } finally {
    
    
                lock.unlock();
            }
        }, "t1");
        thread.start();
    }
}

控制台输出如下:
在这里插入图片描述

4. 5种await方法的源码分析

通过第三部分的方法使用介绍,相信读者已经掌握了这5种方法是如何使用的,以及使用的区别。下面来分析分析底层源码是如何实现的。

4.1 await()

在这里插入图片描述

  • 首先判断是否中断过,如果发生过中断,那么就会抛出异常。

  • 调用addConditionWaiter方法将当前线程封装成Node结点,并加入到等待队列的末尾。

    • addConditionWaiter的代码如下在这里插入图片描述
      当队尾结点不属于等待状态的时候,则调用unlinkCanceledWaiters()将不处于等待状态的所有结点从等待队列中移除,具体的代码如下:在这里插入图片描述
  • 接着调用fullyRelease释放锁,并记录下状态值。说明await()的线程不再持有锁,这一点和Object中的wait方法是一样的。

    • fullyRelease代码如下,立马调用了release方法释放锁的过程,之前的博客中介绍过,不再赘述。在这里插入图片描述
  • 调用isOnSyncQueue判断是否在同步队列上,如果当前结点不在同步队列上,说明他在等待队列上,将其阻塞。这里用while循环是为了,等它下次醒来之后再一次的判断是否在同步队列上,如果还是不在同步队列,说明他还在等待队列当中。它就需要继续等待,将其阻塞。

  • 等到他下次被唤醒了,会调用checkInterruptWhileWaiting,判断在阻塞期间是否发生了中断。如果发生了中断,说明取消等待。代码如下:

    • 如果发生了中断,那么则会调用transferAfterCanceledWait方法在这里插入图片描述

    • transferAfterCanceledWait方法有两种情况

      第一种:在调用signal唤醒之前取消,那么就可以cas成功,将结点的状态更新,然后将其转移到同步队列,返回true。

      第二种:在调用signal之后发生了中断,那么就返回false。此处的while循环是为了确保signal方法执行的时候将结点顺利转移到同步队列。signal方法的执行逻辑后面会讲。在这里插入图片描述

    • 总结:在signal调用前发生了中断,会返回抛出异常的标记THROW_IE,在signal调用后发生了中断会返回重新中断的标记REINTERRUPT。

  • 再接着就是调用acquireQueued方法,此时的结点一定在同步队列上了,所以该方法的执行逻辑,就和之前博客中介绍过的同步阻塞的结点抢锁的逻辑一样。不清楚的读者可以回看之前的博客。

  • 之后就是,判断当前结点的nextWaiter是否为空,如果不为空的话,调用unlinkCancelledWaiters()将取消等待的结点从等待队列中移除。

  • 最后判断interruptMode响应中断的模式,如果不等于0的话,说明发生过中断。要么抛出异常,要么重新中断。在这里插入图片描述

4.2 awaitUninterruptibly()

分析过await方法的源码,再来看接下来的源码就比较容易了。该方法与await的主要区别在于,不可中断。所以如果发生了中断,并不会抛出异常,只有一个措施就是重新中断(中断补偿)。下面来看看代码:
在这里插入图片描述
大致逻辑基本相同,仅仅在发生中断的处理上不太一样。此处不需要记录响应中断的模式,无论结点是在同步队列还是等待队列上发生的中断,都采取中断补偿的机制。

4.3 awaitNanos(long nanosTimeout)

该方法让线程在一个时间范围内等待,超过这个时间范围,那么就会自己醒来。源码实现如下:
在这里插入图片描述
根据传入的参数,计算出等待的截止时间,逻辑和前面的都差不多。

最主要的区别,在于红色框框的部分,while循环的条件是结点在等待队列上。如果剩余等待时间nanosTimeout小于等于0,那么它就会取消等待,调用transferAfterCanelledWait方法进行队列转移,转移到同步队列上。

如果剩余时间大于spinForTimeoutThreshold,那么该线程会阻塞。这里设置一个阈值,是为了避免时间过短,导致频繁的系统调用(阻塞,唤醒)。

4.4 awaitUntil(Date deadline)

该方法和上面的几乎一样,只是不需要计算截止时间,传入的参数就是截止的时间。
在这里插入图片描述

4.5 await(long time, TimeUnit unit)

该方法可以说是awaitNanos(long time)的升级版吧,根据时间单位和传入的数字,转换成纳秒时长。之后的逻辑都一样的。
在这里插入图片描述

5. signal和signalAll的源码分析

  • signal源码分析在这里插入图片描述

    • 调用isHeldExclusively()判断当前线程是否是锁的持有者。如果不是的话,抛出异常。在这里插入图片描述

    • 拿到等待队列中的第一个结点,如果不为空则调用doSignal方法,实现唤醒线程。

    doSignal方法如下:在这里插入图片描述

    • 将头结点从等待队列中移除,更新队列的头结点。
    • 让尝试转移first结点。因为是多线程,所以可能执行到这里的时候,first结点已经转移了。那么就要将first指针指向新的头结点。尝试唤醒新的头结点。
    • 这就是这do-while循环的意图。

    transferForSignal方法如下:在这里插入图片描述

    • 尝试CAS修改结点的状态,如果失败了返回false。说明该结点已经转移了。

    • 如果修改成功。那么就调用enq方法,将结点从等待队列转移到同步队列,enq方法会返回node结点的前驱。

    • 然后判断前驱结点p的ws。此处分2种情况。

      第一种:ws大于0,说明这个结点是一个取消状态的结点,那么就可以唤醒当前node结点的线程。

      第二种:ws<=0,CAS修改前驱结点的状态为-1。如果修改失败了,说明它本身结点的状态就是-1了。那么此时它有义务唤醒后继结点,也就是唤醒当前node结点的额线程。

    以上就是signal方法的源码,比较的简单。

  • signalAll源码分析在这里插入图片描述
    这里和上面的逻辑是一样的。只是内部调用的方法不一样,主要来看看doSignalAll方法的不同。
    在这里插入图片描述
    该方法的逻辑也很简单,从头结点开始,一个一个的将等待队列当中的结点转移到同步队列当中。

猜你喜欢

转载自blog.csdn.net/gongsenlin341/article/details/113549583