一、CountDownLatch
CountDownLatch 是 AQS 共享模式使用的锁,共享的概念就是 N 条线程需要同一把锁,当 N 条线程全部执行完成后,才会继续向下执行。
这是一个简单应用:
public static void main(String[] args) {
CountDownLatch doneSignal = new CountDownLatch(2);
new Thread(() -> {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("加入线程:" + Thread.currentThread().getName());
doneSignal.countDown();
}).start();
new Thread(() -> {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("加入线程:" + Thread.currentThread().getName());
doneSignal.countDown();
}).start();
new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + "加入等待队列,准备执行");
doneSignal.await();
System.out.println(Thread.currentThread().getName() + "开始执行");
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + "加入等待队列,准备执行");
doneSignal.await();
System.out.println(Thread.currentThread().getName() + "开始执行");
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
实现原理:
1、构造函数:
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
private static final class Sync extends AbstractQueuedSynchronizer {
Sync(int count) {
setState(count);
}
}
这个 Sync 是 CountDownLatch 自己的静态内部类,与 ReentrantLock 的 Sync 是不一样的。Sync 构造函数中的 count,代表这个共享模式下线程的条数,每次添加一条线程 count 都会减 1,当 count 达到 0 时,才会放过所有线程一起执行。CountDownLatch 使用起来其实只有两个方法:countDown 和 await,下面分别看:
2、countDown
public void countDown() {
sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
protected boolean tryReleaseShared(int releases) {
for (;;) {
int c = getState();
if (c == 0)
return false;
int nextc = c-1;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
每次都把 state 减 1,直到 state 为 0 时,才会调用 doReleaseShared 方法,这个方法我们和后面的一起分析。
3、await:
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
//同样需要判断一下线程的中断状态
if (Thread.interrupted())
throw new InterruptedException();
//只会返回1或者-1,1代表可以执行了,-1还没达到容量
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
//这个方法其实不需要传参。。。
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
tryAcquireShared 这个方法被调用的很多,用来检查 state 是否达到 0。而只有线程调用 countDown 这个方法时,state 才会进行相应的操作。所以只要线程数还没达到容量的要求都会进入 doAcquireSharedInterruptibly,这个方法才有将线程挂起的操作;否则,直接向下执行代码。
注意 await 方法中,没有涉及到减少 state 值的操作,state 值的设置都在 countDown 方法中。
4、doAcquireSharedInterruptibly
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
//addWaiter还是将当前线程包装为Node对象,但是标记了共享模式
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
//与之前的方法很相似
for (;;) {
final Node p = node.predecessor();
//检查前驱节点是不是头部,是的话检查一下state达到0了吗
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
//线程挂起
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
这个方法与之前的 acquireQueued 加入等待队列的方法相似,都是判断一下,当前线程对象的前驱节点是否是头部节点,不是的话,直接挂起就可以了。如果是头部节点,并且 state 达到 0 了,调用 setHeadAndPropagate 方法。
private void setHeadAndPropagate(Node node, int propagate) {
//直接将head设置为当前线程
Node h = head; // Record old head for check below
setHead(node);
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
//所有线程都已经在执行了,或者这是共享模式
if (s == null || s.isShared())
doReleaseShared();
}
}
//分析是否是共享模式,这是抽取出来的,便于理解
Node node = new Node(Thread.currentThread(), mode);
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
final boolean isShared() {
return nextWaiter == SHARED;
}
如果当前线程的前驱节点就是头部线程的话,将头部节点设置为当前线程,然后开始释放锁。后面的代码是表达 addWaiter 时,标记了这个节点是共享模式的。
5、doReleaseShared
private void doReleaseShared() {
for (;;) {
Node h = head;
//h == null || h == tail的情况:头部节点为空,头部节点刚刚初始化,头部节点就是等待队列的尾节点,就是所有线程都已经开始执行
if (h != null && h != tail) {
int ws = h.waitStatus;
//头部节点还没开始执行,如果这时已经有节点入队,那么头部节点状态会被设置为-1
if (ws == Node.SIGNAL) {
//将头部节点状态置为0,失败的话继续循环
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
//成功的话解除头部节点的后续节点的挂起状态,也就是唤醒等待队列的第一个节点
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
//如果唤醒后,头部线程还是之前的头部,那么退出循环
if (h == head) // loop if head changed
break;
}
}
唤醒就比较简单了,唤醒的是头部节点的后续节点,后续节点还停留在 parkAndCheckInterrupt 中,把它唤醒,再由这条被唤醒去唤醒其他线程。
总结:
共享锁的使用方法就是:
- 这里有多条任务线程,他们都调用 countDown 方法,这个方法只是用来记录线程数量是否达到 CountDownLatch 初始化给定的值。多条任务线程并不会要求它们一起执行,可以有先后,可以有时间长短不同,但是它们执行完都会回馈给 CountDownLatch。
- 这里还有多条主线程,它们执行前都调用了 await 方法,await 方法会把所有调用者都阻塞起来,放入到等待队列中,等待所有任务线程执行完成后,再依次放开主线程去执行。
- 当最后一条任务线程进来时,也就是使 state 到达 0 的线程,会由它来开始唤醒主线程。头节点的线程唤醒后,开始调用 setHeadAndPropagate,唤醒后续节点的线程。每条等待队列中的线程都负责将后续线程唤醒,然后退出循环,开始执行各自主线程内的方法。
二、CyclicBarrier
CyclicBarrier 和 CountDownLatch 很类似,但是 CountDownLatch 只能使用一次,大家可以测试一下,使用 CountDownLatch 时,当 state 已经达到 0,再调用 countDown 会发现,不管再添加多少条任务线,await 方法都会在 state 达到 0 时开始执行。
而 CyclicBarrier 字面意思,可重复使用,可以看下面一个例子:
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(2);
new Thread(new myRunnable(cyclicBarrier)).start();
new Thread(new myRunnable(cyclicBarrier)).start();
new Thread(new myRunnable(cyclicBarrier)).start();
new Thread(new myRunnable(cyclicBarrier)).start();
}
static class myRunnable implements Runnable{
CyclicBarrier cyclicBarrier;
myRunnable(CyclicBarrier cyclicBarrier){
this.cyclicBarrier = cyclicBarrier;
}
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName() + "准备执行");
cyclicBarrier.await();
System.out.println(Thread.currentThread().getName() + "开始执行");
} catch (Exception e) {
e.printStackTrace();
}
}
}
可以看出,每有 2 条线程进入 CyclicBarrier,await 就会开始执行;每次线程数量达到初始化设定的值后,await 开始执行,并会重置这个值,重新开始计算线程数量。
CyclicBarrier 的源码实现和 CountDownLatch 大相径庭,CountDownLatch 基于 AQS 的共享模式的使用,而 CyclicBarrier 基于 Condition 来实现。
三、Semaphore
Semaphore 可以比作一个停车场,有出有进,当停满的时候,只有出去一个才可以放进来一个。Semaphore 就是这种实现思路,也是 AQS 共享锁的体现,每条线程共享一个池子。可以看下面一个例子:
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(2);
for (int i = 1; i < 5; i ++) {
new Thread(new myRunnable(semaphore, i)).start();
}
}
static class myRunnable implements Runnable{
Semaphore semaphore;
int i;
myRunnable(Semaphore semaphore, int i){
this.semaphore = semaphore;
this.i = i;
}
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName() + "准备执行");
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + "开始执行");
Thread.sleep(i * 1000);
semaphore.release();
System.out.println(Thread.currentThread().getName() + "执行完成");
} catch (Exception e) {
e.printStackTrace();
}
}
}
当新线程进来时,调用 acquire 方法(开到了停车场),这时不一定会有获取到锁(停车位),需要看 semaphore 池子中已经有多少条线程。如果没有达到设定的值,可以获得锁,并开始执行(停到车位中),执行完成后需要 release 释放锁,并告诉 semaphore 池子为止减少 1,可以唤醒后面等待的线程来拿锁(开走,后面的车可以继续进来停);如果已经达到设定的值(停车位满了),就需要等待其他线程释放锁,并且释放池子中的资源。