目录
CyclicBarrier和CountDownLatch的区别
AQS 组件总结
Semaphore
(信号量)-允许多个线程同时访问: synchronized
和 ReentrantLock
都是一次只允许一个线程访问某个资源,Semaphore
(信号量)可以指定多个线程同时访问某个资源。 需要拿到许可才能执行,并可以选择公平和非公平模式。
CountDownLatch
(倒计时器): CountDownLatch
是一个同步工具类,用来协调多个线程之间的同步。这个工具通常用来控制线程等待,它可以让某一个线程等待直到倒计时结束,再开始执行。
CyclicBarrier
(循环栅栏): CyclicBarrier
和 CountDownLatch
非常类似,它也可以实现线程间的技术等待,但是它的功能比 CountDownLatch
更加复杂和强大。主要应用场景和 CountDownLatch
类似。CyclicBarrier
的字面意思是可循环使用(Cyclic
)的屏障(Barrier
)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。CyclicBarrier
默认的构造方法是 CyclicBarrier(int parties)
,其参数表示屏障拦截的线程数量,每个线程调用 await()
方法告诉 CyclicBarrier
我已经到达了屏障,然后当前线程被阻塞。CyclicBarrier 可以在最后一个线程达到屏障之前,选择先执行一个操作。
CountDownLatch 是一个线程等待其他线程, CyclicBarrier 是多个线程互相等待。
CountDownLatch 的计数是减 1 直到 0,CyclicBarrier 是加 1,直到指定值。
CountDownLatch 是一次性的, CyclicBarrier 可以循环利用。
等待多线程完成的CountDownLatch
CountDownLatch允许一个或多个线程等待其他线程完成操作。
CountDownLatch
允许 count
个线程阻塞在一个地方,直至所有线程的任务都执行完毕。
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
它的构造方法,会传入一个 count 值,用于计数。
常用的方法有两个:
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
public void countDown() {
sync.releaseShared(1);
}
/**
* 倒数锁存器的同步控制。使用aqs状态来表示计数。
*/
private static final class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 4982264981922014374L;
Sync(int count) {
setState(count);
}
int getCount() {
return getState();
}
/**
* 发现有点欺骗观众的感觉,因为传入的参数acquires根本没有使用,逻辑只是检查AQS的state是否为0。
* 如果state为0返回大于0的数,如果state不为0返回小于0的数。
* public final void acquireSharedInterruptibly(int arg)
* throws InterruptedException {
* if (Thread.interrupted())
* throw new InterruptedException();
* if (tryAcquireShared(arg) < 0)
* doAcquireSharedInterruptibly(arg);
* }
* 所以,只要tryAcquireShared返回了 > 0的数,acquireSharedInterruptibly就直接返回了不会阻塞了,
* 因为此时state已经为0了,说明已经调用了足够次数的countDown了。
* 如果tryAcquireShared返回了 < 0的数,acquireSharedInterruptibly就需要调用下面的doAcquireSharedInterruptibly,
* 将当前线程包装成node扔到suyc queue上去,走循环 抢锁->阻塞 的流程了。
*
* 线程在tryAcquireShared失败后,一定会阻塞在parkAndCheckInterrupt这里的。
* 所有的调用CountDownLatch.await()阻塞的线程,都是阻塞在这里的。
*
* 而当那个调用countDown从而将count从1变成0的线程执行完countDown后,
* 会唤醒sync queue中的head后继线程,已经说了线程是阻塞在parkAndCheckInterrupt这里,
* 所以head后继线程会从parkAndCheckInterrupt处唤醒,然后继续下一次循环,
* 执行tryAcquireShared会返回>0的数(此时count已经为0了),然后执行setHeadAndPropagate,在里面然后又会执行doReleaseShared。
*
* 现在好了,不仅调用countDown从而将count从1变成0的线程会执行doReleaseShared,
* 调用await()的线程被唤醒后也会执行doReleaseShared,之后被唤醒的线程也会去执行doReleaseShared。
* 这样不断唤醒,很快在sync queue上的所有线程都会被唤醒。之所以说它快,是因为每个尝试获取锁(即tryAcquireShared动作)的线程,
* 在获取锁完毕后并退出await()时就已经唤醒了自己的后继,当然,这本来就是共享锁获取的流程。
*/
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
/**
* tryReleaseShared的返回值很重要,直接影响能否执行doReleaseShared。
* if (tryReleaseShared(arg)) {
* doReleaseShared();
* return true;
* }
* return false;
*
* 获得当前AQS的state
* 如果state是0,那么不能再减了直接返回false,因为state最多能减到0。
* 如果state不是0,需要CAS设置state。
* CAS操作成功了,返回nextc == 0。
* 当然CAS操作可能失败,失败了就需要再次循环执行CAS,因为可能同时有多个线程在同时执行countDown。
* CAS操作成功返回的是nextc == 0,nextc为CAS设置成功后,state的新值。
* 也就是说,只有那个将state从1设置为0的线程,才会返回true,其他调用countDown的线程都会返回false。
*
* 而doReleaseShared的逻辑就是“唤醒所有阻塞在await方法处的线程”。
* 回到releaseShared的逻辑,只有tryReleaseShared(arg)返回了true,doReleaseShared()才会执行,才会去唤醒所有阻塞在await方法处的线程。
* 关于共享锁的流程之前已经讲过,我们只需要知道doReleaseShared()会唤醒sync queue中的head后继,
* 而被唤醒的线程tryAcquireShared成功后在一定条件下也会去调用doReleaseShared()唤醒它的后继,这样可能会有多个线程同时执行doReleaseShared()。
* 重点在于,唤醒线程的速度很快,几乎可以算是同时进行的。
*/
protected boolean tryReleaseShared(int releases) {
// Decrement count; signal when transition to zero
for (;;) {
int c = getState();
if (c == 0)
return false;
int nextc = c-1;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
}
CountDownLatch