首页 > 其他分享 >AQS与CAS分析

AQS与CAS分析

时间:2023-03-04 14:11:06浏览次数:24  
标签:分析 调用 AQS CAS 获取 线程 方法 节点

1. AQS(AbstractQueuedSynchronizer)分析

1.1 说明

AQS(AbstractQueuedSynchronizer)从字面意思来说其是抽象的队列同步器,它是一套实现多线程同步功能的框架,在源码中被广泛使用,尤其是在JUC中,比如 ReentrantLock、Semaphore、CountDownLatch、ThreadPoolExecutor,这些底层使用的都是AQS。在AQS内部封装好了大部分同步需要的逻辑,基本上直接调用就行。

1.2 分析

为了方便分析理解AQS,我们这里可以从ReentrantLock类来进行分析
ReentrantLock
首先先从它的加锁开始:
image
从上面可以看出reentrantLock.lock();它的加锁是通过调用sync.lock();来进行实现的,而sync又是什么呢?我们可以发现其是ReentrantLock的一个内部类,如下:
image
由上面可知sync这个内部类继承了AQS,因此由此可以知道ReentrantLock是通过内部类sync来继承AQS,扩展AQS的功能,然后ReentrantLock通过定义Sync的全局变量引用进而来使用AQS。
Sync在ReentrantLock被继承了两次,分别是公平锁和非公平锁,其中非公平锁的方式如下:
image
由上面可以看到非公平锁的lock中主要有下面两点:

  • 通过CAS设置state成功,表示获取到了锁,然后将当前线程通过setExclusiveOwnerThread()设置为独占式线程。其中setExclusiveOwnerThread()是AQS从AOS那里继承过来的方法。
  • 通过CAS设置state失败,表示当前已被其他线程持有,则进行后面的处理,调用acquire()方法。acquire()方法也是AQS中的方法。
    image
    由上面可知,其大概分为下面几个步骤:
    • 1.tryAcquire(arg)尝试获取锁。image
      从上面可以看出AQS中的这个方法是只抛出了一个异常,所以正常需要我们重写这个方法,ReentrantLock非公平锁对该方法的重写为:
      imageimage
      由上图中可以看出其尝试获取锁的步骤为:先获取当前线程,然后获取锁的状态,如果锁的状态等于0,表示无锁,则利用CAS将当前状态更改,同时将当前线程设为独占式锁,返回true。如果当前状态不等于0,且当前持有的锁的线程为当前线程,则表示可重入锁,则增加状态值,重新设置状态值,返回true。当情况不是上面两种情况中的任何一种情况,则返回false,

    • 2.addWaiter()尝试获取锁失败,则调用该方法,将当前线程作为一个节点加入到等待队列中。imageimage
      这里这两个方法主要是双相队列的创建,其中enq方法是针对没有初始化的队列。

    • 3.acquireQueued()方法主要是处理加入到队列中的节点,通过自旋去尝试获取锁,根据情况将线程挂起或者取消。image
      由图可以看到其步骤为:
      (1)在一个循环中先获取它的前驱节点,判断其前驱节点是否是头节点,如果是头节点则说明其有获取锁的资格,然后会尝试获取锁,获取到锁之后将当前节点设置为头节点,见原先的头节点指向变为null,方便GC,返回当前interrupted结束循环。
      (2)如果没有获取到锁,则执行shouldParkAfterFailedAcquire()方法来判断线程是否应该阻塞,具体方法如下:image

        由图可以看出:
        (2.1)waitStatus(这个的状态值会在后面讲node节点的时候说明)为SIGNAL时则直接返回true。
        (2.2)waitStatus的值大于0,也就是取消状态的时候,会一直向前查找最靠近的非取消节点,然后将其变为当前节点的前置节点,然后返回false重新走```acquireQueued()```尝试获取锁。
        (2.3)waitStatus的值为剩余的情况,则会直接将前驱节点的的waitStatus设为SIGNAL,然后返回false重新走```acquireQueued()```尝试获取锁。
      

      如果shouldParkAfterFailedAcquire()判断当前线程应该阻塞的话,则会调用parkAndCheckInterrupt()方法来执行真正的阻塞代码:
      image
      image
      这里只是调用了LockSupport中的park方法。在LockSupport.park()方法中调用了Unsafe API来执行底层native方法将线程挂起

所以这边ReentantLock加锁的流程基本上算是结束了,其大致流程可以如下所示:

  • 调用其内部类Sync实现的AQS的lock方法
  • 利用CAS设置status获取锁,如果成功获取锁,则将当前线程设置为独占模式,如果没用成功,则调用AQS的模板方法acquire()方法获取锁
  • acquire()通过调用子类自定义实现的tryAcquire获取锁
  • 如果获取锁失败,通过addWaiter方法将当前线程构造为Node节点插入到同步队列队尾
  • 随后使用acquireQueued()方法处理节点,以自选的方法尝试获取锁,如果失败则判断是否需要将当前线程阻塞,如果需要阻塞,最后调用LockSupport中的park方法来实现线程阻塞

PS:上面的是非公平锁的加锁步骤,至于公平锁的加锁步骤大致与非公平锁的相似,只是加锁时是直接用acquire的tryAcquire方法尝试获取锁,以及其tryAcquire方法与非公平锁的有些区别,具体的如下:image
其比非公平锁增加了一步判断是否是第一个的方法。

释放锁的过程:
首先从ReentrantLock.unlock()方法说起:image
可见其调用了sync的release方法,也就是AQS中的release方法:
image
可以看到他会调用tryRelease来判断是否释放锁,而tryRelease也是Sync内部类中重写的方法,如下:
image
可以看到当当前线程为持有线程的话,会判断状态值是否等于0,如果等于0,则将当前独占线程设置为null,更新状态值,如果不为0的话只会更新状态值。
当tryRelease方法判断应该释放锁的话,会调用unparkSuccessor()方法来将后续节点唤醒,如下:image
从上面可以看出,如果当前节点的状态为负值,则会尝试使用CAS清除状态,然后获取当前节点的下一节点,如果不存在下一节点,或者下一节点的状态为已取消,则会从队尾开始往前找,找到不是第一个不是已取消状态的节点,也就是需要唤醒的节点,此时可以确定下一节点不为null,并且其状态不是已取消状态,可以直接调用LockSupport.unpark();方法唤醒节点。此时调用到这个方法就和加锁时的LockSupport.park()方法相似都是调用本地方法了,这里就不做分析了。

Node节点说明:
Node节点就时创建队列的node节点,其中也有节点值,前置节点,后置节点,共享模式标记,独占模式标记等等,具体如下:
image
这里主要说明的时waitStatus的几个状态值。

waitStauts值 描述
CANCELLED(1) 取消状态,表示当前线程因超时或者中断被取消,是一个终结状态。
SIGNAL(-1) 当前现成的后续线程被阻塞或者即将被阻塞,当前线程释放锁或者取消后需要唤醒后续线程。
CONDITION(-2) 当前线程在condition队列中,正在等待条件
PROPAGATE(-3) 用于将唤醒后继线程传递下去,这个状态是为了完善和增强共享锁的唤醒机制。在一个节点成为头节点之前,是不会跃迁为此状态的,也就是其仅适用于头节点
0 以上都不是,表示无锁

2. CAS(Compare And Swap)分析

CAS就是比较与交换,是一种乐观锁的方式。就是在修改值之前会先比较当前值与修改之前的值是否相同,如果相同的话会进行修改为要修改的值,如果不同的话则会不进行更改。
但是这也会有个问题,就是ABA问题,就比如一个值在修改之前是A,然后另一个线程修改为B,然后又被修改为A,此时看起来就像没有进行修改过,解决ABA问题也很简单,就是添加一个版本号,修改过之后就将版本号进行加1。
另外就是其无法保证代码块的原子性,CAS只能保证单个变量的原子性操作,如果要保证多个变量的原子性操作就要使用悲观锁了

标签:分析,调用,AQS,CAS,获取,线程,方法,节点
From: https://www.cnblogs.com/mcj123/p/17166032.html

相关文章