CountDownLatch
CountDownLatch是一个同步工具类,它允许一个或者多个线程一直等待,知道其他线程的操作执行完毕再执行。
CountDownLatch提供了两个方法,一个是countDown,一个是await,countDownLatch初始化的时候需要传入一个整数,在这个整数倒数到0之前,调用了await方法的程序都必须要等待,然后通过countDown来倒数。
public static void main(String[] args) throws InterruptedException {
final CountDownLatch countDownLatch = new CountDownLatch(4);
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("" + Thread.currentThread().getName() + "-执行中");
countDownLatch.countDown();
System.out.println("" + Thread.currentThread().getName() + "-执行完毕");
}
}, "t1").start();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("" + Thread.currentThread().getName() + "-执行中");
countDownLatch.countDown();
System.out.println("" + Thread.currentThread().getName() + "-执行完毕");
}
}, "t2").start();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("" + Thread.currentThread().getName() + "-执行中");
countDownLatch.countDown();
System.out.println("" + Thread.currentThread().getName() + "-执行完毕");
}
}, "t3").start();
countDownLatch.await();
System.out.println("所有线程已经执行完毕");
}
从代码实现看,类似join的功能,但是比join更加灵活。CountDownLatch构造函数会接受一个int类型的参数作为计数器的初始值,当调用CountDownLatch的countDown方法时,这和计数器就会减一。
模拟高并发场景
static CountDownLatch countDownLatch = new CountDownLatch(1);
public static void main(String[] args) {
for (int i = 0; i < 1000; i++) {
new Thread(new CountDownLatchDemo()).start();
}
countDownLatch.countDown();
}
@Override
public void run() {
try {
countDownLatch.await();
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println("ThreadName:"+Thread.currentThread().getName());
}
总的来说,凡是涉及到需要指定某个任务再执行之前,要等到前置任务执行完毕之后才能执行的场景,都可以使用CountDownLatch。
CountDownLatch源码解析
对于countDownLatch,只要有await()方法和countDown()方法。
countDown()方法每次调用都会将state减1,直到state的值为0;而await是一个阻塞方法,当state减为0的时候,await方法才会返回。await可以被多个线程调用。所有调用了await方法的线程阻塞在AQS的紫色队列来,条件满足(state==0),将线程从队列中一个个唤醒过来。
acquireSharedInterruptibly
countDownLatch也使用到AQS,在CountDownLatch内部写了一个Sync并且继承了AQS这个抽象类重写了AQS中的共享锁方法。如下代码,这块代码只要是判断当前线程是否获取到了共享锁;(在CountDownLatch中,使用的是共享锁机制,因为CountDownLatch并不需要实现互斥的特性)
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
//state如果不等于0,说明当前线程需要加入带共享锁队列
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
doAcquireSharedInterruptibly
1.addWaiter设置为shared模式
2.tryAcquire和tryAcquireShared的返回值不同,因此会多出一个判断过程
3.在判断前驱结点是头结点后,调用了setHeadAndPropagate方法,而不是简单地更新了一下头结点
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedE
//创建一个共享模式的节点添加到队列中
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head) {
//判断尝试获得锁
int r = tryAcquireShared(arg);
//r>=0表示获取到了执行权限,这个时候state!=0,所以不会执行这段代码
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);
}
}
图解分析
加入这个时候有3个线程调用了await方法,由于这个时候state的值还不为0,所以这三个线程都会加入到AQS队列中。并且三个线程都处于阻塞状态。
CountDownLatch.countDown
由于线程被await方法阻塞了,所以只有等到countDown方法使得state=0的时候才会被唤醒。
1.只有当state减为0的时候,tryReleaseShared才会返回true,否则只是简单的state=state-1
2.如果state=0,则调用doReleaseShared唤醒处于await状态下的线程
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
//用自旋的方式实现state减1
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;
}
}
AQS.doReleaseShared
共享锁的释放和独占锁的释放有一定的差别,前面唤醒锁的逻辑和独占锁是一样的,先判断头结点是不是SIGNAL状态,如果是,则修改为0,并且唤醒头结点的而下一个节点
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);
}
//这个CAS失败的场景是:执行到这里的时候,刚好有一个节点入队,入队会将这个ws设置为-1
else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
//如果当到这里的时候,前面唤醒的线程已经占了了head,那么再循环
//通过检查头节点是否改变了,如果改变了就继续循环
if (h == head) // loop if head changed
break;
}
}
PROPAGATE:标识为PROPAGATE状态的节点,是共享模式下的节点状态,处于这个状态下的节点,会对县城内的唤醒进行传播
h==head:说明头节点还没有被刚刚用unparkSuccessor唤醒的线程(这里可以理解为ThreadB)占有,是break退出循环。
h!=head:头节点被刚刚唤醒的线程(这里可以理解为ThreadB)占有,那么这里重新进入下一轮玄幻,唤醒下一个节点(这里是ThreadB)。然后后面唤醒传递。。
一旦ThreadA被唤醒,代码又回到了doAcquireSharedInterruptibly中来执行。如果当前state满足等于0的条件,则会执行setHeadAndPropagate方法
if (p == head) {
//判断尝试获得锁
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
setHeadAndPropagate
这个方法主要作用是把被唤醒的节点,设置成head节点。然后继续唤醒队列中的其他线程。
由于现在队列有3个线程处于阻塞状态,一旦ThreadA被唤醒,并且设置为head之后,会继续唤醒后续的ThreadB。
private void setHeadAndPropagate(Node node, int propagate) {
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();
}
}
Semaphore
semaphore 也就是我们常说的信号灯,semaphore 可以控 制同时访问的线程个数,通过 acquire 获取一个许可,如 果没有就等待,通过 release 释放一个许可。有点类似限流 的作用。叫信号灯的原因也和他的用处有关,比如某商场 就 5 个停车位,每个停车位只能停一辆车,如果这个时候 来了 10 辆车,必须要等前面有空的车位才能进入。
public class SemaphoreDemo {
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(5);
for (int i = 0; i < 10; i++) {
new Car(i, semaphore).start();
}
}
static class Car extends Thread {
private int num;
private Semaphore semaphore;
public Car(int num, Semaphore semaphore) {
this.num = num;
this.semaphore = semaphore;
}
public void run() {
try {
semaphore.acquire();
System.out.println("第" + num + "占用一个停车位");
TimeUnit.SECONDS.sleep(2);
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
使用场景
Semaphore比较常见的就是用来作限流操作。
Semaphore源码分析
从Semaphore的功能来看,我们可以猜测它的底层原理一定是基于AQS的共享锁。
创建Semaphore实例的时候,需要一个参数permits,这个基本上可以确定是设置给AQS的state的,然后每个线程调用acquire的时候,执行state=state-1,release的时候执行state=state+1,当然,acquire的时候,如果state=0,说明没有资源了,需要等待其他的线程release。
Semaphore分公平策略和非公平策略
FairSync
static final class FairSync extends Sync {
private static final long serialVersionUID = 2014338818796000944L;
FairSync(int permits) {
super(permits);
}
protected int tryAcquireShared(int acquires) {
for (;;) {
//区别就在于是不是会先判断是否有线程在排队,然后才进行CAS键操作
if (hasQueuedPredecessors())
return -1;
int available = getState();
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
}
NoFairSync
通过对别发现公平锁和非公平锁的区别就是在于是否多了一个hasQueuedPredecessors的判断
static final class NonfairSync extends Sync {
private static final long serialVersionUID = -2694183684443567898L;
NonfairSync(int permits) {
super(permits);
}
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;
}
}
//都是基于共享锁来实现的
CyclicBarrier
CyclicBarrier的字面意思是可循环使用(Cyclic)的屏障(Barrier)。他要做的事情是,让一组线程到达一个屏障(也可以佳作同步点)时被阻塞,知道最后一个线程到达平衡住那个是,屏障才会开门,所有被屏障拦截的线程才会继续工作。CyclicBarrier默认的构造方法是CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await方法告诉CyclicBarrier当前线程已经到达了屏障,然后当前线程被阻塞。
使用场景
当存在需要所有的子任务都完成时,才会执行主任务,这个时候就可以选择使用CyclicBarrier。
案例
DataImportThread
public class DataImportThread extends Thread{
private CyclicBarrier cyclicBarrier;
private String path;
public DataImportThread(CyclicBarrier cyclicBarrier,String path){
this.cyclicBarrier = cyclicBarrier;
this.path = path;
}
@Override
public void run() {
System.out.println("开始导入:"+path+"位置的数据");
try {
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
}
CyclicBarrierDemo
public class CyclicBarrierDemo extends Thread{
@Override
public void run() {
System.out.println("开始进行数据分析");
}
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(3,new CyclicBarrierDemo());
new Thread(new DataImportThread(cyclicBarrier,"file1")).start();
new Thread(new DataImportThread(cyclicBarrier,"file2")).start();
new Thread(new DataImportThread(cyclicBarrier,"file3")).start();
}
}
注意点:
1)对于制定计数值parties。若由于某种原因,没有足够的线程调用CyclicBarrier的await,则所有调用await的线程都会被阻塞。
2)同样的CyclicBarrier也可以调用await(timeout, unit),设置超时时间,在设定时间内,如果没有足够线程到达,则解除阻塞状态,继续工作。
3)通过reset重置计数,会使得进入await的线程出现BrokenBarrierExecption;
4)如果采用是CyclicBarrier(int parteis, Runnable barrierAction)构造方法,执行barrierAction操作的是最后一个到达的线程。
实现原理
CyclicBarrier相比CountDownLatch来说,要简单很多,源码实现是基于ReentrantLock和Condition的组合使用。如下图,CyclicBarrier和CountDownLatch是不是很像,只是CyclicBarrier可以不止一个栅栏,因为他的栅栏(Barrier)可以重复使用。