一、等待多线程完成的CountDownLatch
1、案例介绍
public class CountDownLatchDemo {
private static CountDownLatch countDownLatch = new CountDownLatch(2);
public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(2);
executorService.execute(() -> {
try {
TimeUnit.SECONDS.sleep(1);
System.out.println("child threadOne over");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
countDownLatch.countDown();
}
});
executorService.execute(() -> {
try {
TimeUnit.SECONDS.sleep(1);
System.out.println("child threadTwo over");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
countDownLatch.countDown();
}
});
System.out.println("wait all child thread over");
countDownLatch.await();
System.out.println("all child thread over");
executorService.shutdown();
}
}
创建了一个CountDownLatch实例,因为有两个子线程所以构造函数的传参为2。主线程调用countDownLatch.await()方法后会被阻塞。子线程执行完毕后调用countDownLatch.countDown()方法让countDownLatch内部的计数器减1,所有子线程执行完毕并调用countDown()方法后计数器会变为0,这时候主线程的await()方法才会返回
2、CountDownLatch源码分析
CountDownLatch是使用AQS实现的,通过下面的构造函数把计数器的值赋给了AQS的状态变量state,也就是使用AQS的状态值来表示计数器值
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
Sync(int count) {
setState(count);
}
1)、await()方法
当线程调用CountDownLatch对象的await()方法后,当前线程会被阻塞,直到下面的情况之一发生才会返回:当所有线程都调用了CountDownLatch对象的countDown()方法后,也就是计数器的值为0时;其他线程调用了当前线程的interrupt()方法中断了当前线程,当前线程就会抛出InterruptedException异常,然后返回
public void await() throws InterruptedException {
//调用AQS中的模板方法acquireSharedInterruptibly
sync.acquireSharedInterruptibly(1);
}
AQS中的acquireSharedInterruptibly方法:
//AQS获取共享资源时可被中断的方法
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
//如果线程被中断则抛出异常
if (Thread.interrupted())
throw new InterruptedException();
//调用CountDownLatch中sync重写的tryAcquireShared方法,查看当前计数器值是否为0,为0则直接返回,否则进入AQS的队列等待
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
sync类实现的AQS的接口:
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
2)、countDown()方法
public void countDown() {
//调用AQS中的模板方法releaseShared
sync.releaseShared(1);
}
AQS中的releaseShared方法:
public final boolean releaseShared(int arg) {
//调用CountDownLatch中sync重写的tryReleaseShared方法
if (tryReleaseShared(arg)) {
//AQS的释放资源方法
doReleaseShared();
return true;
}
return false;
}
sync类实现的AQS的接口:
protected boolean tryReleaseShared(int releases) {
//循环进行CAS,直到当前线程成功完成CAS使计数器值(状态值state)减1并更新到state
for (;;) {
int c = getState();
//如果当前状态值为0则直接返回(为了防止当计数器值为0后,其他线程又调用了countDown方法,防止状态值变为负数)
if (c == 0)
return false;
//使用CAS让计数器值减1
int nextc = c-1;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
}
二、同步屏障CyclicBarrier
1、案例介绍
public class CyclicBarrierDemo {
private static CyclicBarrier cyclicBarrier = new CyclicBarrier(2, new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "执行回调方法");
}
});
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(2);
executorService.execute(() -> {
try {
System.out.println(Thread.currentThread().getName() + " step1");
cyclicBarrier.await();
System.out.println(Thread.currentThread().getName() + " step2");
cyclicBarrier.await();
System.out.println(Thread.currentThread().getName() + " step3");
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
});
executorService.execute(() -> {
try {
System.out.println(Thread.currentThread().getName() + " step1");
cyclicBarrier.await();
System.out.println(Thread.currentThread().getName() + " step2");
cyclicBarrier.await();
System.out.println(Thread.currentThread().getName() + " step3");
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
});
executorService.shutdown();
}
}
运行结果:
pool-1-thread-1 step1
pool-1-thread-2 step1
pool-1-thread-2执行回调方法
pool-1-thread-1 step2
pool-1-thread-2 step2
pool-1-thread-2执行回调方法
pool-1-thread-1 step3
pool-1-thread-2 step3
多个线程之间是相互等待的,假如计数器值为N,那么随后调用await()方法的N-1个线程都会因为到达屏障点而被阻塞,当第N个线程调用await()后,计数器值为0了,这时候第N个线程才会发出通知唤醒前面的N-1个线程。也就是当全部线程都到达屏障点时才能一块继续向下执行
此外从上面的案例中还可以得知,CyclicBarrier的计数器具备自动重置的功能,可以循环利用,回调任务是由最后一个到达屏障的线程执行的
2、CyclicBarrier源码分析
CyclicBarrier基于独占锁实现,本质底层还是基于AQS的。parties用来记录线程个数,这里表示多少线程调用await()后,所有线程才会冲破屏障继续往下运行。而count—开始等于parties,每当有线程调用await()方法就递减1,当count为0时就表示所有线程都到了屏障点。而parties始终用来记录总的线程个数,当count计数器值变为0后,会将parties的值赋给count,从而进行复用
public CyclicBarrier(int parties, Runnable barrierAction) {
if (parties <= 0) throw new IllegalArgumentException();
this.parties = parties;
this.count = parties;
this.barrierCommand = barrierAction;
}
还有一个变量barrierCommand也通过构造函数传递,这是一个任务,这个任务的执行时机是当所有线程都到达屏障点后。使用lock首先保证了更新计数器count的原子性,另外使用lock的条件变量trip支持线程间使用await()和signal()操作进行同步
在变量generation内部有一个变量broken,其用来记录当前屏障是否被打破。这里的broken并没有被声明为volatile的,因为是在锁内使用变量,所以不需要声明
private static class Generation {
boolean broken = false;
}
await()方法:
当前线程调用CyclicBarrier的该方法时会被阻塞,直到满足下面条件之一才会返回:parties个线程都调用了await()方法,也就是线程都到了屏障点;其他线程调用了当前线程的interrupt()方法中断了当前线程,则当前线程会抛出InterruptedException异常而返回;与当前屏障点关联的Generation对象的broken标志被设置为true时,会抛出BrokenBarrierException异常,然后返回
public int await() throws InterruptedException, BrokenBarrierException {
try {
//调用内部的dowait()方法,第一个参数为flase表示不设置超时时间
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();
}
//如果index==0则说明所有线程都到了屏障点,此时执行初始化时传递的任务
int index = --count;
if (index == 0) { // tripped
boolean ranAction = false;
try {
final Runnable command = barrierCommand;
//(1)执行任务
if (command != null)
command.run();
ranAction = true;
//(2)激活其他因调用await方法而被阻塞的线程,并重置CyclicBarrier
nextGeneration();
//返回
return 0;
} finally {
if (!ranAction)
breakBarrier();
}
}
//(3)index!=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();
}
}
nextGeneration()方法:
private void nextGeneration() {
//唤醒条件队列里面阻塞的线程
trip.signalAll();
//重置CyclicBarrier
count = parties;
generation = new Generation();
}
当一个线程调用了dowait方法后,首先会获取独占锁lock,如果创建CycleBarrier时传递的参数为10,那么后面9个调用线程会被阻塞。然后当前获取到锁的线程会对计数器count进行递减操作,递减后count=index=9,因为index!=0所以当前线程会执行代码(3)。如果当前线程调用的是无参数的await()方法,则这里timed=false,所以当前线程会被放入条件变量trip的条件阻塞队列,当前线程会被挂起并释放获取的lock锁。如果调用的是有参数的await方法则timed=true,然后当前线程也会被放入条件变量的条件队列并释放锁资源,不同的是当前线程会在指定时间超时后自动被激活
当第一个获取锁的线程由于被阻塞释放锁后,被阻塞的9个线程中有一个会竞争到lock锁,然后执行与第一个线程同样的操作,直到最后一个线程获取到lock锁,此时已经有9个线程被放入了条件变量trip的条件队列里面。最后count=index等于0,所以执行代码(1),如果创建CyclicBarrier时传递了任务,则在其他线程被唤醒前先执行任务,任务执行完毕后再执行代码(2),唤醒其他9个线程,并重置CyclicBarrier,然后这10个线程就可以继续向下运行了
三、使用CountDownLatchDown和CyclicBarrier优化对账系统
在学习极客时间的《Java并发编程实战》这门课时,关于CountDownLatchDown和CyclicBarrier的文章《CountDownLatch和CyclicBarrier:如何让多线程步调一致?》中,王宝令老师通过一个优化对账系统的案例更好的诠释了CountDownLatchDown和CyclicBarrier的使用,同时作者解决问题的思路也值得我们借鉴,下面是我对文章进行的总结和梳理,同时也强烈推荐学习一下这门课程,对并发编程的讲解还是很不错的
对账系统业务:用户通过在线商城下单,会生成电子订单,保存在订单库;之后物流会生成派送单给用户发货,派送单保存在派送单库。为了防止漏派送或者重复派送,对账系统每天还会校验是否存在异常订单
对账系统的核心代码如下,就是在一个单线程里面循环查询订单、派送单,然后执行对账,最后将写入差异库
while (存在未对账订单) {
//查询未对账订单
pos = getPOrders();
//查询派送单
dos = getDOrders();
//执行对账操作
diff = check(pos, dos);
//差异写入差异库
save(diff);
}
首先要优化性能,就要找到这个对账系统的瓶颈所在:目前的对账系统,由于订单量和派送单量巨大,所以查询未对账订单getPOrders()和查询派送单getDOrders()相对较慢,目前对账系统是单线程执行的,图形化后是下图这个样子
查询未对账订单getPOrders()和查询派送单getDOrders()这两个操作并没有先后顺序的依赖可以并行处理,执行过程如下图所示。对比一下单线程的执行示意图,在同等时间内,并行执行的吞吐量近乎单线程的2倍
1、用CountDownLatch实现线程等待
//创建2个线程的线程池
Executor executor = Executors.newFixedThreadPool(2);
while (存在未对账订单) {
//计数器初始化为2
CountDownLatch latch = new CountDownLatch(2);
//查询未对账订单
executor.execute(() -> {
pos = getPOrders();
latch.countDown();
});
//查询派送单
executor.execute(() -> {
dos = getDOrders();
latch.countDown();
});
//等待两个查询操作结束
latch.await();
//执行对账操作
diff = check(pos, dos);
//差异写入差异库
save(diff);
}
在while循环里面,创建了一个CountDownLatch,计数器的初始值等于2,之后在pos = getPOrders();和dos = getDOrders();两条语句的后面对计数器执行减1操作,这个对计数器减1的操作是通过调用latch.countDown();来实现的。在主线程中,通过调用latch.await()来实现对计数器等于0的等待
2、进一步优化性能
getPOrders()和getDOrders()这两个查询操作和对账操作check()、save()之间也是可以并行的,也就是说,在执行对账操作的时候,可以同时去执行下一轮的查询操作,如下图所示
针对对账这个项目,设计了两个队列,并且两个队列的元素之间还有对应关系。具体如下图所示,订单查询操作将订单查询结果插入订单队列,派送单查询操作将派送单插入派送单队列,这两个队列的元素之间是有一一对应的关系的。两个队列的好处是,对账操作可以每次从订单队列出一个元素,从派送单队列出一个元素,然后对这两个元素执行对账操作,这样数据一定不会乱掉
线程T1和线程T2只有都生产完1条数据的时候,才能一起向下执行,也就是说,线程T1和线程T2要互相等待,步调要一致;同时当线程T1和T2都生产完一条数据的时候,还要能够通知线程T3执行对账操作
3、用CyclicBarrier实现线程同步
//订单队列
Vector<P> pos;
//派送单队列
Vector<D> dos;
//执行回调的线程池
Executor executor = Executors.newFixedThreadPool(1);
final CyclicBarrier barrier = new CyclicBarrier(2, () -> {
executor.execute(() -> check());
});
void check() {
P p = pos.remove(0);
D d = dos.remove(0);
//执行对账操作
diff = check(p, d);
//差异写入差异库
save(diff);
}
void checkAll() {
//循环查询订单库
Thread T1 = new Thread(() -> {
while (存在未对账订单) {
//查询订单库
pos.add(getPOrders());
//等待
barrier.await();
}
});
T1.start();
//循环查询运单库
Thread T2 = new Thread(() -> {
while (存在未对账订单) {
//查询运单库
dos.add(getDOrders());
//等待
barrier.await();
}
});
T2.start();
}
首先创建了一个计数器初始值为2的CyclicBarrier,还传入了一个回调函数,当计数器减到0的时候,会调用这个回调函数
线程T1负责查询订单,当查出一条时,调用barrier.await()来将计数器减1,同时等待计数器变为0;线程T2负责查询派送单,当查出一条时,也调用barrier.await()来将计数器减1,同时等待计数器变为0;当T1和T2都调用barrier.await()的时候,计数器会减到0,此时T1和T2就可以执行下一条语句了,同时会调用barrier的回调函数来执行对账操作
CyclicBarrier的计数器有自动重置的功能,当减到0的时候,会自动重置设置的初始值
回调函数中使用了一个固定大小为1的线程池。首先使用线程池是为了异步操作,否则回调函数是同步调用的,也就是本次对账操作执行完才能进行下一轮的检查;其次线程数量固定为1,防止了多线程并发导致的数据不一致,因为订单和派送单是两个队列,只有单线程去两个队列中取消息才不会出现消息不匹配的问题
4、总结
CountDownLatch主要用来解决一个线程等待多个线程的场景,而CyclicBarrier是一组线程之间互相等待,而且CyclicBarrier的计数器具备自动重置的功能,可以循环利用,CyclicBarrier还可以设置回调函数
四、信号量Semaphore
1、信号量模型
信号量模型包括一个计数器,一个等待队列,三个方法。在信号量模型里,计数器和等待队列对外是透明的,所以只能通过信号量模型提供的三个方法来访问它们,这三个方法分别是:init()、down()和up()
- init():设置计数器的初始值
- down():计数器的值减1;如果此时计数器的值小于0,则当前线程将被阻塞,否则当前线程可以继续执行
- up():计数器的值加1;如果此时计数器的值小于或者等于0,则唤醒等待队列中的一个线程,并将其从等待队列中移除
init()、down()和up()三个方法都是原子性的。在Java SDK里面,信号量模型是由java.util.concurrent.Semaphore
实现的,Semaphore这个类能够保证这三个方法都是原子操作
信号量模型里面down()、up()这两个操作历史上最早称为P操作和V操作,所以心好累模型也被称为PV原语。在Semaphore中,down()和up()对应的则是acquire()和release()
2、Semaphore使用
1)、使用Semaphore实现累加器(互斥)
static int count;
//初始化信号量
static final Semaphore semaphore = new Semaphore(1);
//用信号量保证互斥
static void addOne() {
try {
semaphore.acquire();
count += 1;
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release();
}
}
假设两个线程T1和T2同时访问addOne()方法,当它们同时调用acquire()的时候,由于acquire()是一个原子操作,所以只能由一个线程(假设T1)把信号量里的计数器减为0,另外一个线程(T2)则是将计数器减为-1。对于线程T1,信号量里面的计数器的值是0,大于等于0,所以线程T1会继续执行;对于线程T2,信号量里面的计数器的值是-1,小于0,按照信号量模型里的对down()操作的描述,线程T2将被阻塞。所以此时只有线程T1会进入临界区执行count += 1
当线程T1执行release()操作,也就是up()操作的时候,信号量里计数器的值是-1,加1之后的值是0,小于等于0,按照信号量模型里对up()操作的描述,此时等待队列中的T2将会被唤醒。于是T2在T1执行完临界区代码之后才获得了进入临界区执行的机会,从而保证了互斥性
2)、使用Semaphore控制同时访问特定资源的线程数量
public class SemaphoreDemo {
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(8);
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + "开始执行");
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release();
}
}).start();
}
}
}
运行结果:第一次有8个线程执行了打印,等待5秒后,后两个线程执行了打印
3)、使用Semaphore实现一个对象池
public class ObjPool<T, R> {
final List<T> pool;
//用信号量实现限流器
final Semaphore sem;
//构造函数
ObjPool(int size, T t) {
pool = new Vector<T>();
for (int i = 0; i < size; i++) {
pool.add(t);
}
sem = new Semaphore(size);
}
//利用对象池的对象,调用func
R exec(Function<T, R> func) throws InterruptedException {
T t = null;
sem.acquire();
try {
t = pool.remove(0);
return func.apply(t);
} finally {
pool.add(t);
sem.release();
}
}
public static void main(String[] args) throws InterruptedException {
//创建对象池
ObjPool<Long, String> pool = new ObjPool<Long, String>(10, 2L);
//通过对象池获取t,之后执行
pool.exec(t -> {
System.out.println(t);
return t.toString();
});
}
}
Semaphore可以允许多个线程访问一个临界区,用Vector保存对象实例,Vector是线程安全的,用Semaphore 实现限流器。关键的代码是ObjPool里面的exec()方法,这个方法里面实现了限流的功能。在这个方法里面,我们首先调用acquire()方法(与之匹配的是在finally里面调用release()方法),假设对象池的大小是10,信号量的计数器初始化为10,那么前10个线程调用acquire()方法,都能继续执行,而其他线程则会阻塞在acquire()方法上。对于通过信号灯的线程,我们为每个线程分配了一个对象t(这个分配工作是通过pool.remove(0)实现的),分配完之后会执行一个回调函数func,而函数的参数正是前面分配的对象t;执行完回调函数之后,它们就会释放对象(这个释放工作是通过pool.add(t)实现的),同时调用release()方法来更新信号量的计数器。如果此时信号量里计数器的值小于等于0,那么说明有线程在等待,此时会自动唤醒等待的线程
3、Semaphore源码分析
Semaphore还是使用AQS实现的。Sync只是对AQS的一个修饰,并且Sync有两个实现类,用来指定获取信号量时是否采用公平策略
public Semaphore(int permits) {
sync = new NonfairSync(permits);
}
public Semaphore(int permits, boolean fair) {
sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}
Sync(int permits) {
setState(permits);
}
Semaphore默认釆用非公平策略,如果需要使用公平策略则可以使用带两个参数的构造函数来构造Semaphore对象
如CountDownLatch构造函数传递 的初始化信号量个数permits被赋给了AQS的state状态变量一样,这里AQS的state值表示当前持有的信号量个数
1)、acquire()方法
public void acquire() throws InterruptedException {
//调用AQS中的模板方法acquireSharedInterruptibly
sync.acquireSharedInterruptibly(1);
}
AQS中的acquireSharedInterruptibly方法:
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
//调用Semaphore中sync重写的tryAcquireShared方法,根据构造函数确定使用的公平策略
if (tryAcquireShared(arg) < 0)
//如果获取失败则放入阻塞队列。然后再次尝试,如果失败则调用park方法挂起当前线程
doAcquireSharedInterruptibly(arg);
}
非公平策略:
protected int tryAcquireShared(int acquires) {
return nonfairTryAcquireShared(acquires);
}
final int nonfairTryAcquireShared(int acquires) {
for (;;) {
//获取当前信号量值
int available = getState();
//计算当前剩余值
int remaining = available - acquires;
//如果当前剩余值小于0或者CAS设置成功则返回
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
如果线程A先调用了aquire()方法获取信号量,但是当前信号量个数为0,那么线程A会被放入AQS的阻塞队列。过一段时间后线程C调用了release()方法释放了一个信号量,如果当前没有其他线程获取信号量,那么线程A就会被激活,然后获取该信号量,但是假如线程C释放信号量后,线程C调用了aquire()方法,那么线程C就会和线程A去竞争这个信号量资源。如果采用非公平策略,线程C完全可以在线程A被激活前,或者激活后先于线程A获取到该信号量,也就是在这种模式下阻塞线程和当前请求的线程是竞争关系,而不遵循先来先得的策略
公平策略:
protected int tryAcquireShared(int acquires) {
for (;;) {
//公平策略是看当前线程节点的前驱节点是否也在等待获取该资源,如果是则自己放弃获取的权限, 然后当前线程会被放入AQS阻塞队列,否则就去获取
if (hasQueuedPredecessors())
return -1;
int available = getState();
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
}
2)、release()方法
public void release() {
//调用AQS中的模板方法releaseShared
sync.releaseShared(1);
}
AQS中的releaseShared方法:
public final boolean releaseShared(int arg) {
//尝试释放资源
if (tryReleaseShared(arg)) {
//释放资源成功则调用park方法唤醒AQS队列里面最先挂起的线程
doReleaseShared();
return true;
}
return false;
}
Sync中重写的tryReleaseShared方法:
protected final boolean tryReleaseShared(int releases) {
for (;;) {
//获取当前信号量值
int current = getState();
//将当前信号量值增加releases
int next = current + releases;
if (next < current)
throw new Error("Maximum permit count exceeded");
//使用CAS保证更新信号量值的原子性
if (compareAndSetState(current, next))
return true;
}
}