AQS以及其相关同步器的源码解析

概念

AQSAbstractQueuedSynchronizer)抽象的队列同步器。是用来构建锁或者其他同步器组件的重量级基础框架以及整个JUC体系的基石。通过内置的 FIFO 队列(先入先出队列)来完成资源获取线程的排队工作,将每条要去抢占资源的线程封装成一个 Node 节点来实现锁的分配,并通过一个 volatile 类型的 int 类型变量表示持有锁的状态,使用 CAS 完成对 state 值的修改。

所以我对AQS简单粗暴的理解:AQS = state + FIFO队列。

其中NodeCLH队列中的节点,是AQS中最基本的数据结构。

Node = waitStatus + Thread

AQS有关的类:

  • ReentrantLock
  • CountDownLatch
  • ReentrantReadWriteLock
  • Semephore
  • CyclicBarrier
  • .......

这些类的底层都有继承了AQS,所以可以说AQSJUC中最重要的基石。

下面为AQS相关的UML图:

ReentrantLock

ReentrantLock可重入锁,指一个线程可以对一个临界资源重复加锁。为独占锁,不支持共享。

ReentrantLock提供两个构造方法,有参构造是根据参数创建公平锁或非公平锁,而无参构造方式默认是非公平锁,因为非公平锁的性能高,大部分业务使用的都是非公平锁。

  1. public ReentrantLock() {
  2. sync = new NonfairSync();
  3. }
  4. public ReentrantLock(boolean fair) {
  5. sync = fair ? new FairSync() : new NonfairSync();
  6. }

所以可以看到,ReentrantLock有两种加锁方式:公平锁(FairSync)与非公平锁(NonfairSync)。

使用方式

  1. public class AQSDemo {
  2. // static ReentrantLock lock = new ReentrantLock();
  3. static ReentrantLock lock = new ReentrantLock(true);
  4. static int num = 10;
  5. public static void main(String[] args) {
  6. new Thread(AQSDemo::test, "线程1").start();
  7. new Thread(AQSDemo::test, "线程2").start();
  8. new Thread(AQSDemo::test, "线程3").start();
  9. }
  10. static void test() {
  11. String name = Thread.currentThread().getName();
  12. while (num > 1) {
  13. lock.lock();
  14. try{
  15. num--;
  16. System.out.printf("%s:%d%n", name, num);
  17. } finally {
  18. lock.unlock();
  19. }
  20. }
  21. }
  22. }

上面这段代码,非公平锁和公平锁的打印结果是不同的。

非公平锁结果:

  1. 线程19
  2. 线程18
  3. 线程17
  4. 线程16
  5. 线程15
  6. 线程14
  7. 线程13
  8. 线程12
  9. 线程11
  10. 线程20
  11. 线程3:-1

公平锁结果:

  1. 线程19
  2. 线程28
  3. 线程37
  4. 线程16
  5. 线程25
  6. 线程34
  7. 线程13
  8. 线程22
  9. 线程31
  10. 线程10
  11. 线程2:-1

源码

ReentrantLock的内部类Sync,该类实现了AQS。又有两个子类实现了Sync类:FairSyncNonfairSync

公平锁:讲究的是先来后到,先进先出。如果这个锁的等待队列已经有线程在等待,那么当前线程就会进入等待队列当中排队。

非公平锁:没有排队的概念,谁抢到是谁的。是需要抢占的锁。

公平锁和非公平锁的不同就是,非公平锁首先会调用CASstate从0改为1,成功则表示获取到了锁,使用这个setExclusiveOwnerThread方法记录下当前占用state的线程;否则与公平锁一样调用acquire方法获取锁。acquireAQS中提供的模板方法:

  1. public final void acquire(int arg) {
  2. if (!tryAcquire(arg) &&
  3. acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
  4. selfInterrupt();
  5. }

tryAcquire 尝试着获取 state,如果成功,跳过后面的步骤;如果失败,则执行 acquireQueued 方法将线程加入 CLH 等待队列中。

tryAcquire

非公平锁和公平锁的实现方式有些不同。

首先看看非公平锁的tryAcquire

  1. protected final boolean tryAcquire(int acquires) {
  2. return nonfairTryAcquire(acquires);
  3. }
  4. /**
  5. * Performs non-fair tryLock. tryAcquire is implemented in
  6. * subclasses, but both need nonfair try for trylock method.
  7. */
  8. final boolean nonfairTryAcquire(int acquires) {
  9. final Thread current = Thread.currentThread();
  10. int c = getState();
  11. if (c == 0) {
  12. // 如果 c(锁状态)为0,表示此时的资源是空闲的,则使用CAS获取锁,记录当前占用锁的线程,并返回
  13. // 这里可以看出是非公平锁,因为所有线程都是用CAS获取锁的,不需要排队
  14. if (compareAndSetState(0, acquires)) {
  15. setExclusiveOwnerThread(current);
  16. return true;
  17. }
  18. }
  19. // 如果已有线程获得锁,且该线程再次获得了锁,获取资源数 +1
  20. // 这里可以印证 ReentrantLock 是可重入锁
  21. else if (current == getExclusiveOwnerThread()) {
  22. int nextc = c + acquires;
  23. if (nextc < 0) // overflow
  24. throw new Error("Maximum lock count exceeded");
  25. // 更新state,这里不需要用CAS更新,此时的锁就是当前线程占有的,其他线程没机会执行。此时更新state是线程安全的
  26. setState(nextc);
  27. return true;
  28. }
  29. return false;
  30. }

然后是公平锁的tryAcquire。基本上和非公平锁的代码是一样的,区别在于加锁的时候需要判断是否已经有队列存在,没有采取加锁,有则直接返回false

  1. /**
  2. * Fair version of tryAcquire. Don't grant access unless
  3. * recursive call or no waiters or is first.
  4. */
  5. protected final boolean tryAcquire(int acquires) {
  6. final Thread current = Thread.currentThread();
  7. int c = getState();
  8. if (c == 0) {
  9. // 判断是否已经有队列存在,没有才去加锁
  10. if (!hasQueuedPredecessors() &&
  11. compareAndSetState(0, acquires)) {
  12. setExclusiveOwnerThread(current);
  13. return true;
  14. }
  15. }
  16. else if (current == getExclusiveOwnerThread()) {
  17. int nextc = c + acquires;
  18. if (nextc < 0)
  19. throw new Error("Maximum lock count exceeded");
  20. setState(nextc);
  21. return true;
  22. }
  23. return false;
  24. }
  25. public final boolean hasQueuedPredecessors() {
  26. Node t = tail;
  27. Node h = head;
  28. Node s;
  29. // 首尾不相等,且head节点线程不是当前线程则表示需要进入队列
  30. return h != t &&
  31. ((s = h.next) == null || s.thread != Thread.currentThread());
  32. }

acquireQueued

如果tryAcquire尝试加锁失败,则会执行acquireQueued这个方法,也就是将线程加入到FIFO等待队列。

  1. public final void acquire(int arg) {
  2. if (!tryAcquire(arg) &&
  3. acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
  4. selfInterrupt();
  5. }

可以看到调用了addWaiter方法,将当前线程的Node节点入队。从Node.EXCLUSIVE可以看出,这个节点是独占模式。

addwaiter代码:

  1. private Node addWaiter(Node mode) {
  2. Node node = new Node(Thread.currentThread(), mode);
  3. Node pred = tail;
  4. // 尾节点不为null表示已经存在队列,直接将当前线程作为尾节点
  5. if (pred != null) {
  6. node.prev = pred;
  7. // 如果尾节点存在,使用CAS将等待线程入队
  8. if (compareAndSetTail(pred, node)) {
  9. pred.next = node;
  10. return node;
  11. }
  12. }
  13. // 如果节点是空的,则执行 enq 方法
  14. enq(node);
  15. return node;
  16. }

enq方法代码:

  1. private Node enq(final Node node) {
  2. for (;;) {
  3. Node t = tail;
  4. // 判断尾节点是否为空,如果是空的,说明FIFO队列的head、tail还未构建,需要先构建head节点,然后使用CAS入队
  5. if (t == null) { // Must initialize
  6. if (compareAndSetHead(new Node()))
  7. tail = head;
  8. } else {
  9. // 尾节点不为空,将等待线程入队
  10. node.prev = t;
  11. if (compareAndSetTail(t, node)) {
  12. t.next = node;
  13. return t;
  14. }
  15. }
  16. }
  17. }

问:使用 CAS 创建 head 节点的时候只是简单调用了 new Node() 方法,并不像其他节点那样记录 thread,为什么?

答:因为 head 节点只代表当前有线程占用了 state,至于占用 state 的是哪个线程,之前有调用了 setExclusiveOwnerThread 方法,即已经记录在 exclusiveOwnerThread 属性里了。

现在有个问题,就是入队后的线程,要如何处理?是马上阻塞吗?

如果是马上阻塞意味着线程从运行态转为阻塞态,涉及到用户态向内核态的切换,而且唤醒后也要从内核态转为用户态,开销相对比较大。所以AQS对这种入队的线程采用的方式,是通过让它们自旋来竞争锁,

但是当前锁是独占锁,如果锁一直被被某个线程占有,其他等待队列中的线程一直自旋没太大意义,反而会占用CPU影响性能。

所以更合适的方式是,它们自旋一两次次数,竞争不到锁后识趣地阻塞以等待前置节点释放锁后再来唤醒它。

另外如果锁在自旋过程中被中断了,或者自旋超时了,应该处于「取消」状态。

所以,在Node节点中就定义了waitStatus这个变量,来记录每个Node可能所处的状态。在前面的概念介绍时,有对各个状态进行了展示。

  1. // 由于超时或中断,节点已被取消
  2. static final int CANCELLED = 1;
  3. // 节点阻塞(park)必须在其前驱结点为 SIGNAL 的状态下才能进行,如果结点为 SIGNAL,则其释放锁或取消后,可以通过 unpark 唤醒下一个节点,
  4. static final int SIGNAL = -1;
  5. // 表示线程在等待条件变量(先获取锁,加入到条件等待队列,然后释放锁,等待条件变量满足条件;只有重新获取锁之后才能返回)
  6. static final int CONDITION = -2;
  7. // 表示后续结点会传播唤醒的操作,共享模式下起作用
  8. static final int PROPAGATE = -3;
  9. //等待状态
  10. volatile int waitStatus;

接下来才真正到了acquireQueued的代码,加锁的核心逻辑:

  1. final boolean acquireQueued(final Node node, int arg) {
  2. boolean failed = true;
  3. try {
  4. boolean interrupted = false;
  5. for (;;) {
  6. final Node p = node.predecessor();
  7. // 如果前一个节点是 head,且尝试自旋获取锁成功
  8. if (p == head && tryAcquire(arg)) {
  9. // 将 head 结点指向当前节点,原 head 结点出队。这样原 head 不可达,会被GC
  10. setHead(node);
  11. p.next = null; // help GC
  12. failed = false;
  13. return interrupted;
  14. }
  15. // 如果前一个节点不是 head 或者竞争锁失败,则判断锁是否应该停止自旋进入阻塞状态
  16. // s为true表示线程可以进入阻塞中断,调用parkAndCheckInterrupt让线程阻塞
  17. if (shouldParkAfterFailedAcquire(p, node) &&
  18. parkAndCheckInterrupt())
  19. interrupted = true;
  20. }
  21. } finally {
  22. if (failed)
  23. // 如果线程自旋中因为异常等原因获取锁最终失败,则取消加锁节点
  24. cancelAcquire(node);
  25. }
  26. }
  27. private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
  28. int ws = pred.waitStatus;
  29. // 如果前置节点的状态是 SIGNAL,则当前节点可以进入阻塞状态
  30. if (ws == Node.SIGNAL)
  31. return true;
  32. // 如果前置节点为取消状态,则前置节点需要移除
  33. if (ws > 0) {
  34. do {
  35. node.prev = pred = pred.prev;
  36. } while (pred.waitStatus > 0);
  37. pred.next = node;
  38. } else {
  39. // 将前置节点的 waitStatus 设为 SIGNAL
  40. compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
  41. }
  42. return false;
  43. }

unlock

无论是非公平锁还是公平锁,解锁中也是调用AQS中的模板方法:

  1. public final boolean release(int arg) {
  2. // 尝试释放锁是否成功
  3. if (tryRelease(arg)) {
  4. Node h = head;
  5. if (h != null && h.waitStatus != 0)
  6. // 锁释放成功后,唤醒 head 之后的节点,让它来竞争锁
  7. unparkSuccessor(h);
  8. return true;
  9. }
  10. return false;
  11. }

tryRelease是定义在ReentrantLock的内部类Sync中的:

  1. protected final boolean tryRelease(int releases) {
  2. int c = getState() - releases;
  3. // 只有持有锁的线程才能释放锁,所以如果当前锁不是持有锁的线程,则抛异常
  4. if (Thread.currentThread() != getExclusiveOwnerThread())
  5. throw new IllegalMonitorStateException();
  6. boolean free = false;
  7. // 说明线程持有的锁全部释放了,需要释放 exclusiveOwnerThread 的持有线程
  8. if (c == 0) {
  9. free = true;
  10. setExclusiveOwnerThread(null);
  11. }
  12. setState(c);
  13. return free;
  14. }

unparkSuccessor唤醒节点代码:


  1. private void unparkSuccessor(Node node) {
  2. // 获取节点的 waitStatus
  3. int ws = node.waitStatus;
  4. if (ws < 0)
  5. compareAndSetWaitStatus(node, ws, 0);
  6. // 以下逻辑是取队列第一个非取消状态的结点,并将其唤醒
  7. Node s = node.next;
  8. if (s == null || s.waitStatus > 0) {
  9. s = null;
  10. for (Node t = tail; t != null && t != node; t = t.prev)
  11. if (t.waitStatus <= 0)
  12. s = t;
  13. }
  14. if (s != null)
  15. LockSupport.unpark(s.thread);
  16. }

公平锁和非公平锁的区别就在于,非公平锁不管是否有线程在排队,先抢锁;而公平锁则会判断是否存在队列,有线程在排队则直接进入队列排队。

另外线程在park被唤醒后非公平锁还会抢锁,公平锁仍然需要排队。所以非公平锁的性能比公平锁高很多,大部分情况下我们使用非公平锁即可。

总结

ReentrantLock是可重入的互斥锁,虽然具有与synchronized相同功能,但是会比synchronized更加灵活,且能提供更多的方法。与synchronized的异同如下:

ReentrantLock synchronized
锁实现机制 依赖AQS 监视器模式
灵活性 支持响应中断、超时、尝试获取锁 无法设置超时时间,无法提供外部方法
释放形式 必须显示调用unlock()释放锁 自动释放监视器
锁类型 公平锁和非公平锁 非公平锁
条件队列 可关联多个条件队列 关联一个条件队列
可重入性 可重入 可重入

ReentrantLock底层基于AQS实现,UML关系图如下。

Semaphore

使用方式

Semaphore(信号量)是AQS共享模式的一个应用,允许多个线程同时对共享资源进行操作,并且可以有效的控制并发数,利用它可以很好的实现流量控制。Semaphore提供了两个带参构造器,这两个构造器都必须传入一个初始的许可证数量,使用构造器一构造出来的信号量在获取许可证时会采用非公平方式获取,使用构造器二可以通过参数指定获取许可证的方式(公平或者非公平)。


  1. public Semaphore(int permits) {
  2. sync = new NonfairSync(permits);
  3. }
  4. public Semaphore(int permits, boolean fair) {
  5. sync = fair ? new FairSync(permits) : new NonfairSync(permits);
  6. }

Semaphore提供了很多方法来获取许可,这里列举一下:

  • void acquire() throws InterruptedException 获取一个许可,会阻塞等待其他线程释放许可
  • void acquire(int permits) throws InterruptedException 获取指定的许可数,会阻塞等待其他线程释放
  • void acquireUninterruptibly() 获取一个许可,会阻塞等待其他线程释放许可,可以被中断
  • void acquireUninterruptibly(int permits) 获取指定的许可数,会阻塞等待其他线程释放许可,可以被中断
  • boolean tryAcquire() 尝试获取许可,不会进行阻塞等待
  • boolean tryAcquire(int permits) 尝试获取指定的许可数,不会阻塞等待
  • boolean tryAcquire(long timeout, TimeUnit unit) 尝试获取许可,可指定等待时间
  • boolean tryAcquire(int permits, long timeout, TimeUnit unit) 尝试获取指定的许可数,可指定等待时间

释放许可则是使用了release方法。

源码

获取许可的源码先浅看这四个:

  1. public void acquire() throws InterruptedException {
  2. sync.acquireSharedInterruptibly(1);
  3. }
  4. public void acquireUninterruptibly() {
  5. sync.acquireShared(1);
  6. }
  7. public boolean tryAcquire() {
  8. return sync.nonfairTryAcquireShared(1) >= 0;
  9. }
  10. public boolean tryAcquire(long timeout, TimeUnit unit) throws InterruptedException {
  11. return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
  12. }

上面的是Semaphore提供的默认获取许可证操作,每次只获取一个许可证。

acquire

先看看acquire获取许可里面的acquireSharedInterruptibly这个方法。这个方法是AQS里面的方法。

  1. public final void acquireSharedInterruptibly(int arg)
  2. throws InterruptedException {
  3. // 如果线程中断抛出异常
  4. if (Thread.interrupted())
  5. throw new InterruptedException();
  6. // 尝试获取锁(共享锁)
  7. if (tryAcquireShared(arg) < 0)
  8. doAcquireSharedInterruptibly(arg);
  9. }

其中tryAcquireShared也是在AQS里面是抽象方法,FairSyncNonfairSync这两个派生类实现了该方法的逻辑。FairSync实现的是公平获取的逻辑,而NonfairSync实现的非公平获取的逻辑。

  1. // 公平锁获取逻辑
  2. static final class FairSync extends Sync {
  3. // ...省略代码
  4. protected int tryAcquireShared(int acquires) {
  5. for (;;) {
  6. // 如果队列中有线程排队则获取锁失败
  7. if (hasQueuedPredecessors())
  8. return -1;
  9. // 获取可用许可
  10. int available = getState();
  11. // 获取剩余可用许可
  12. int remaining = available - acquires;
  13. // 如果剩余许可小于0则直接返回,否则先更新许可状态之后再返回
  14. if (remaining < 0 ||
  15. compareAndSetState(available, remaining))
  16. return remaining;
  17. }
  18. }
  19. }
  20. // 非公平锁获取逻辑
  21. static final class NonfairSync extends Sync {
  22. // ...省略代码
  23. protected int tryAcquireShared(int acquires) {
  24. return nonfairTryAcquireShared(acquires);
  25. }
  26. }
  27. // 非公平锁获取逻辑
  28. final int nonfairTryAcquireShared(int acquires) {
  29. for (;;) {
  30. int available = getState();
  31. int remaining = available - acquires;
  32. if (remaining < 0 ||
  33. compareAndSetState(available, remaining))
  34. return remaining;
  35. }
  36. }

可以看到公平锁和非公平锁的获取代码很像,区别在于是否需要排队等待。

对于tryAcquireShared获取锁的返回值含义,返回负数表示获取失败,零表示当前线程获取成功但后续线程不能再获取,正数表示当前线程获取成功并且后续线程也能够获取。

acquireUninterruptibly

可以被中断的获取阻塞的方法。

  1. public void acquireUninterruptibly() {
  2. sync.acquireShared(1);
  3. }
  4. public final void acquireShared(int arg) {
  5. if (tryAcquireShared(arg) < 0)
  6. doAcquireShared(arg);
  7. }

可以看到其实还是调用了tryAcquireShared尝试获取锁的方法,不做赘述。

tryAcquire

  1. public boolean tryAcquire() {
  2. return sync.nonfairTryAcquireShared(1) >= 0;
  3. }

可以看到直接调用的就是非公平锁的代码,前面的acquire中也有说明具体实现,不做赘述。

release

它的操作很简单,就是调用了AQSreleaseShared方法。

  1. // 释放一个许可
  2. public void release() {
  3. sync.releaseShared(1);
  4. }
  5. // 释放共享锁
  6. public final boolean releaseShared(int arg) {
  7. // 尝试释放锁
  8. if (tryReleaseShared(arg)) {
  9. // 如果成功了则唤醒其他线程
  10. doReleaseShared();
  11. return true;
  12. }
  13. return false;
  14. }
  15. // 尝试释放锁
  16. protected final boolean tryReleaseShared(int releases) {
  17. // 自旋
  18. for (;;) {
  19. // 获取锁状态
  20. int current = getState();
  21. int next = current + releases;
  22. if (next < current) // overflow
  23. throw new Error("Maximum permit count exceeded");
  24. // 以CAS的方式更新锁状态成功返回true,否则继续自旋
  25. if (compareAndSetState(current, next))
  26. return true;
  27. }
  28. }

总结

Semaphore的底层也是AQS,它内部维护了一个计数器,可加可减,acquire()方法是做减法,release()方法是做加法,可基于Semaphore实现限流操作。

SemaphoreAQSUML图如下:

CountDownLatch

CountDownLatch可被称为门阀、计数器或者闭锁,它能够使一个线程在等待另外一些线程完成各自工作之后,再继续执行。它相当于是一个计数器,这个计数器的初始值就是线程的数量,每当一个任务完成后,计数器的值就会减一,当计数器的值为 0 时,表示所有的线程都已经任务了,然后在 CountDownLatch 上等待的线程就可以恢复执行接下来的任务。

CountDownLatch有一个典型的应用场景就是当一个服务启动时,同时会加载很多组件和服务,这时候主线程会等待组件和服务的加载。当所有的组件和服务都加载完毕后,主线程和其他线程在一起完成某个任务。

提供了一个构造方法,必须指定其初始值。还指定了 countDown 方法,这个方法的作用主要用来减小计数器的值,当计数器变为 0 时,在 CountDownLatchawait 的线程就会被唤醒,继续执行其他任务。

  1. // 构造函数
  2. public CountDownLatch(int count) {
  3. if (count < 0) throw new IllegalArgumentException("count < 0");
  4. this.sync = new Sync(count);
  5. }
  6. // countDown方法
  7. // 这个方法与前面Semaphore提供的释放许可的代码是一样的,用的都是AQS中的方法。
  8. public void countDown() {
  9. sync.releaseShared(1);
  10. }
  11. // await方法
  12. // 同样与之前Semaphore获取锁acquire中的方法相同,也是AQS的方法,这个方法将使当前线程在计数器减到0之前一直等待,除非线程被中断。
  13. public void await() throws InterruptedException {
  14. sync.acquireSharedInterruptibly(1);
  15. }

可以看到countDown方法和await方法都是基于的AQS中的方法。

CountDownLatchAQS之间的UML图如下:

CyclicBarrier

CyclicBarrier类可以实现一组线程相互等待,当所有线程都到达某个屏障点后再进行后续的操作。其实说白了,就是等待线程数达到指定的数量后才会执行指定的程序。

CyclicBarrier提供两个构造函数,

CyclicBarrier类的内部有一个计数器,每个线程在到达屏障点的时候都会调用await方法将自己阻塞,此时计数器会减1,当计数器减为0的时候所有因调用await方法而被阻塞的线程将被唤醒。这就是实现一组线程相互等待的原理。

可以先看看CyclicBarrier内的一些成员变量

  1. private static class Generation {
  2. boolean broken = false;
  3. }
  4. // 同步操作锁
  5. private final ReentrantLock lock = new ReentrantLock();
  6. // 线程拦截器
  7. private final Condition trip = lock.newCondition();
  8. // 每次拦截线程的数量
  9. private final int parties;
  10. // 换代前执行任务。在唤醒所有线程之前可以通过指定barrierCommand来执行自己的任务
  11. private final Runnable barrierCommand;
  12. //当前generation
  13. private Generation generation = new Generation();
  14. //计数器
  15. private int count;

可以看到CyclicBarrier内部是通过条件队列trip来对线程进行阻塞的,并且其内部维护了两个int型的变量partiescountparties表示每次拦截的线程数,该值在构造时进行赋值。count是内部计数器,它的初始值和parties相同,以后随着每次await方法的调用而减1,直到减为0就将所有线程唤醒。

CyclicBarrier有两种构造函数

  1. // 可指定需要拦截的线程数
  2. public CyclicBarrier(int parties) {
  3. this(parties, null);
  4. }
  5. // 除了可以指定要拦截的线程数,还可以指定当所有线程都被唤醒后需要执行的任务。
  6. public CyclicBarrier(int parties, Runnable barrierAction) {
  7. if (parties <= 0) throw new IllegalArgumentException();
  8. this.parties = parties;
  9. this.count = parties;
  10. this.barrierCommand = barrierAction;
  11. }

CyclicBarrier类最主要的功能就是使先到达屏障点的线程阻塞并等待后面的线程,其中它提供了两种等待的方法,分别是定时等待和非定时等待。

  1. // 定时等待
  2. public int await(long timeout, TimeUnit unit)
  3. throws InterruptedException,
  4. BrokenBarrierException,
  5. TimeoutException {
  6. return dowait(true, unit.toNanos(timeout));
  7. }
  8. // 非定时等待
  9. public int await() throws InterruptedException, BrokenBarrierException {
  10. try {
  11. return dowait(false, 0L);
  12. } catch (TimeoutException toe) {
  13. throw new Error(toe); // cannot happen
  14. }
  15. }

可以看到无论是定时等待还是非定时等待,它们都调用了dowait方法,只不过是传入的参数不同。

  1. // 核心等待方法
  2. private int dowait(boolean timed, long nanos)
  3. throws InterruptedException, BrokenBarrierException,
  4. TimeoutException {
  5. final ReentrantLock lock = this.lock;
  6. lock.lock();
  7. try {
  8. final Generation g = generation;
  9. if (g.broken)
  10. throw new BrokenBarrierException();
  11. // 检查当前线程是否被中断
  12. if (Thread.interrupted()) {
  13. breakBarrier();
  14. throw new InterruptedException();
  15. }
  16. // 计数器值减1
  17. int index = --count;
  18. // 计数器的值减为0则需唤醒所有线程并转换到下一代
  19. if (index == 0) { // tripped
  20. boolean ranAction = false;
  21. try {
  22. // 先执行指定的任务
  23. final Runnable command = barrierCommand;
  24. if (command != null)
  25. command.run();
  26. ranAction = true;
  27. // 唤醒所有线程并转到下一代
  28. nextGeneration();
  29. return 0;
  30. } finally {
  31. if (!ranAction)
  32. breakBarrier();
  33. }
  34. }
  35. // 如果计数器不为0则自旋
  36. for (;;) {
  37. try {
  38. // 根据传入的参数来决定是定时等待还是非定时等待
  39. if (!timed)
  40. // 非定时任务
  41. trip.await();
  42. else if (nanos > 0L)
  43. // 定时任务
  44. nanos = trip.awaitNanos(nanos);
  45. } catch (InterruptedException ie) {
  46. if (g == generation && ! g.broken) {
  47. // 线程中断 打破栅栏 并唤醒其他线程
  48. // 可以说明在等待过程中有一个线程被中断则全部结束,所有之前被阻塞的线程都会被唤醒。
  49. breakBarrier();
  50. throw ie;
  51. } else {
  52. Thread.currentThread().interrupt();
  53. }
  54. }
  55. if (g.broken)
  56. throw new BrokenBarrierException();
  57. // 是否是正常的换代操作而被唤醒,如果是则返回计数器的值
  58. if (g != generation)
  59. return index;
  60. // 超时报异常
  61. if (timed && nanos <= 0L) {
  62. breakBarrier();
  63. throw new TimeoutException();
  64. }
  65. }
  66. } finally {
  67. lock.unlock();
  68. }
  69. }
  70. //打翻当前栅栏
  71. private void breakBarrier() {
  72. //将当前栅栏状态设置为打翻
  73. generation.broken = true;
  74. //设置计数器的值为需要拦截的线程数
  75. count = parties;
  76. //唤醒所有线程
  77. trip.signalAll();
  78. }

CyclicBarrierAQSUML图。

CyclicBarrier和CountDownLatch

这俩有点像,有很不同。

CountDownLatch主要用来解决一个线程等待多个线程的场景。对于CountDownLatch来说,重点是那一个线程, 是它在等待,而另外那N个线程在把某个事情做完之后可以继续等待,也可以终止。

CyclicBarrier是一组线程之间互相等待,只要有一个线程没有完成,其他线程都要等待。对于CyclicBarrier来说,重点是那一组(N个)线程,他们之间任何一个没有完成,所有的线程都必须等待。

CyclicBarrier的计数器由自己控制,而CountDownLatch的计数器则由使用者来控制,在CyclicBarrier中线程调用await方法不仅会将自己阻塞还会将计数器减1,而在CountDownLatch中线程调用await方法只是将自己阻塞而不会减少计数器的值。

除此之外 CountDownLatch 的计数器是不能循环利用的,也就是说一旦计数器减到 0,再有线程调用 await(),该线程会直接通过。

CyclicBarrier 的计数器是可以循环利用的,而且具备自动重置的功能,一旦计数器减到 0 会自动重置到设置的初始值。除此之外,CyclicBarrier 还可以设置回调函数。

总结

各种同步器总结:

同步器
ReentrantLock 分为公平锁和非公平锁,是可重入独占锁;注意与synchronized关键字的对比;lock加锁unlock解锁,主要是理解内部的实现方式。
Semaphore 允许多个线程同时对共享资源进行操作,并且可以有效的控制并发数。关键方法是acquire获取许可、release释放许可。
CountDownLatch 一个线程在等待另外一些线程完成各自工作之后,再继续往下执行。调用await方法的线程会被挂起,知道count值为0再继续执行;countDown方法就是讲count的值减1。
CyclicBarrier 注意与CountDownLatch的区别,它是一组线程相互等待,当所有线程都到达某个屏障点后再进行后续的操作。调用await方法线程来告诉CyclicBarrier已经到达同步点。

[Java][并发编程]AQS以及其相关同步器的源码解析的更多相关文章

  1. 【Java并发编程】21、线程池ThreadPoolExecutor源码解析

    一.前言 JUC这部分还有线程池这一块没有分析,需要抓紧时间分析,下面开始ThreadPoolExecutor,其是线程池的基础,分析完了这个类会简化之后的分析,线程池可以解决两个不同问题:由于减少了 ...

  2. java并发编程的艺术(三)---lock源码

    本文来源于翁舒航的博客,点击即可跳转原文观看!!!(被转载或者拷贝走的内容可能缺失图片.视频等原文的内容) 若网站将链接屏蔽,可直接拷贝原文链接到地址栏跳转观看,原文链接:https://www.cn ...

  3. 并发编程(六)——AbstractQueuedSynchronizer 之 Condition 源码分析

    我们接着上一篇文章继续,本文讲讲解ReentrantLock 公平锁和非公平锁的区别,深入分析 AbstractQueuedSynchronizer 中的 ConditionObject 公平锁和非公 ...

  4. Java中的容器(集合)之HashMap源码解析

    1.HashMap源码解析(JDK8) 基础原理: 对比上一篇<Java中的容器(集合)之ArrayList源码解析>而言,本篇只解析HashMap常用的核心方法的源码. HashMap是 ...

  5. Java并发编程--AQS

    概述 抽象队列同步器(AbstractQueuedSynchronizer,简称AQS)是用来构建锁或者其他同步组件的基础框架,它使用一个整型的volatile变量(命名为state)来维护同步状态, ...

  6. [Java多线程]-线程池的基本使用和部分源码解析(创建,执行原理)

    前面的文章:多线程爬坑之路-学习多线程需要来了解哪些东西?(concurrent并发包的数据结构和线程池,Locks锁,Atomic原子类) 多线程爬坑之路-Thread和Runable源码解析 多线 ...

  7. react 中间件相关的一些源码解析

    零.随便说说中间件 在react的使用中,我们可以将数据放到redux,甚至将一些数据相关的业务逻辑放到redux,这样可以简化我们组件,也更方便组件抽离.封装.复用,只是redux不能很好的处理异步 ...

  8. Java中的容器(集合)之ArrayList源码解析

    1.ArrayList源码解析 源码解析: 如下源码来自JDK8(如需查看ArrayList扩容源码解析请跳转至<Java中的容器(集合)>第十条):. package java.util ...

  9. 并发编程(四):ThreadLocal从源码分析总结到内存泄漏

    一.目录      1.ThreadLocal是什么?有什么用?      2.ThreadLocal源码简要总结?      3.ThreadLocal为什么会导致内存泄漏? 二.ThreadLoc ...

  10. 并发编程(五)——AbstractQueuedSynchronizer 之 ReentrantLock源码分析

    本文将从 ReentrantLock 的公平锁源码出发,分析下 AbstractQueuedSynchronizer 这个类是怎么工作的,希望能给大家提供一些简单的帮助. AQS 结构 先来看看 AQ ...

随机推荐

  1. linux tcpdump 使用小结(一)

    转载请注明出处: 很多情况在服务联调和定位过程中,需要在服务器上进行抓包进行问题定位.在Linux环境上使用tcpdump命令进行抓包: 1.tcpdump -s 0 -l -i any -w - p ...

  2. chorm如何静音特定网页

    如图所示,右键想要静音的网页标签,选择将这个网页静音即可

  3. 【ARM】为堆和栈保留空的内存块

    此示例演示如何使用分散加载描述为堆栈和堆保留和清空内存块.它还显示链接器生成的相关符号. 在以下示例中,执行区域定义STACK 0x800000 EMPTY -0x10000定义了一个名为STACK ...

  4. SpringBoot实现限流注解

    SpringBoot实现限流注解 在高并发系统中,保护系统的三种方式分别为:缓存,降级和限流. 限流的目的是通过对并发访问请求进行限速或者一个时间窗口内的的请求数量进行限速来保护系统,一旦达到限制速率 ...

  5. 百度网盘(百度云)SVIP超级会员共享账号每日更新(2023.12.10)

    一.百度网盘SVIP超级会员共享账号 可能很多人不懂这个共享账号是什么意思,小编在这里给大家做一下解答. 我们多知道百度网盘很大的用处就是类似U盘,不同的人把文件上传到百度网盘,别人可以直接下载,避免 ...

  6. [转帖]如何不耍流氓的做运维之-SHELL脚本

    https://www.cnblogs.com/luoahong/articles/8504691.html 前言 大家都是文明人,尤其是做运维的,那叫一个斯文啊.怎么能耍流氓呢?赶紧看看,编写SHE ...

  7. [转帖]echo 输出不换行-e \c

    http://www.my889.com/i/1952 在shell中,echo输出会自动换行.有时候在循环中不希望echo输出换行.代码实现如下: 1 echo -e " \c" ...

  8. Redis scan等命令的学习与研究

    Redis scan等命令的学习与研究 摘要 背景跟前几天说的一个问题类似. 为了验证自己的设想, 所以晚上继续写脚本进行了一轮次的验证. 不过上次讨论时,打击好像都没听懂我说的 所以这次准备从基础开 ...

  9. [转帖]Docker相关的概念和原理

    https://www.jianshu.com/p/9737cbe33304 chroot chroot就是可以改变某进程的根目录,使这个程序不能访问目录之外的其他目录.Docker是利用Linux的 ...

  10. [转帖]发布即巅峰!万字长文:Java性能调优六大工具:MAT内存分析工具

    jianshu.com/p/4ed3dd8b7b83 MAT是MemoryAnalyzerTool的简称,它是一款功能强大的Java堆内存分析器,可以用于查找内存泄漏以及查看内存消耗情况.MAT是 基 ...