目录
ReentrantLock、synchronized 大家经常使用也基本了解的比较多,今天介绍一下其他几个很棒的并发工具类
一、CountDownLatch 和 CyclicBarrier
例子背景:4个工作,开发工作1、开发工作2、测试、上线。
单线程执行所有的工作,整个执行顺序为串行,占用的资源少但是完成时长就会更长。
串行开发
private Boolean WORK = Boolean.TRUE;
@Test
public void one() throws InterruptedException {
Integer i = 0;
//一直有工作
while (WORK){
long start = System.currentTimeMillis();
System.out.println("######开始第" + (++i) +"次需求######");
System.out.println("【开发工作1】开始");
Thread.sleep(1000);
System.out.println("【开发工作1】完成");
System.out.println("【开发工作2】开始");
Thread.sleep(3000);
System.out.println("【开发工作2】完成");
System.out.println("【测试】完成");
System.out.println("【上线】完成");
long use = System.currentTimeMillis() - start;
System.out.println("######完成第" + i +"次需求用时:" + use + "######");
}
}
执行结果:
通过结果可以看到单线程执行基本耗时在:4008ms
🤔:产品上线的效率还能提高不?
👨💻:可以,我们多加几个人。请看CountDownLatch
1.1 CountDownLatch
为了提高处理效率我们可以开启多线程,但是需要注意一个问题那么就是开发工作1和开发工作2 要全部完成才能开始测试,所以需要让子线程与主线程的一个协同同步,即主线程等待多个子线程。
针对此类问题可以通过Java 的CountDownLatch 来完成线程间的协调。
多人协作
1.1.1 CountDownLatch使用
CountDownLatch类位于java.util.concurrent包下,利用它可以实现类似计数器的功能。比如有一个任务A,它要等待其他4个任务执行完毕之后才能执行,此时就可以利用CountDownLatch来实现这种功能了。
CountDownLatch类只提供了一个构造器:
/**
* Constructs a {@code CountDownLatch} initialized with the given count.
*
* @param count the number of times {@link #countDown} must be invoked
* before threads can pass through {@link #await}
* @throws IllegalArgumentException if {@code count} is negative
*/
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
重要的三个操作方法:
//调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
和await()类似,只不过等待一定的时间后count值还没变为0的话就会继续执行
public boolean await(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}
//将count值减1
public void countDown() {
sync.releaseShared(1);
}
了解了基本的方法,实现一下具体的Demo:
/**
* 通过两个工作并行完成,但是需要全部完成后才可以提测,因此主线程需要等待两个线程才能开始继续
* 本例使用 CountDownLatch 来完成子线程与主线程的协同
* @throws InterruptedException
*/
@Test
public void twoCountDownLatch() throws InterruptedException {
final Integer[] i = {0};
//一直有工作
while (WORK){
long start = System.currentTimeMillis();
System.out.println("######开始第" + (++i[0]) +"次需求######");
CountDownLatch countDownLatch = new CountDownLatch(2);
//第一组开发工作
new Thread(new Runnable() {
@SneakyThrows
@Override
public void run() {
System.out.println("第" + (i[0]) + "次【开发工作1】开始");
Thread.sleep(1000);
System.out.println("第" + (i[0]) + "次【开发工作1】完成");
countDownLatch.countDown();
}
}).start();
//第二组开发工作
new Thread(new Runnable() {
@SneakyThrows
@Override
public void run() {
System.out.println("第" + (i[0]) + "次【开发工作2】开始");
Thread.sleep(3000);
System.out.println("第" + (i[0]) + "次【开发工作2】完成");
countDownLatch.countDown();
}
}).start();
countDownLatch.await();
System.out.println("第" + (i[0]) + "次【测试】完成");
System.out.println("第" + (i[0]) + "次【上线】完成");
long use = System.currentTimeMillis() - start;
System.out.println("######完成第" + (i[0]) +"次需求用时:" + use + "ms ######");
}
}
执行结果:
通过结果可以看到,让开发工作并行后明显缩短了每次产品的交付时间。由4008ms 减少至 3003ms
1.1.2 关键源码
CountDownLatch 内部定义了一个 Syns 类,Sync继承了大名鼎鼎的AQS
/**
* Synchronization control For CountDownLatch.
* Uses AQS state to represent count.
*/
private static final class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 4982264981922014374L;
Sync(int count) {
setState(count);
}
int getCount() {
return getState();
}
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -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;
}
}
}
🤔:还有没有办法再提高一些效率呢,人多了但是有一些时候空闲有点浪费?
👨💻:当然可以!美团的开发是宇宙最强😎💪,我们可以一个工作接着一个工作的干根本停不下来。
那么怎么提高RD的工作效率呢!请看CyclicBarrier
1.2 CyclicBarrier
通过上一工作流程我们可以看到在测试和上线阶段RD1和RD2 处于空闲状态,我们可以让上一个项目的测试和下一个产品开发进行并行。
提出一个假设前提:RD1 和 RD 2 要一起开始新工作,不能独自去开发新产品。
1.因此我们需要让RD1 和 RD 2 协同完成工作,不能RD1 完成一个工作就去开始下一个,需要等待RD2
2.需要在开发工作完成后能够通知测试工作开始
所以又要RD 互相通信等待不能独自去开发下一个产品,又要RD 能够通知QA 进行测试工作。因此可以使用 CyclicBarrier 来实现线程间的互相等待
多人协作2.0
1.2.1 CyclicBarrier使用
CyclicBarrier字面意思回环栅栏,通过它可以实现让一组线程等待至某个状态之后再全部同时执行。叫做回环是因为当所有等待线程都被释放以后,CyclicBarrier可以被重用。我们暂且把这个状态就叫做barrier,当调用await()方法之后,线程就处于barrier了
CyclicBarrier 构造函数有两个。一个支持一次回环结束后进行动作的执行;另一个只是帮助完成多线程间的协同控制。参数parties指让多少个线程或者任务等待至barrier状态;参数barrierAction为当这些线程都达到barrier状态时会执行的内容。
/**
* Creates a new {@code CyclicBarrier} that will trip when the
* given number of parties (threads) are waiting upon it, and which
* will execute the given barrier action when the barrier is tripped,
* performed by the last thread entering the barrier.
*
* @param parties the number of threads that must invoke {@link #await}
* before the barrier is tripped
* @param barrierAction the command to execute when the barrier is
* tripped, or {@code null} if there is no action
* @throws IllegalArgumentException if {@code parties} is less than 1
*/
public CyclicBarrier(int parties, Runnable barrierAction) {
if (parties <= 0) throw new IllegalArgumentException();
this.parties = parties;
this.count = parties;
this.barrierCommand = barrierAction;
}
/**
* Creates a new {@code CyclicBarrier} that will trip when the
* given number of parties (threads) are waiting upon it, and
* does not perform a predefined action when the barrier is tripped.
*
* @param parties the number of threads that must invoke {@link #await}
* before the barrier is tripped
* @throws IllegalArgumentException if {@code parties} is less than 1
*/
public CyclicBarrier(int parties) {
this(parties, null);
}
重要的三个操作方法:
类似于CountDownLatch,CyclicBarrier 提供了两个await()方法主要区别在于一个设置了超时时间,可以防止线程“一直等待”
public int await() throws InterruptedException, BrokenBarrierException {
try {
return dowait(false, 0L);
} catch (TimeoutException toe) {
throw new Error(toe); // cannot happen
}
}
public int await(long timeout, TimeUnit unit)
throws InterruptedException,
BrokenBarrierException,
TimeoutException {
return dowait(true, unit.toNanos(timeout));
}
了解了基本的方法,实现一下具体的Demo:
Executor executor = Executors.newFixedThreadPool(1);
@Test
public void threeCyclicBarrier() throws InterruptedException {
//计数器
final Integer[] x = {0};
final Integer[] y = {0};
final Integer[] z = {0};
final Integer[] j = {0};
final Long[] start = new Long[10000];
CyclicBarrier barrier = new CyclicBarrier(2,
() -> {
executor.execute(() ->
{
System.out.println("第" + (++j[0]) + "次【测试】完成");
System.out.println("第" + (j[0]) + "次【上线】完成");
long use = System.currentTimeMillis() - start[j[0]];
System.out.println("######完成第" + j[0] + "次需求用时:" + use + "ms ######");
});
});
//一直有工作
while (WORK) {
System.out.println("######开始第" + (++x[0]) + "次需求######");
start[x[0]] = System.currentTimeMillis();
//第一组开发工作
new Thread(new Runnable() {
@SneakyThrows
@Override
public void run() {
System.out.println("第" + (++y[0]) + "次【开发工作1】开始");
Thread.sleep(1000);
System.out.println("第" + (y[0]) + "次【开发工作1】完成");
barrier.await();
}
}).start();
//第二组开发工作
new Thread(new Runnable() {
@SneakyThrows
@Override
public void run() {
System.out.println("第" + (++z[0]) + "次【开发工作2】开始");
Thread.sleep(3000);
System.out.println("第" + (z[0]) + "次【开发工作2】完成");
barrier.await();
}
}).start();
}
}
👨💻:效率实在太高了,一次完整产品才花费 1008ms
本质上还是提供并发的工作
1.2.2 关键源码
/** The lock for guarding barrier entry */
private final ReentrantLock lock = new ReentrantLock();
/** Condition to wait on until tripped */
private final Condition trip = lock.newCondition();
/**
* Main barrier code, covering the various policies.
*/
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();
}
int index = --count;
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();
}
}
// loop until tripped, broken, interrupted, or timed out
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 {
// We're about to finish waiting even if we had not
// been interrupted, so this interrupt is deemed to
// "belong" to subsequent execution.
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();
}
}
通过CountDownLatch 也可以完成上述demo 的协同效果,但是需要我们管理每组工作的CountDownLatch ,进行多线程的数据交互管理,而CyclicBarrier则可以方便的“自动绑定”互相等待线程结束时的动作。 大家可以自己动手实现以下CountDownLatch 版的demo 完成宇宙最强开发的无缝衔接工作效率
举一个其他的例子缓解一下审美疲劳,思路可以用来实现上边的Demo
实现N个线程同时启动,然后依次打印各自的序号 😁
@Test
public void testThread(){
int n = 10;
CountDownLatch countDownLatchCur = new CountDownLatch(1);
Te t = new Te(0, countDownLatchCur);
Te tMain = t;
tMain.start();
for (int i = 1; i < n ; i++){
t = new Te(i, t.getCountDownLatchMy()).start();
}
//测试一下打印最后一个。 but 第一个不开始,你真的打印不出来哦!
t.start();
//让第一个线程开始打印吧,大家都等半天了
tMain.getCountDownLatch().countDown();
}
public class Te{
Integer cur ;
CountDownLatch countDownLatch;
CountDownLatch countDownLatchMy = new CountDownLatch(1);
public Te(Integer cur, CountDownLatch countDownLatch){
this.cur = cur;
this.countDownLatch = countDownLatch;
}
public Te start (){
new Thread(() -> {
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(cur);
countDownLatchMy.countDown();
}).start();
return this;
}
public CountDownLatch getCountDownLatchMy(){
return this.countDownLatchMy;
}
public CountDownLatch getCountDownLatch(){
return this.countDownLatch;
}
}
1.3 小结:
CountDownLatch 和 CyclicBarrier 是 Java 并发包提供的两个非常易用的线程同步工具类,这两个工具类用法的区别在这里还是有必要再强调一下:CountDownLatch 主要用来解决一个线程等待多个线程的场景,可以类比一个产品上线,只有各个依赖服务都部署成功才能开启新产品的操作入口;而 CyclicBarrier 是一组线程之间互相等待,类似家庭聚会人齐了才会开饭。除此之外 CountDownLatch 的计数器是不能循环利用的,也就是说一旦计数器减到 0,再有线程调用 await(),该线程会直接通过。但 CyclicBarrier 的计数器是可以循环利用的,而且具备自动重置的功能,一旦计数器减到 0 会自动重置到你设置的初始值。除此之外,CyclicBarrier 还可以设置回调函数,可以说是功能丰富。
二、CompletionService
2.1 CompletionService使用
CompletionService 接口的实现类是 ExecutorCompletionService,这个实现类的构造方法有两个,分别是:ExecutorCompletionService(Executor executor);ExecutorCompletionService(Executor executor, BlockingQueue<Future<V>> completionQueue)。这两个构造方法都需要传入一个线程池,如果不指定 completionQueue,那么默认会使用无界的 LinkedBlockingQueue。任务执行结果的 Future 对象就是加入到 completionQueue 中。
/**
* Creates an ExecutorCompletionService using the supplied
* executor for base task execution and a
* {@link LinkedBlockingQueue} as a completion queue.
*
* @param executor the executor to use
* @throws NullPointerException if executor is {@code null}
*/
public ExecutorCompletionService(Executor executor) {
if (executor == null)
throw new NullPointerException();
this.executor = executor;
this.aes = (executor instanceof AbstractExecutorService) ?
(AbstractExecutorService) executor : null;
this.completionQueue = new LinkedBlockingQueue<Future<V>>();
}
/**
* Creates an ExecutorCompletionService using the supplied
* executor for base task execution and the supplied queue as its
* completion queue.
*
* @param executor the executor to use
* @param completionQueue the queue to use as the completion queue
* normally one dedicated for use by this service. This
* queue is treated as unbounded -- failed attempted
* {@code Queue.add} operations for completed tasks cause
* them not to be retrievable.
* @throws NullPointerException if executor or completionQueue are {@code null}
*/
public ExecutorCompletionService(Executor executor,
BlockingQueue<Future<V>> completionQueue) {
if (executor == null || completionQueue == null)
throw new NullPointerException();
this.executor = executor;
this.aes = (executor instanceof AbstractExecutorService) ?
(AbstractExecutorService) executor : null;
this.completionQueue = completionQueue;
}
CompletionService 的实现原理是内部维护了一个阻塞队列,当任务执行结束就把任务的执行结果加入到阻塞队列中,把任务执行结果的 Future 对象加入到阻塞队列中。
private final BlockingQueue<Future<V>> completionQueue;
public Future<V> submit(Callable<V> task) {
if (task == null) throw new NullPointerException();
RunnableFuture<V> f = newTaskFor(task);
executor.execute(new QueueingFuture<V>(f, completionQueue));
return f;
}
Demo中,我们没有指定 completionQueue,因此默认使用无界的 LinkedBlockingQueue。
通过 CompletionService 接口提供的 submit() 方法提交了三个查询地理位置信息操作,这三个操作将会被 CompletionService 异步执行。最后,我们通过 CompletionService 接口提供的 take() 方法获取一个 Future 对象,调用 Future 对象的 get() 方法就能返回查询地理位置信息操作的执行结果了。
CompletionService 的 3 个方法take()、poll() 都是和阻塞队列相关的,都是从阻塞队列中获取并移除一个元素;它们的区别在于如果阻塞队列是空的,那么调用 take() 方法的线程会被阻塞,而 poll() 方法会返回 null 值。 poll(long timeout, TimeUnit unit) 方法支持以超时的方式获取并移除阻塞队列头部的一个元素,如果等待了 timeout unit 时间,阻塞队列还是空的,那么该方法会返回 null 值。
/**
*伪代码,无法执行
*/
//同时查询多个地图服务,然后拼装一个准确的地理位置信息描述
private Executor executor;
@Test
public void testC(){
CompletionService<Map<String, String>> cs = new ExecutorCompletionService<>(executor);
Map<String, String> selectMap = new ConcurrentHashMap<>();
selectMap.put("百度地图", "地点");
selectMap.put("腾讯地图", "地点");
selectMap.put("高德地图", "地点");
for (Map.Entry<String, String> entry : selectMap.entrySet()) {
cs.submit(() -> seletcMapService.select(entry.getKey(), entry.getValue());
}
Map<String, MapInfo> MapInfoMap = new ConcurrentHashMap<>();
for (int i = 0; i < selectMap.size(); i++) {
try {
// CompletionService 内部的 BlockingQueue<Future<V>> completionQueue;
MapInfo mapInfo = cs.take().get();
MapInfoMap.put(mapInfo.getkey(), mapInfo);
} catch (InterruptedException | ExecutionException e) {
Thread.currentThread().interrupt();
log.error("查询数据异常",e);
}
}
}
2.2 小结
除了CompletionService,我们更经常使用Stream,那么到底应该选哪个呢,引用《Java 8实战》一段知识。
参考:
《Java 并发编程》