并发编程AQS(第一章)

群里的小伙伴艾特说想看看AQS,本来是不想写的,因为这块的内容多且复杂,三言两语很难讲清楚。这里的话 我只做了一些概念性的东西,具体的实现,还需要自己来做,如果没有头绪的话,可以根据我说的原理一步步百度再了解。

AQS 定义

AQS是AbstractQueuedSynchronizer的简称。AQS提供了一种实现阻塞锁和一系列依赖FIFO等待队列的同步器的框架。它为不同场景提供了实现锁及同步机制的基本框架,为同步状态的原子性管理、线程的阻塞、线程的解除阻塞及排队管理提供了一种通用的机制。此外,AQS是为了实现依赖于先进先出等待队列 的阻塞锁和相关同步器(信号量、事件,等等)提供一个框架它使用了一个原子的int value status来作为同步器的状态值(如:独占锁。1代表已占有,0代表未占有),通过该类提供的原子获取或修改state方法(getState, setState and compareAndSetState),我们可以把它作为同步器的基础框架类来实现各种同步器。 AQS还定义了一个实现了Condition接口的ConditionObject内部类。Condition 将 Object 监视器方法(wait、notify 和 notifyAll)分解成截然不同的对象,以便通过将这些对象与任意 Lock 实现组合使用,为每个对象提供多个等待 set (wait-set)。其中,Lock 替代了 synchronized 方法和语句的使用,Condition 替代了 Object 监视器方法的使用。
简单来说,就是Condition提供类似于Object的wait、notify的功能signal和await,都是可以使一个正在执行的线程挂起(推迟执行),直到被其他线程唤醒。但是Condition更加强大,如支持多个条件谓词、保证线程唤醒的顺序和在挂起时不需要拥有锁。

state状态

上面定义中,我们讲到AQS提供的原子获取或修改state的三种方法。根据字面意思 可以知道getState, setState 分别对应着获取state 修改state。这两个方法都是比较常规的getset方法所以不做过多解释。如果只看这两个方法,让你去修改state状态。这个时候可能会出现一个问题,我们很多情况下,都必须要先获取state进行判断,然后再修改state。这个时候如果要采用if判断 然后再set方法的话,在并发情况下很有可能出现问题。对于这种情况,我们可以使compareAndSetState方法,这个方法是AQS采用CAS操作提供原子性的。可以完美避免这个问题,这个方法事实上也是调用的Unsafe类提供的一个native方法。

资源共享方式

AQS共定义两种资源共享方式:
Exclusive(独占): 字面意思,只能有一个线程执行,假设state初始状态为0,表示未锁定状态。线程A想使用该资源就把state修改为了1,那么线程B来访问资源时发现state是1并不是0他就会被AQS送入等待队列,直到线程A将该资源设置为0
Share(共享):假设state初始状态为N,当有线程来访问后N就减少1个,直到N=0 这时就会阻塞新的线程来访问资源。当某一个线程执行完毕后会将state+1,相当于释放了该线程持有的锁。这样新的线程就可以继续访问该资源。
打个比方,独占模式就是说,我在群里发了个红包 1包200的,大黄抢到了,别的人你就没有了。 共享模式就是说,我在群里发了个红包,20包200的普通红包,大黄蛋蛋等人都抢到了。但是一旦这个20包被抢完就没得了,只能等我再发一个红包。

AQS两个队列

AQS 底层是实现了两种队列 一个是条件队列,一个是同步队列。具体是怎么回事呢?我们来举个小栗子。 同步队列的话 就是说,比方我们去做核酸检测,做核酸的时候我们需要排队,间隔一米距离,大家都排队,但是负责做核酸的窗口只有一个,这个时候,每个人都在自旋(也就是不断的看自己是不是第一个),如果是第一个的话,那就去索要做核酸的权限,当正在做核酸的人结束了,你就可以去做核酸了,然后你后面的第一个人就开始索要做核酸的权限,等待。这就是同步队列。 但是做核酸又分急缓(不要杠,虽然现实没有,但是咱是打比方呢不是?),如果说你不是很急,那我就给你放到条件队列里去,等急切的人做完核酸了,再把不急的人放到同步队列里去 按之前的逻辑去获取。
这里有个比较重要的点,要说一下。同步队列和条件队列都是由一个个Node组成的。AQS内部有一个静态内部类Node。

这个静态类里有个重中之重的重要方法—acquire在这里插入图片描述
该方法首先调用了tryAcquire方法尝试一次性获取资源,我们可以看到!tryAcquire(arg)是个取反的操作,也就是只有直接无法获取当前资源时才执行下一步。然而AQS并没有提供实现方式。具体实现方式需要继承该框架的子类去实现。至于具体如何才能获取锁就是用户自定义的事情了。当通过tryAcquire()方法获取线程失败后,使用addWaiter()将该线程放入队列的尾部。然后进入等待状态,知道其他线程释放资源后唤醒此线程,自己在拿到资源,然后就能干自己的事情了。这就和买好了票排到了队尾等待前面的人一个一个完成任务后轮到自己。实际上该方法一直在自旋(发呆)直到自己变成了头结点,如果变成头结点后该线程已经中断则中断该线程的执行。
acquireQueued方法的主要流程是节点进入队列尾巴后,检查状态,找到安全的等待位置等待然后调用park()让线程进入wait状态,等待unPark()或者interrput()唤醒。被唤醒后,看自己是不是有资格拿到资源。如果拿到,将head指向当前节点。并返回从入队到拿到的整个过程中是否被中断过。如果没有则回到1号流程循环执行。下面我把这块源码详细讲一下。为了方便我贴了一张图然后把源码复制出来写注释
在这里插入图片描述

final boolean acquireQueued(final Node node, int arg) {
//这里标记是否拿到资源,true是没拿到,注意true是没拿到
        boolean failed = true;
        try {
        // 标记等待过程中是否被中断过
            boolean interrupted = false;
            //这个地方进行自选
            for (;;) {
            // 获取当前节点的前置节点
                final Node p = node.predecessor();
                // 如果自己的前置节点是头结点,并且自己获取到了资源权限
                if (p == head && tryAcquire(arg)) {
                //那就把自己设置成头节点
                    setHead(node);
  // 将前置节点的后续节点强连接断开相当于自己已经是头结点了不需要前置节点了。帮助GC回收垃圾
                    p.next = null; // help GC
                    //标记已经获取到节点
                    failed = false;
                    // 判断该线程是否被中断过。
                    return interrupted;
                }
                // 这两个方式是检查状态、和让线程休息 下面会详细讲解这两个方法的作用
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    //如果等待过程中被中断过,哪怕只有那么一次,就将interrupted标记为true
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

结尾

这一块先整理这么多吧,后续还会再出续章。
在这里插入图片描述

  • 4
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值