AQS(抽象的队列同步器)

目录

1 是什么

2 AQS是JUC内容中最重要的基石

2.1 和AQS相关的

2.1.1 ReentrantLock

2.1.2 CountDownLatch 计数器

2.1.3 ReentrantReadWriteLock 读写锁

2.1.4 Semphore 信号灯

2.2 锁和同步器的关系

3 能做什么?

3.1 加锁会导致阻塞

3.2 解释

4 AQS初步 

4.1 AQS解释说明

4.2 AQS内部体系架构

4.2.1 AQS自身

4.2.2 内部类Node(Node在AQS类的内部)

4.3 AQS同步队列的基本结构

5 从ReentrantLock聊AQS

5.1 Lock接口的实现类,基本都是通过【聚合】了一个【队列同步器】的子类完成线程访问控制的

5.2 ReentrantLock的原理

5.3 lock方法看公平和非公平

5.4 非公平锁,方法lock()

5.4.1 lock()

5.4.2 acquire()

5.4.3 tryAcquire()

5.4.4 addWaiter(Node.EXCLUSIVE)

源码和三大流程走向

5.4.5 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)

5.5 方法unlock()

6 总结

7 参考文献


前提了解:

公平锁和非公平锁

可重入锁

LockSupport

自旋锁

数据结构之链表

设计模式之模板设计模式   

1 是什么

抽象的队列同步器

队列:单/双列表, ArrayList就是链表结构

是用来构建锁或者其他同步器组件(一系列根据锁相关的组件)的重量级基础框架及整个JUC体系的基石,通过内置的FIFO队列来完成资源获取线程的排队工作,并通过一个int类型变量表示持有锁的状态。

 

 JUC中有几个AQS缩写的相关内容,长得很相似,通常聊的AQS是指AbstractQueuedSynchronizer

2 AQS是JUC内容中最重要的基石

2.1 和AQS相关的

2.1.1 ReentrantLock

2.1.2 CountDownLatch 计数器

2.1.3 ReentrantReadWriteLock 读写锁

2.1.4 Semphore 信号灯

.。。。。。。还有很多

2.2 锁和同步器的关系

锁:面向锁的使用者:定义了程序员和锁交互的使用层API

同步器:比如java并发大神Douglee,面向锁的实现者:统一规范并简化了锁的实现,屏蔽了同步状态管理、阻塞线程排队和通知、唤醒机制等。

3 能做什么?

3.1 加锁会导致阻塞

有阻塞就需要排队,实现排队必须要有某种形式的队列来进行管理。 

3.2 解释

 

4 AQS初步 

4.1 AQS解释说明

有阻塞就需要排队,实现排队必然需要队列

AbstractQueuedSynchronizer里面,排队的线程会装到Node类里

4.2 AQS内部体系架构

4.2.1 AQS自身

4.2.1.1 AQS的int变量

AQS的同步状态state成员变量:

银行办理业务的手里窗口状态:

  • 0就是没有人,自由状态可以办理
  • 大于等于1,有人占用窗口,等着去排队

4.2.1.2 AQS的CLH队列

CLH队列(三个大牛名字组成),为一个双向队列

银行候客区的等待顾客 

4.2.1.3 小总结

有阻塞就需要排队,实现排队必然需要队列

state变量+CLH变种的双端队列

4.2.2 内部类Node(Node在AQS类的内部)

4.2.2.1 Node的int变量

  • node的等待状态waitState成员变量: volatile int waitStatus
  • 解释:等候区其他顾客(其他线程)的等待状态;队列中每个排队的个体就是一个node.

4.2.2.2 Node类讲解-内部结构

4.2.2.3 Node类讲解-属性说明

4.3 AQS同步队列的基本结构

 

5 从ReentrantLock聊AQS

5.1 Lock接口的实现类,基本都是通过【聚合】了一个【队列同步器】的子类完成线程访问控制的

lock unlock 底层都是sync.lock(), sync.release()

sync是ReetrantLock的一个内部类,sync又继承了AQS抽象队列同步器

5.2 ReentrantLock的原理

5.3 lock方法看公平和非公平

Lock lock = new ReentrantLock();

默认不传值是非公平锁

 

5.4 非公平锁,方法lock()

 

 

public class AQSDemo {

    public static void main(String[] args) {
        ReentrantLock lock = new ReentrantLock();
        //带入一个银行办理业务的案例来模拟我们的AQS如何进行线程的管理和通知唤醒机制
        //3个线程模拟3个来银行网点,受理窗口办理业务的顾客

        //A顾客就是第一个顾客,此时受理窗口没人,A可直接去办理
        new Thread(() -> {
            lock.lock();
            try {
                System.out.println("-----A thread come in");
                try { TimeUnit.MINUTES.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
           } finally {
                lock.unlock();
            }

        }, "A").start();

        //第2个顾客,第2个线程->由于受理业务的窗口只有一个(只能一个线程持有锁),此时B只能等待
        //进入候客区
        new Thread(() -> {
            lock.lock();
            try {
                System.out.println("-----B thread come in");
            } finally {
                lock.unlock();
            }

        }, "B").start();

        //第3个顾客,第3个线程->由于受理业务的窗口只有一个(只能一个线程持有锁),此时C只能等待
        //进入候客区
        new Thread(() -> {
            lock.lock();
            try {
                System.out.println("-----C thread come in");
            } finally {
                lock.unlock();
            }

        }, "C").start();
    }
}

5.4.1 lock()

5.4.2 acquire()

 A线程执行后,B线程执行,由于A已抢占,B只能排队,执行acquire(1)

5.4.3 tryAcquire()

AQS设定了tryAcquire方法,在子接口进行实现,查看非公平lock

getState,由于A已经改成了1,越过if代码块,当前线程为B,占用线程为A,两者不相等,越过else if代码块,返回false。!tryAcquire(arg) 值取反 = true, 继续下一个方法 addWaiter()

若是后续线程执行该方法 满足if、elseif 中的条件 最后返回true的话,结束,不执行addWaiter

5.4.4 addWaiter(Node.EXCLUSIVE)

tail是尾节点,当前队列中没值,尾节点为null,故不执行if中的代码,执行enq(node)

节点tail为空,故创建了一个node节点(初始化一个占位哨兵节点),头节点指向node,尾节点指向了头节点

循环第二次时, tail已经有了哨兵(占位)节点的数据,走else代码

将哨兵节点作为当前线程node的前指针指向,将尾节点改为当前线程的node节点,也就是B线程

将哨兵节点的下一指针指向当前线程节点。

返回。

即:双向链表中,第一个节点为虚节点(也交哨兵节点),其实并不存储任何信息,只是占位。真正的第一个有数据的节点,是从第二个节点开始的。

接下来线程C重复B线程做的操作。

C节点执行addWaiter时,tail已经有值了,尾指针目前有指向B,故执行if中的代码。

C的前指针 = prev, prev目前=tail,故C的前指针指向了B。

尾指针指向调整为C。

返回。

源码和三大流程走向

5.4.5 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)

B执行进入,获取前节点,哨兵节点。 

p == head 判断前一节点是否等于头节点,当前B在执行,C还未执行,B的前节点是哨兵节点也就是头节点,故 = true . 再次尝试tryAcquire,看下前一线程是否释放,也就是A是否结束。

如果A没有结束,tryAcquire = false, 执行shouldParkAfterFailedAcquire, 在请求失败之后应该进行阻塞的方法。

此时waitStatus未改变过,初始化时是0,故执行最后的else方法,将前节点的waitStatus改为-1 (前节点目前是哨兵节点),即哨兵节点waitStatus = -1;

假设tryAcquire第二次抢占又失败了,又一次进入shouldParkAfterFailedAcquire方法,ws=Node.SIGNAL, 返回true.

执行sparkAndCheckInterrupt

B被阻塞,排队中,除非占用线程释放锁,B才会被唤醒执行。

C节点同B节点一样,执行到这里也会被阻塞。

5.5 方法unlock()

可以看到又是一个模板方法,需要由子类去实现

A线程要释放,获取state-1 = 0

free会从false改true

设置占用线程为null

设置state = 0;

返回true.

上层方法中if=true以后,

node节点 = 头节点

头节点是哨兵节点,非空,且waitStatus现在是-1, if语句为true,执行unparkSuccessor(头节点)

此时头节点waitStatus = -1, 将waitStatus改为0

获取哨兵节点的下一节点,目前是B节点

B节点不为null,B节点的waitStatus=0, 所以执行解锁。LockSupport.unpark(s.thread);

B被唤醒了。

回到之前B被lock的代码 parkAndCheckInterrupt返回true,B跳出if代码块,继续for循环

 此时B的前节点p是头节点,再次tryAcquire

获取state = 0, 更改0为1, 设置当前占用节点为B线程

setHead(node);//将B作为head节点

 

B节点变成新的哨兵节点,原来的哨兵节点改为null,被GC回收掉

failed = false,后面finally代码块中,cancelAcquire(node);取消排队的代码不会被执行,因为中间没中断过。

结束。

6 总结(思维导图)【2】

7 参考文献

以上内容均来自下方视频,博客记录仅作为个人学习笔记使用

【1】Java面试_高频重点面试题 (第一、二、三季)_ 面试 第1、2、3季_柴林燕_周阳_哔哩哔哩_bilibili

【2】AQS 源码 | ProcessOn免费在线作图,在线流程图,在线思维导图 |

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
AQS(AbstractQueuedSynchronizer)是Java中实现同步的框架,它提供了一种基于FIFO队列的阻塞和唤醒机制。AQS的阻塞队列原理是通过CLH(Craig, Landin, and Hagersten)队列来实现的。 CLH队列是一种虚拟的双向链表,它仅存在节点之间的关联关系,而不存在队列的实例。每个请求共享资源的线程都会被封装成一个CLH队列的节点(Node)。当线程请求共享资源时,它会被添加到CLH队列的尾部,并进入阻塞状态。 当共享资源被占用时,其他线程请求该资源的线程会被放入CLH队列的末尾,即排队等待。这种排队等待的方式可以保证请求资源的线程按照FIFO的顺序获得资源,避免了饥饿现象。当资源释放后,AQS会自动唤醒队列中的下一个线程,使其获得资源并继续执行。 需要注意的是,AQS同步队列(Sync queue)是一个双向链表,包括头节点(head)和尾节点(tail),用于后续的调度。而条件队列(Condition queue)是一个单向链表,只有在使用Condition时才会存在,并且可能会有多个条件队列。 总结一下,AQS实现阻塞队列的原理是通过CLH队列来实现的,当共享资源被占用时,请求资源的线程会被添加到CLH队列中排队等待。当资源释放后,AQS会自动唤醒队列中的下一个线程,使其获得资源并继续执行。同步队列用于后续的调度,而条件队列只在使用Condition时才会存在。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值