队列同步器AQS原理分析及具体实现

7 篇文章 0 订阅

Java中的并发编程很多都是以队列同步器AbstractQueuedSynchronizer为基础的, 例如ReentrantLock,CountDownLatch等。

下面介绍其构成以及相应的实现。

构成

private volatile int state;

AbstractQueuedSynchronizer中通过一个int类型的成员变量state来表示同步状态,该变量使用volatile来修饰,保证多线程之间的可见性。

以及一个内置的队列来完成资源排队。

其中对于state有如下几个操作方法:

  • getState() 获取同步状态
  • setState() 设置同步状态
  • compareAndSetState() 使用CAS设置同步状态,该方法能保证状态设置的原子性
具体原理

同步器依赖内部的一个双向队列来完成同步状态的管理。该队列遵循先入先出的原则。

当线程获取同步状态失败时,同步器会执行以下步骤:

  1. 构造一个节点Node, 该节点包含当前线程、当前线程的等待状态、前驱节点引用、后驱节点引用等信息
  2. 将该节点加入到队列当中(加入到队列的尾结点)
  3. 阻塞该线程

当释放同步状态时,只会将首节点的线程唤醒, 使其尝试获取同步状态。

image-20210407214734999

同步器中包含了指向头结点和尾结点的应用head, tail, 这两个节点作用分别如下:

  • 首节点是获取同步状态成功的节点, 首节点的线程在释放同步状态时, 会唤醒后继节点, 当后继节点成功获取到同步状态时,就会将自己设置为首节点, 以此类推。
  • 因为新加入的节点都会放在尾节点, 所以为了线程安全,提供了一个利用CAS设置尾结点的方法compareAndSetTail()。
同步状态获取

同步状态获取的方式分为两种:

  • 独占式获取同步状态, 在同一时刻只能有一个线程能获取到同步状态,其他线程都将会被阻塞,例如文件的写操作。 对应acquire()方法
  • 共享式获取同步状态, 在同一时刻允许有多个线程获取到同步状态,例如文件的读操作。对应acquireShared()方法

下图是同步器独占式获取同步状态的流程图。

image-20210407220327281

ReentrantLock

可重入锁。支持一个线程对资源的重复加锁。

同时, 该锁还支持是否公平的获取锁。注意,synchronized支持的锁也是可重入的,因为它会在对象头存储获取锁的线程ID。

几个问题:

  1. ReentrantLock是如何实现可重入的呢?

首先ReentrantLock组合了同步器AbstractQueuedSynchronizer来实现锁的功能。其中state在ReentrantLock用来表示线程获取锁的次数,当线程获取锁时:

​ 需要判断当前线程是否是获取锁的线程, 如果是则将state值加1, 并返回获取锁成功的标识, 这样就能保证可重入。

  1. 锁是如何释放的?

答案是仍然使用state来进行释放。 当调用unlock()释放锁时, 同步器会将state的值减1, 只有当state的值为0时, 才能返回锁释放成功的标识。

  1. 公平锁时如何实现的?

ReentrantLock默认是非公平锁, 在获取锁时,只要使用CAS设置同步状态成功, 那么就表示该线程就获取锁成功。

对于公平锁,在获取同步状态时, 还需要判断同步队列中当前节点是否有前驱节点, 如果有, 则说明有更早的线程在等待获取锁, 则当前线程需要加入到等待队列的尾结点, 等待之前所有线程获取节点成功并释放之后才能尝试获取锁。

ReentrantReadWriteLock

可重入的读写锁。维护了一个读锁和一个写锁。其中读锁是共享锁, 同一时刻可以有多个线程拥有,写锁是排他锁, 同一时刻只能有一个写锁。

其也是组合AbstractQueuedSynchronizer来实现的。具体如何实现的呢?

关键也是state变量的设计, 将state变量按位拆成2部分, 高16位表示读状态, 低16位表示写状态, 在进行读锁和写锁的操作时,和ReentrantLock类似,对这两个状态进行加减运算。

写锁获取流程:

  1. 如果当前线程已经获取了写锁, 则增加写状态,并返回获取写锁成功的标识。此处保证了可重入
  2. 如果有其他线程获取了读锁或写锁,则当前线程进入等待状态,等待其他线程的写锁和读锁全部释放完毕, 再去尝试获取。此处保证了排他性。

读锁获取流程:

  1. 如果有其他线程获取了写锁, 则当前线程进入等待状态。
  2. 如果没有其他线程获取了写锁,读锁总是能保证成功获取。特别的,如果当前线程已经获取了读锁, 则增加读状态(保证可重入性),并返回获取读锁成功的标识。
CountDownLatch

某个任务的执行,需要等到多个线程都执行完毕之后才可以进行。 该场景可以使用CountDownLatch来实现。

其也是组合AbstractQueuedSynchronizer来实现的。关键是state的设计。state被用来表示需要等待的线程个数

在CountDownLatch中,主要涉及到如下三个方法:

  • 构造函数中count参数, 指定需要等待的线程个数, 也就是state变量
  • countDown()方法, 每调用一次,state减1
  • await()方法, 等待, 只有当state等于0时, 才会从该方法处返回, 否则会一直阻塞。

注意, countDown()方法最好写在finally中, 防止发生死锁。因为如果在调用countDown()之前程序发生了异常, 导致该方法没有执行,那么state变量就永远不可能为0, 此时调用await()方法的线程则会永远阻塞在该处。

或者使用await(long timeout, TimeUnit unit), 加入超时参数, 当达到超时时间自动返回。

参考资料:

为0, 此时调用await()方法的线程则会永远阻塞在该处。

或者使用await(long timeout, TimeUnit unit), 加入超时参数, 当达到超时时间自动返回。

参考资料:

  1. 《Java并发编程的艺术》
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值