[Java][并发编程]AQS以及其相关同步器的源码解析
AQS以及其相关同步器的源码解析
概念
AQS
(AbstractQueuedSynchronizer
)抽象的队列同步器。是用来构建锁或者其他同步器组件的重量级基础框架以及整个JUC
体系的基石。通过内置的 FIFO
队列(先入先出队列)来完成资源获取线程的排队工作,将每条要去抢占资源的线程封装成一个 Node
节点来实现锁的分配,并通过一个 volatile
类型的 int
类型变量表示持有锁的状态,使用 CAS
完成对 state
值的修改。
所以我对AQS
简单粗暴的理解:AQS = state + FIFO队列。
其中Node
是CLH
队列中的节点,是AQS
中最基本的数据结构。
Node = waitStatus + Thread
和AQS
有关的类:
ReentrantLock
CountDownLatch
ReentrantReadWriteLock
Semephore
CyclicBarrier
- .......
这些类的底层都有继承了AQS
,所以可以说AQS
是JUC
中最重要的基石。
下面为AQS
相关的UML
图:
ReentrantLock
ReentrantLock
可重入锁,指一个线程可以对一个临界资源重复加锁。为独占锁,不支持共享。
ReentrantLock
提供两个构造方法,有参构造是根据参数创建公平锁或非公平锁,而无参构造方式默认是非公平锁,因为非公平锁的性能高,大部分业务使用的都是非公平锁。
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
所以可以看到,ReentrantLock
有两种加锁方式:公平锁(FairSync
)与非公平锁(NonfairSync
)。
使用方式
public class AQSDemo {
// static ReentrantLock lock = new ReentrantLock();
static ReentrantLock lock = new ReentrantLock(true);
static int num = 10;
public static void main(String[] args) {
new Thread(AQSDemo::test, "线程1").start();
new Thread(AQSDemo::test, "线程2").start();
new Thread(AQSDemo::test, "线程3").start();
}
static void test() {
String name = Thread.currentThread().getName();
while (num > 1) {
lock.lock();
try{
num--;
System.out.printf("%s:%d%n", name, num);
} finally {
lock.unlock();
}
}
}
}
上面这段代码,非公平锁和公平锁的打印结果是不同的。
非公平锁结果:
线程1:9
线程1:8
线程1:7
线程1:6
线程1:5
线程1:4
线程1:3
线程1:2
线程1:1
线程2:0
线程3:-1
公平锁结果:
线程1:9
线程2:8
线程3:7
线程1:6
线程2:5
线程3:4
线程1:3
线程2:2
线程3:1
线程1:0
线程2:-1
源码
ReentrantLock
的内部类Sync
,该类实现了AQS
。又有两个子类实现了Sync
类:FairSync
和 NonfairSync
:
公平锁:讲究的是先来后到,先进先出。如果这个锁的等待队列已经有线程在等待,那么当前线程就会进入等待队列当中排队。
非公平锁:没有排队的概念,谁抢到是谁的。是需要抢占的锁。
公平锁和非公平锁的不同就是,非公平锁首先会调用CAS
将state
从0改为1,成功则表示获取到了锁,使用这个setExclusiveOwnerThread
方法记录下当前占用state
的线程;否则与公平锁一样调用acquire
方法获取锁。acquire
是AQS
中提供的模板方法:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
tryAcquire
尝试着获取 state
,如果成功,跳过后面的步骤;如果失败,则执行 acquireQueued
方法将线程加入 CLH
等待队列中。
tryAcquire
非公平锁和公平锁的实现方式有些不同。
首先看看非公平锁的tryAcquire
:
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
/**
* Performs non-fair tryLock. tryAcquire is implemented in
* subclasses, but both need nonfair try for trylock method.
*/
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 如果 c(锁状态)为0,表示此时的资源是空闲的,则使用CAS获取锁,记录当前占用锁的线程,并返回
// 这里可以看出是非公平锁,因为所有线程都是用CAS获取锁的,不需要排队
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 如果已有线程获得锁,且该线程再次获得了锁,获取资源数 +1
// 这里可以印证 ReentrantLock 是可重入锁
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
// 更新state,这里不需要用CAS更新,此时的锁就是当前线程占有的,其他线程没机会执行。此时更新state是线程安全的
setState(nextc);
return true;
}
return false;
}
然后是公平锁的tryAcquire
。基本上和非公平锁的代码是一样的,区别在于加锁的时候需要判断是否已经有队列存在,没有采取加锁,有则直接返回false
:
/**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 判断是否已经有队列存在,没有才去加锁
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
// 首尾不相等,且head节点线程不是当前线程则表示需要进入队列
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
acquireQueued
如果tryAcquire
尝试加锁失败,则会执行acquireQueued
这个方法,也就是将线程加入到FIFO等待队列。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
可以看到调用了addWaiter
方法,将当前线程的Node
节点入队。从Node.EXCLUSIVE
可以看出,这个节点是独占模式。
addwaiter
代码:
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
// 尾节点不为null表示已经存在队列,直接将当前线程作为尾节点
if (pred != null) {
node.prev = pred;
// 如果尾节点存在,使用CAS将等待线程入队
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 如果节点是空的,则执行 enq 方法
enq(node);
return node;
}
enq
方法代码:
private Node enq(final Node node) {
for (;;) {
Node t = tail;
// 判断尾节点是否为空,如果是空的,说明FIFO队列的head、tail还未构建,需要先构建head节点,然后使用CAS入队
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
// 尾节点不为空,将等待线程入队
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
问:使用
CAS
创建head
节点的时候只是简单调用了new Node()
方法,并不像其他节点那样记录thread
,为什么?答:因为
head
节点只代表当前有线程占用了state
,至于占用state
的是哪个线程,之前有调用了setExclusiveOwnerThread
方法,即已经记录在exclusiveOwnerThread
属性里了。
现在有个问题,就是入队后的线程,要如何处理?是马上阻塞吗?
如果是马上阻塞意味着线程从运行态转为阻塞态,涉及到用户态向内核态的切换,而且唤醒后也要从内核态转为用户态,开销相对比较大。所以AQS
对这种入队的线程采用的方式,是通过让它们自旋来竞争锁,
但是当前锁是独占锁,如果锁一直被被某个线程占有,其他等待队列中的线程一直自旋没太大意义,反而会占用CPU
影响性能。
所以更合适的方式是,它们自旋一两次次数,竞争不到锁后识趣地阻塞以等待前置节点释放锁后再来唤醒它。
另外如果锁在自旋过程中被中断了,或者自旋超时了,应该处于「取消」状态。
所以,在Node
节点中就定义了waitStatus
这个变量,来记录每个Node
可能所处的状态。在前面的概念介绍时,有对各个状态进行了展示。
// 由于超时或中断,节点已被取消
static final int CANCELLED = 1;
// 节点阻塞(park)必须在其前驱结点为 SIGNAL 的状态下才能进行,如果结点为 SIGNAL,则其释放锁或取消后,可以通过 unpark 唤醒下一个节点,
static final int SIGNAL = -1;
// 表示线程在等待条件变量(先获取锁,加入到条件等待队列,然后释放锁,等待条件变量满足条件;只有重新获取锁之后才能返回)
static final int CONDITION = -2;
// 表示后续结点会传播唤醒的操作,共享模式下起作用
static final int PROPAGATE = -3;
//等待状态
volatile int waitStatus;
接下来才真正到了acquireQueued
的代码,加锁的核心逻辑:
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
// 如果前一个节点是 head,且尝试自旋获取锁成功
if (p == head && tryAcquire(arg)) {
// 将 head 结点指向当前节点,原 head 结点出队。这样原 head 不可达,会被GC
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// 如果前一个节点不是 head 或者竞争锁失败,则判断锁是否应该停止自旋进入阻塞状态
// s为true表示线程可以进入阻塞中断,调用parkAndCheckInterrupt让线程阻塞
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
// 如果线程自旋中因为异常等原因获取锁最终失败,则取消加锁节点
cancelAcquire(node);
}
}
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
// 如果前置节点的状态是 SIGNAL,则当前节点可以进入阻塞状态
if (ws == Node.SIGNAL)
return true;
// 如果前置节点为取消状态,则前置节点需要移除
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 将前置节点的 waitStatus 设为 SIGNAL
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
unlock
无论是非公平锁还是公平锁,解锁中也是调用AQS
中的模板方法:
public final boolean release(int arg) {
// 尝试释放锁是否成功
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
// 锁释放成功后,唤醒 head 之后的节点,让它来竞争锁
unparkSuccessor(h);
return true;
}
return false;
}
tryRelease
是定义在ReentrantLock
的内部类Sync
中的:
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
// 只有持有锁的线程才能释放锁,所以如果当前锁不是持有锁的线程,则抛异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
// 说明线程持有的锁全部释放了,需要释放 exclusiveOwnerThread 的持有线程
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
unparkSuccessor
唤醒节点代码:
private void unparkSuccessor(Node node) {
// 获取节点的 waitStatus
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
// 以下逻辑是取队列第一个非取消状态的结点,并将其唤醒
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);
}
公平锁和非公平锁的区别就在于,非公平锁不管是否有线程在排队,先抢锁;而公平锁则会判断是否存在队列,有线程在排队则直接进入队列排队。
另外线程在park被唤醒后非公平锁还会抢锁,公平锁仍然需要排队。所以非公平锁的性能比公平锁高很多,大部分情况下我们使用非公平锁即可。
总结
ReentrantLock
是可重入的互斥锁,虽然具有与synchronized
相同功能,但是会比synchronized
更加灵活,且能提供更多的方法。与synchronized
的异同如下:
ReentrantLock |
synchronized |
|
---|---|---|
锁实现机制 | 依赖AQS |
监视器模式 |
灵活性 | 支持响应中断、超时、尝试获取锁 | 无法设置超时时间,无法提供外部方法 |
释放形式 | 必须显示调用unlock() 释放锁 |
自动释放监视器 |
锁类型 | 公平锁和非公平锁 | 非公平锁 |
条件队列 | 可关联多个条件队列 | 关联一个条件队列 |
可重入性 | 可重入 | 可重入 |
ReentrantLock
底层基于AQS
实现,UML
关系图如下。
Semaphore
使用方式
Semaphore
(信号量)是AQS
共享模式的一个应用,允许多个线程同时对共享资源进行操作,并且可以有效的控制并发数,利用它可以很好的实现流量控制。Semaphore
提供了两个带参构造器,这两个构造器都必须传入一个初始的许可证数量,使用构造器一构造出来的信号量在获取许可证时会采用非公平方式获取,使用构造器二可以通过参数指定获取许可证的方式(公平或者非公平)。
public Semaphore(int permits) {
sync = new NonfairSync(permits);
}
public Semaphore(int permits, boolean fair) {
sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}
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
方法。
源码
获取许可的源码先浅看这四个:
public void acquire() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
public void acquireUninterruptibly() {
sync.acquireShared(1);
}
public boolean tryAcquire() {
return sync.nonfairTryAcquireShared(1) >= 0;
}
public boolean tryAcquire(long timeout, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}
上面的是Semaphore
提供的默认获取许可证操作,每次只获取一个许可证。
acquire
先看看acquire
获取许可里面的acquireSharedInterruptibly
这个方法。这个方法是AQS
里面的方法。
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
// 如果线程中断抛出异常
if (Thread.interrupted())
throw new InterruptedException();
// 尝试获取锁(共享锁)
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
其中tryAcquireShared
也是在AQS
里面是抽象方法,FairSync
和NonfairSync
这两个派生类实现了该方法的逻辑。FairSync
实现的是公平获取的逻辑,而NonfairSync
实现的非公平获取的逻辑。
// 公平锁获取逻辑
static final class FairSync extends Sync {
// ...省略代码
protected int tryAcquireShared(int acquires) {
for (;;) {
// 如果队列中有线程排队则获取锁失败
if (hasQueuedPredecessors())
return -1;
// 获取可用许可
int available = getState();
// 获取剩余可用许可
int remaining = available - acquires;
// 如果剩余许可小于0则直接返回,否则先更新许可状态之后再返回
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
}
// 非公平锁获取逻辑
static final class NonfairSync extends Sync {
// ...省略代码
protected int tryAcquireShared(int acquires) {
return nonfairTryAcquireShared(acquires);
}
}
// 非公平锁获取逻辑
final int nonfairTryAcquireShared(int acquires) {
for (;;) {
int available = getState();
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
可以看到公平锁和非公平锁的获取代码很像,区别在于是否需要排队等待。
对于tryAcquireShared
获取锁的返回值含义,返回负数表示获取失败,零表示当前线程获取成功但后续线程不能再获取,正数表示当前线程获取成功并且后续线程也能够获取。
acquireUninterruptibly
可以被中断的获取阻塞的方法。
public void acquireUninterruptibly() {
sync.acquireShared(1);
}
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
可以看到其实还是调用了tryAcquireShared
尝试获取锁的方法,不做赘述。
tryAcquire
public boolean tryAcquire() {
return sync.nonfairTryAcquireShared(1) >= 0;
}
可以看到直接调用的就是非公平锁的代码,前面的acquire
中也有说明具体实现,不做赘述。
release
它的操作很简单,就是调用了AQS
的releaseShared
方法。
// 释放一个许可
public void release() {
sync.releaseShared(1);
}
// 释放共享锁
public final boolean releaseShared(int arg) {
// 尝试释放锁
if (tryReleaseShared(arg)) {
// 如果成功了则唤醒其他线程
doReleaseShared();
return true;
}
return false;
}
// 尝试释放锁
protected final boolean tryReleaseShared(int releases) {
// 自旋
for (;;) {
// 获取锁状态
int current = getState();
int next = current + releases;
if (next < current) // overflow
throw new Error("Maximum permit count exceeded");
// 以CAS的方式更新锁状态成功返回true,否则继续自旋
if (compareAndSetState(current, next))
return true;
}
}
总结
Semaphore
的底层也是AQS
,它内部维护了一个计数器,可加可减,acquire()
方法是做减法,release()
方法是做加法,可基于Semaphore
实现限流操作。
Semaphore
与AQS
的UML
图如下:
CountDownLatch
CountDownLatch
可被称为门阀、计数器或者闭锁,它能够使一个线程在等待另外一些线程完成各自工作之后,再继续执行。它相当于是一个计数器,这个计数器的初始值就是线程的数量,每当一个任务完成后,计数器的值就会减一,当计数器的值为 0 时,表示所有的线程都已经任务了,然后在 CountDownLatch
上等待的线程就可以恢复执行接下来的任务。
CountDownLatch
有一个典型的应用场景就是当一个服务启动时,同时会加载很多组件和服务,这时候主线程会等待组件和服务的加载。当所有的组件和服务都加载完毕后,主线程和其他线程在一起完成某个任务。
提供了一个构造方法,必须指定其初始值。还指定了 countDown
方法,这个方法的作用主要用来减小计数器的值,当计数器变为 0 时,在 CountDownLatch
上 await
的线程就会被唤醒,继续执行其他任务。
// 构造函数
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
// countDown方法
// 这个方法与前面Semaphore提供的释放许可的代码是一样的,用的都是AQS中的方法。
public void countDown() {
sync.releaseShared(1);
}
// await方法
// 同样与之前Semaphore获取锁acquire中的方法相同,也是AQS的方法,这个方法将使当前线程在计数器减到0之前一直等待,除非线程被中断。
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
可以看到countDown
方法和await
方法都是基于的AQS
中的方法。
CountDownLatch
与AQS
之间的UML
图如下:
CyclicBarrier
CyclicBarrier
类可以实现一组线程相互等待,当所有线程都到达某个屏障点后再进行后续的操作。其实说白了,就是等待线程数达到指定的数量后才会执行指定的程序。
CyclicBarrier
提供两个构造函数,
CyclicBarrier
类的内部有一个计数器,每个线程在到达屏障点的时候都会调用await
方法将自己阻塞,此时计数器会减1,当计数器减为0的时候所有因调用await
方法而被阻塞的线程将被唤醒。这就是实现一组线程相互等待的原理。
可以先看看CyclicBarrier
内的一些成员变量
private static class Generation {
boolean broken = false;
}
// 同步操作锁
private final ReentrantLock lock = new ReentrantLock();
// 线程拦截器
private final Condition trip = lock.newCondition();
// 每次拦截线程的数量
private final int parties;
// 换代前执行任务。在唤醒所有线程之前可以通过指定barrierCommand来执行自己的任务
private final Runnable barrierCommand;
//当前generation
private Generation generation = new Generation();
//计数器
private int count;
可以看到CyclicBarrier
内部是通过条件队列trip
来对线程进行阻塞的,并且其内部维护了两个int
型的变量parties
和count
,parties
表示每次拦截的线程数,该值在构造时进行赋值。count
是内部计数器,它的初始值和parties
相同,以后随着每次await
方法的调用而减1,直到减为0就将所有线程唤醒。
CyclicBarrier
有两种构造函数
// 可指定需要拦截的线程数
public CyclicBarrier(int parties) {
this(parties, null);
}
// 除了可以指定要拦截的线程数,还可以指定当所有线程都被唤醒后需要执行的任务。
public CyclicBarrier(int parties, Runnable barrierAction) {
if (parties <= 0) throw new IllegalArgumentException();
this.parties = parties;
this.count = parties;
this.barrierCommand = barrierAction;
}
CyclicBarrier
类最主要的功能就是使先到达屏障点的线程阻塞并等待后面的线程,其中它提供了两种等待的方法,分别是定时等待和非定时等待。
// 定时等待
public int await(long timeout, TimeUnit unit)
throws InterruptedException,
BrokenBarrierException,
TimeoutException {
return dowait(true, unit.toNanos(timeout));
}
// 非定时等待
public int await() throws InterruptedException, BrokenBarrierException {
try {
return dowait(false, 0L);
} catch (TimeoutException toe) {
throw new Error(toe); // cannot happen
}
}
可以看到无论是定时等待还是非定时等待,它们都调用了dowait
方法,只不过是传入的参数不同。
// 核心等待方法
private int dowait(boolean timed, long nanos)
throws InterruptedException, BrokenBarrierException,
TimeoutException {
final ReentrantLock lock = this.lock;
lock.lock();
try {
final Generation g = generation;
if (g.broken)
throw new BrokenBarrierException();
// 检查当前线程是否被中断
if (Thread.interrupted()) {
breakBarrier();
throw new InterruptedException();
}
// 计数器值减1
int index = --count;
// 计数器的值减为0则需唤醒所有线程并转换到下一代
if (index == 0) { // tripped
boolean ranAction = false;
try {
// 先执行指定的任务
final Runnable command = barrierCommand;
if (command != null)
command.run();
ranAction = true;
// 唤醒所有线程并转到下一代
nextGeneration();
return 0;
} finally {
if (!ranAction)
breakBarrier();
}
}
// 如果计数器不为0则自旋
for (;;) {
try {
// 根据传入的参数来决定是定时等待还是非定时等待
if (!timed)
// 非定时任务
trip.await();
else if (nanos > 0L)
// 定时任务
nanos = trip.awaitNanos(nanos);
} catch (InterruptedException ie) {
if (g == generation && ! g.broken) {
// 线程中断 打破栅栏 并唤醒其他线程
// 可以说明在等待过程中有一个线程被中断则全部结束,所有之前被阻塞的线程都会被唤醒。
breakBarrier();
throw ie;
} else {
Thread.currentThread().interrupt();
}
}
if (g.broken)
throw new BrokenBarrierException();
// 是否是正常的换代操作而被唤醒,如果是则返回计数器的值
if (g != generation)
return index;
// 超时报异常
if (timed && nanos <= 0L) {
breakBarrier();
throw new TimeoutException();
}
}
} finally {
lock.unlock();
}
}
//打翻当前栅栏
private void breakBarrier() {
//将当前栅栏状态设置为打翻
generation.broken = true;
//设置计数器的值为需要拦截的线程数
count = parties;
//唤醒所有线程
trip.signalAll();
}
CyclicBarrier
和AQS
的UML
图。
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以及其相关同步器的源码解析的更多相关文章
- 【Java并发编程】21、线程池ThreadPoolExecutor源码解析
一.前言 JUC这部分还有线程池这一块没有分析,需要抓紧时间分析,下面开始ThreadPoolExecutor,其是线程池的基础,分析完了这个类会简化之后的分析,线程池可以解决两个不同问题:由于减少了 ...
- java并发编程的艺术(三)---lock源码
本文来源于翁舒航的博客,点击即可跳转原文观看!!!(被转载或者拷贝走的内容可能缺失图片.视频等原文的内容) 若网站将链接屏蔽,可直接拷贝原文链接到地址栏跳转观看,原文链接:https://www.cn ...
- 并发编程(六)——AbstractQueuedSynchronizer 之 Condition 源码分析
我们接着上一篇文章继续,本文讲讲解ReentrantLock 公平锁和非公平锁的区别,深入分析 AbstractQueuedSynchronizer 中的 ConditionObject 公平锁和非公 ...
- Java中的容器(集合)之HashMap源码解析
1.HashMap源码解析(JDK8) 基础原理: 对比上一篇<Java中的容器(集合)之ArrayList源码解析>而言,本篇只解析HashMap常用的核心方法的源码. HashMap是 ...
- Java并发编程--AQS
概述 抽象队列同步器(AbstractQueuedSynchronizer,简称AQS)是用来构建锁或者其他同步组件的基础框架,它使用一个整型的volatile变量(命名为state)来维护同步状态, ...
- [Java多线程]-线程池的基本使用和部分源码解析(创建,执行原理)
前面的文章:多线程爬坑之路-学习多线程需要来了解哪些东西?(concurrent并发包的数据结构和线程池,Locks锁,Atomic原子类) 多线程爬坑之路-Thread和Runable源码解析 多线 ...
- react 中间件相关的一些源码解析
零.随便说说中间件 在react的使用中,我们可以将数据放到redux,甚至将一些数据相关的业务逻辑放到redux,这样可以简化我们组件,也更方便组件抽离.封装.复用,只是redux不能很好的处理异步 ...
- Java中的容器(集合)之ArrayList源码解析
1.ArrayList源码解析 源码解析: 如下源码来自JDK8(如需查看ArrayList扩容源码解析请跳转至<Java中的容器(集合)>第十条):. package java.util ...
- 并发编程(四):ThreadLocal从源码分析总结到内存泄漏
一.目录 1.ThreadLocal是什么?有什么用? 2.ThreadLocal源码简要总结? 3.ThreadLocal为什么会导致内存泄漏? 二.ThreadLoc ...
- 并发编程(五)——AbstractQueuedSynchronizer 之 ReentrantLock源码分析
本文将从 ReentrantLock 的公平锁源码出发,分析下 AbstractQueuedSynchronizer 这个类是怎么工作的,希望能给大家提供一些简单的帮助. AQS 结构 先来看看 AQ ...
随机推荐
- 【南大静态代码分析】作业 2:常量传播和 Worklist 求解器
作业 2:常量传播和 Worklist 求解器 题目链接:https://tai-e.pascal-lab.net/pa2.html 评测链接:https://oj.pascal-lab.net/pr ...
- 百度网盘(百度云)SVIP超级会员共享账号每日更新(2023.12.27)
一.百度网盘SVIP超级会员共享账号 可能很多人不懂这个共享账号是什么意思,小编在这里给大家做一下解答. 我们多知道百度网盘很大的用处就是类似U盘,不同的人把文件上传到百度网盘,别人可以直接下载,避免 ...
- TLS 加密套件的学习与了解
TLS 加密套件的学习与了解 加密套件 什么是加密套件? 加密套件是用于在SSL / TLS握手期间协商安全设置的算法的组合. 在ClientHello和ServerHello消息交换之后,客户端发送 ...
- [转帖]linux中top性能分析工具中的TIME+
top命令的TIME/TIME+是指的进程所使用的CPU时间,不是进程启动到现在的时间,因此,如果一个进程使用的cpu很少,那即使这个进程已经存在N长时间,TIME/TIME+也是很小的数值. 此外, ...
- [转帖]Linux字符截取命令-cut
概述 cut是一个选取命令,.一般来说,选取信息通常是针对"行"来进行分析的,并不是整篇信息分析的. 语法 cut [-bn] [file] 1 或 cut [-c] [file] ...
- [转帖]Linux 防火墙开放特定端口 (iptables)
查看状态: iptables -L -n 下面添加对特定端口开放的方法: 使用iptables开放如下端口 /sbin/iptables -I INPUT -p tcp --dport 8000 -j ...
- [转帖]从v8到v9,Arm服务器发展之路
https://zhuanlan.zhihu.com/p/615344155 01 ARM:3A大作 将 CPU 的设计与制造相分离的代工模式,给 AMD 提供了高度的灵活性.第二.三代 EPYC ...
- CentOS7升级Glibc到超过2.17版本无法启动的解决办法
CentOS7升级Glibc到超过2.17版本无法启动的解决办法 背景 今天有同事告知服务器宕机无法启动. 提示信息为: [sda] Assuming drive cache: write throu ...
- Oracle DBCA 静默删除以及建库的脚本
No.1 背景 公司最近有一个测试环境需要重新备份恢复 但是里面有6个数据库实例 400多G的数据文件. 一般情况下 需要drop user xxx cascade ; 然后执行 drop table ...
- 限制input框中字数的输入maxlength
今天产品提出一个需求就是.限制input框中的的值. 当用户超过10个字符时,用户再次输入的时,就不能够输入了. (最后就能够输入10个字符) maxlength=10 <input maxle ...