学习
实践
活动
工具
TVP
写文章

Java并发编程之AbstractQueuedSynchronizer

No.1 源码解析

AbstractQueuedSynchronizer,队列同步器,简称AQS,它是java并发用来构建锁或者其他同步组件的基础框架。

AQS本身是没有实现任何同步接口的,它仅仅只是定义了同步状态的获取和释放的方法来供自定义的同步组件的使用。

提供一个用于实现依赖于先进先出(FIFO)等待队列的阻塞锁和相关同步器(信号量,事件等)的框架。

可以作为排他模式也可以作为共享模式,当它被定义为一个排他模式时,其他线程对其的获取就被阻止,而共享模式对于多个线程获取都可以成功。

使用一个int类型的值来表示同步状态:

提供了三个方法来对状态进行操作:

AQS的使用依靠继承来完成,子类通过继承自AQS并实现所需的方法来管理同步状态。例如ReentrantLock,CountDownLatch等。

1.1、Node节点

AQS的开始提到了其实现依赖于一个FIFO队列,那么队列中的元素Node就是保存着线程引用和线程状态的容器,每个线程对同步器的访问,都可以看做是队列中的一个节点。

waitStatus 表示节点的状态。其中包含的状态有:

CANCELLED,值为1,表示当前的线程被取消;

SIGNAL,值为-1,表示当前节点的后继节点包含的线程需要运行,也就是unpark;

CONDITION,值为-2,表示当前节点在等待condition,也就是在condition队列中;

PROPAGATE,值为-3,表示当前场景下后续的acquireShared能够得以执行;

值为0,表示当前节点在sync队列中,等待着获取锁。

prev

前驱节点,比如当前节点被取消,那就需要前驱节点和后继节点来完成连接。

next

后继节点。

nextWaiter

存储condition队列中的后继节点。

thread

入队列时的当前线程。

SHARED

表示节点正在共享模式中等待

EXCLUSIVE

标记表示节点正在独占模式下等待

1.2、head节点和tail节点

sync队列的头结点head

sync队列的尾节点tail

其中头结点不存储Thread,仅保存next结点的引用。

对于锁的获取,请求形成节点,将其挂载在尾部,而锁资源的转移(释放再获取)是从头部开始向后进行。

对于同步器维护的状态state,多个线程对其的获取将会产生一个链式的结构。

1.3、state变量

AQS中有一个state变量,该变量对不同的子类实现具有不同的意义,对ReentrantLock来说,它表示加锁的状态:

无锁时state=0,有锁时state>0;

第一次加锁时,将state设置为1;

由于ReentrantLock是可重入锁,所以持有锁的线程可以多次加锁,经过判断加锁线程就是当前持有锁的线程时(即exclusiveOwnerThread==Thread.currentThread()),即可加锁,每次加锁都会将state的值+1,state等于几,就代表当前持有锁的线程加了几次锁;

解锁时每解一次锁就会将state减1,state减到0后,锁就被释放掉,这时其它线程可以加锁;

当持有锁的线程释放锁以后,如果是等待队列获取到了加锁权限,则会在等待队列头部取出第一个线程去获取锁,获取锁的线程会被移出队列;

1.4、API说明

在排它模式下,获取状态

这个方法的实现需要查询当前状态是否允许获取,然后再进行获取(使用compareAndSetState来做)状态。

在排它模式下,状态是否被占用

在排它模式下,释放状态

共享的模式下,获取状态

共享的模式下,释放状态

1.5、插入节点

AQS提供基于CAS的设置尾节点的方法:

需要传递当前线程认为的尾节点和当前节点,设置成功后,当前节点与尾节点建立关联。

1.6、插入删除

同步队列遵循FIFO,首节点是获取同步状态成功的节点,首节点的线程在释放同步状态之后将会唤醒后继节点,后继节点将会在获取同步状态成功的时候将自己设置为首节点。

设置首节点是由获取同步状态成功的线程来完成,因为每次只会有一个线程能够成功的获取到同步状态,所以,设置首节点并不需要CAS来保证。

No.2 同步状态的获取与释放2.1、排他模式下,同步状态的获取

调用tryAcquire方法尝试获取同步状态;

如果获取不到同步状态,将当前线程构造成节点Node并加入同步队列;

再次尝试获取,如果还是没有获取到那么将当前线程从线程调度器上摘下,进入等待状态。

acquire方法的整个执行流程如下:

当前线程获取同步状态并执行了相应的逻辑之后,就需要释放同步状态,让后续节点可以获取到同步状态,调用方法release(int arg)方法可以释放同步状态。

注意:AQS还提供了两种情况的获取同步锁的方式:超时方式和可中断方式

方式一:超时获取同步锁的方法

即在指定的时间段内可以获取到同步状态返回true,否则返回false。

方式二:可被外界中断的获取同步锁的方式,

即在外界对当前线程中断了,线程获取锁的这个操作能够及时响应中断并且提前返回。

判断当前线程是否被中断,如果已经被中断,抛出InterruptedException异常并将中断标志位置为false;

获取同步状态,获取成功并返回,获取不成功调用doAcquireInterruptibly(int arg)排队等待

2.2、排他模式下,同步状态的释放

尝试释放状态,tryRelease保证将状态重置回去,同样采用CAS来保证操作的原子性;

释放成功后,调用unparkSuccessor唤醒当前节点的后继节点线程。

继续来看unparkSuccessor唤醒方法的具体实现:

取出当前节点的next节点,将该节点线程唤醒,被唤醒的线程获取同步状态。这里主要通过LockSupport的unpark方法唤醒线程。

2.3、共享模式下,同步状态的获取

调用tryAcquireShared(int arg)方法尝试获取同步状态:

tryAcquireShared方法返回值 > 0时,表示能够获取到同步状态;

获取失败调用doAcquireShared(int arg)方法进入同步队列

注意:AQS还提供了两种情况的获取同步锁的方式:超时方式和可中断方式

方式一:超时获取同步锁的方法

即在指定的时间段内可以获取到同步状态返回true,否则返回false。

具体流程参考排他模式下的超时方式。

方式二:可被外界中断的获取同步锁的方式,

即在外界对当前线程中断了,线程获取锁的这个操作能够及时响应中断并且提前返回。

判断当前线程是否被中断,如果已经被中断,抛出InterruptedException异常并将中断标志位置为false;

获取同步状态,获取成功并返回,获取不成功调用doAcquireSharedInterruptibly(int arg)排队等待

具体流程参考排他模式下的超时方式。

2.4、共享模式下,同步状态的释放

调用tryReleaseShared方法释放状态;

调用doReleaseShared方法唤醒后继节点;

No.3 实战演练3.1、需求:

设计一个AQS同步器,该工具在同一时刻,只能有两个线程能够访问,其他的线程阻塞。

3.2、设计:

针对“有两个线程能够访问”,则必然是共享模式,需要实现tryAcquireShared和tryReleaseShared方法。

设定一个初始状态为2,每一个线程获取一次就-1,正确的状态为:0,1,2

通过锁来控制资源访问,因此需要implements Lock。

3.3、实现:

·end·

- 如果喜欢,快分享给你的朋友们吧 -

我们一起愉快的玩耍吧

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20180831G1SMYF00?refer=cp_1026
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码关注腾讯云开发者

领取腾讯云代金券