Java面试必备之AQS阻塞队列和条件队列

 更新时间:2021年06月22日 14:49:11   作者:Java萨曼莎  
我们大概知道AQS就是一个框架,把很多功能都给实现了(比如入队规则,唤醒节点中的线程等),我们如果要使用的话只需要实现其中的一些方法(比如tryAcquire等)就行了!这次主要说说AQS中阻塞队列的的入队规则还有条件变量,需要的朋友可以参考下

一.AQS入队规则

我们仔细分析一下AQS是如何维护阻塞队列的,在独占方式获取资源的时候,是怎么将竞争锁失败的线程丢到阻塞队列中的呢?

我们看看acquire方法,这里首先会调用子类实现的tryAcquire方法尝试修改state,修改失败的话,说明线程竞争锁失败,于是会走到后面的这个条件;

这个addWaiter方法就是将当前线程封装成一个Node.EXCLUSIVE类型的节点,然后丢到阻塞队列中;

第一次还没有阻塞队列的时候,会到enq方法里面,我们仔细看看enq方法

enq()方法中,我们在第一次进入这个方法的时候,下面图一所示,tail和head都指向null;

第一次循环,到首先会到图二,然后判断t所指向的节点是不是null,如果是的话,就用CAS更新节点,这个CAS我们可以看作:头节点head为null,我们把head节点更新为一个哨兵节点(哨兵节点就是new Node()),再将tail也指向head,就是图三了

第二次for循环:走到上面的else语句,将新节点的前一个节点设置为哨兵节点;

然后就是CAS更新节点,这里CAS的意思:如果最后的节点tail指向的和t是一样的,那么就将tail指向node节点

最后再将t的下一个节点设置为node,下图所示,就ok了

二.AQS条件变量的使用

什么是条件变量呢?我们在开始介绍AQS的时候,还有一个内部类没有说,就是ConditionObject,还记得前面说过的Unsafe中的park和unpark方法吗?而这个ConditionObject就对这两个方法进行了一次封装,await()和signal()方法,但是更灵活,可以创建多个条件变量,每个条件变量维护一个条件队列(就是一个单向链表,可以看到Node这个内部类中个属性是nextWaiter);

注意:每一个条件变量里面都维护了一个条件队列

举个例子,如下所示;

package com.example.demo.study;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class Study0201 {

    public static void main(String[] args) throws InterruptedException {
        // 创建锁对象
        ReentrantLock lock = new ReentrantLock();
        // 创建条件变量
        Condition condition = lock.newCondition();
        // 以下创建两个线程,里面都会获取锁和释放锁
        Thread thread1 = new Thread(() -> {
            lock.lock();
            try {
                System.out.println("await begin");
                // 注意,这里调用条件变量的await方法,当前线程就会丢到condition条件变量中的条件队列中阻塞
                condition.await();
                System.out.println("await end");
            } catch (InterruptedException e) {
                //
            } finally {
                lock.unlock();
            }

        });

        Thread thread2 = new Thread(() -> {
            lock.lock();
            try {
                System.out.println("signal begin");
                // 唤醒被condition变量内部队列中的某个线程
                condition.signal();
                System.out.println("signal end");
            } finally {
                lock.unlock();
            }
        });
        thread1.start();
        Thread.sleep(500);
        thread2.start();
    }
}

还可以创建多个条件变量,如下所示,每一个条件变量都维护了一个条件队列:

package com.example.demo.study;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class Study0201 {

    public static void main(String[] args) throws InterruptedException {
        // 创建锁对象
        ReentrantLock lock = new ReentrantLock();
        // 创建条件变量1
        Condition condition1 = lock.newCondition();
        //条件变量2
        Condition condition2 = lock.newCondition();
        
        // 以下创建两个线程,里面都会获取锁和释放锁
        Thread thread1 = new Thread(() -> {
            lock.lock();
            try {
                System.out.println("await begin");//1
                condition1.await();
                System.out.println("await end");//5
                
                System.out.println("condition2---signal---start");//6
                condition2.signal();
                System.out.println("condition2---signal---endend");//7
            } catch (InterruptedException e) {
                //
            } finally {
                lock.unlock();
            }

        });

        Thread thread2 = new Thread(() -> {

            lock.lock();
            try {
                System.out.println("signal begin");//2
                condition1.signal();
                System.out.println("signal end");//3
                
                System.out.println("condition2---await---start");//4
                condition2.await();
                System.out.println("condition2---await---end");//8
            } catch (InterruptedException e) {
                //
            } finally {
                lock.unlock();
            }

        });

        thread1.start();
        Thread.sleep(500);
        thread2.start();

    }

}

三.走进条件变量

我们看看上面的获取条件变量的方式Condition condition1 = lock.newCondition(),我们打开newCondition方法,最后就是创建一个ConditionObject实例;这个类是AQS的内部类,通过这个类可以访问AQS内部的属性和方法;

注意:在调用await方法和signal方法之前,必须要先获取锁

然后我们再看看条件变量的await方法,下图所示,我们可以进入到addConditionWaiter()方法内部看看:

public final void await() throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    //新建一个Node.CONDITION节点放到条件队列最后面
    Node node = addConditionWaiter();
    //释放当前线程获取的锁
    int savedState = fullyRelease(node);
    int interruptMode = 0;
    //调用park()方法阻塞挂起当前线程
    while (!isOnSyncQueue(node)) {
        LockSupport.park(this);
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    if (node.nextWaiter != null) // clean up if cancelled
        unlinkCancelledWaiters();
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
}
private Node addConditionWaiter() {
    Node t = lastWaiter;
    //第一次进来,这个lastWaiter是null,即t = null,不会进入到这个if语句
    if (t != null && t.waitStatus != Node.CONDITION) {
        unlinkCancelledWaiters();
        t = lastWaiter;
    }
    //创建一个Node.CONDITION类型的节点,然后下面这个if中就是将第一个节点firstWaiter和最后一个节点都指向这个新创建的节点
    Node node = new Node(Thread.currentThread(), Node.CONDITION);
    if (t == null)
        firstWaiter = node;
    else
        t.nextWaiter = node;
    lastWaiter = node;
    return node;
}

顺便在看看signal方法:

public final void signal() {
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    //条件队列移除第一个节点,然后把这个节点丢到阻塞队列中,然后激活这个线程
    Node first = firstWaiter;
    if (first != null)
        doSignal(first);
}

我们想一想在AQS中阻塞队列和条件队列有什么关系啊?

1.当多个线程调用lock.lock()方法的时候,只有一个线程获取到可锁,其他的线程都会被转为Node节点丢到AQS的阻塞队列中,并做CAS自旋获取锁;

2.当获取到锁的线程对应的条件变量的await()方法被调用的时候,该线程就会释放锁,并把当前线程转为Node节点放到条件变量对应的条件队列中;

3.这个时候AQS的阻塞队列中又会有一个节点中的线程能得到锁了,如果这个线程又恰巧调用了对应条件变量的await()方法时,又会重复2的步骤,然后阻塞队列中又会有一个节点中的线程获得锁

4.然后,又有一个线程调用了条件变量的signal()或者signalAll()方法,就会把条件队列中一个或者所有的节点都移动到AQS阻塞队列中,然后调用unpark方法进行授权,就等着获得锁了;

一个锁对应一个阻塞队列,但是对应多个条件变量,每一个条件变量对应一个条件队列;其中,这两种队列中存放的都是Node节点,Node节点中封装了线程及其状态

到此这篇关于Java面试必备之AQS阻塞队列和条件队列的文章就介绍到这了,更多相关AQS阻塞队列和条件队列内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • JDK1.8新特性之方法引用 ::和Optional详解

    JDK1.8新特性之方法引用 ::和Optional详解

    这篇文章主要介绍了JDK1.8新特性之方法引用 ::和Optional,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2021-09-09
  • Java SSM整合开发统一结果封装详解

    Java SSM整合开发统一结果封装详解

    这篇文章主要介绍了Java SSM整合开发实现统一结果封装,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2022-08-08
  • SpringBoot实现权限验证的示例步骤

    SpringBoot实现权限验证的示例步骤

    权限验证是一种用于控制对系统资源和操作的访问的机制。它允许开发人员定义谁可以执行特定操作或访问特定资源,并确保只有经过授权的用户才能执行这些操作,这篇文章主要介绍了SpringBoot实现权限验证,需要的朋友可以参考下
    2023-08-08
  • Java中判断字符串是中文或者英文的工具类分享

    Java中判断字符串是中文或者英文的工具类分享

    这篇文章主要介绍了Java中判断字符串是中文或者英文的工具类分享,本文直接给出代码,相关说明请看代码的注释,需要的朋友可以参考下
    2014-10-10
  • SpringBoot RedisTemplate分布式锁的项目实战

    SpringBoot RedisTemplate分布式锁的项目实战

    本文主要介绍了SpringBoot RedisTemplate分布式锁的项目实战,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2022-05-05
  • SpringCloud Bus 消息总线的具体使用

    SpringCloud Bus 消息总线的具体使用

    这篇文章主要介绍了SpringCloud Bus 消息总线的具体使用,详细的介绍了什么是消息总线以及具体配置,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-12-12
  • SpringCache的基本使用方法

    SpringCache的基本使用方法

    Spring Cache利用了AOP,实现了基于注解的缓存功能,并且进行了合理的抽象,业务代码不用关心底层是使用了什么缓存框架,只需要简单地加一个注解,就能实现缓存功能了,本文介绍SpringCache的基本使用方法,感兴趣的朋友一起看看吧
    2024-01-01
  • java字符串遍历的几种常用方法总结

    java字符串遍历的几种常用方法总结

    Java字符串是一系列的Unicode字符序列,但是它却常常被误认为是char序列,这篇文章主要给大家介绍了关于java字符串遍历的几种常用方法,文中通过实例代码介绍的非常详细,需要的朋友可以参考下
    2023-04-04
  • 深入理解JVM之Java对象的创建、内存布局、访问定位详解

    深入理解JVM之Java对象的创建、内存布局、访问定位详解

    这篇文章主要介绍了深入理解JVM之Java对象的创建、内存布局、访问定位,结合实例形式详细分析了Java对象的创建、内存布局、访问定位相关概念、原理、操作技巧与注意事项,需要的朋友可以参考下
    2019-09-09
  • Java Hibernate中一对多和多对多关系的映射方式

    Java Hibernate中一对多和多对多关系的映射方式

    Hibernate是一种Java对象关系映射框架,支持一对多和多对多关系的映射。一对多关系可以使用集合属性和单向/双向关联来映射,多对多关系可以使用集合属性和中间表来映射。在映射过程中,需要注意级联操作、延迟加载、中间表的处理等问题
    2023-04-04

最新评论