多线程处理的结果顺序完成和结束
注意:本文章讲的是多线程如何顺序结束,不是多线程如何顺序执行!
使用场景:
主线程每次收到一批数据处理后,需要对数据进行处理,数据长度和大小不定,有的几毫秒就处理好了,有的几秒钟才能处理完。如果采用单线程,耗时过长肯定是无法接受的,所以选择使用多线程。
但是!每一批数据处理完成后,还需要按照接收到数据的顺序,将处理的数据发送出去,所以每一个数据发送完成前还要判断前一个数据是否发送完成。
真的是···很让人头痛。
比如:我们有一个List,现在需要for循环处理这个List,每一个String对象我们都开启一个线程去处理它,最后我们需要将处理的结果按for循环的顺序发送到其他地方。
解决方案:
一、常用方案
等待所有线程全部结束后,按顺序发送。
理论上来说:这不算是多线程的顺序结束,算是最终结果的顺序执行。即我们使用CountDownLatch或者CompletableFuture来阻塞线程,等待所有的线程或者异步任务处理完成后,将结果进行for循环处理。
需要注意的是:使用这种方式获取到的结果List顺序不一定是按照for循环的顺序,需要加标识进行排序处理。
这种方法实现比较简单,但是处理时间取决于当前批次数据的最长数据处理时间。比如当前批次数据某个线程处理长达10秒,那么所有线程处理完的时间就是10秒+for循环发送时间,比for循环单线程处理快是肯定。
二、嵌套方案(效率优于常用方案)
因为处理数据部分是不要求顺序的,只有在发送时才要求顺序发送。那么我想是不是有一种方案,可以先让所有线程进行数据处理,然后当数据处理部分完成后,卡在发送部分前面。因为是for循环执行的线程,所以每一个线程都可以设置前一个线程作为自己的参数,以此实现嵌套效果,当前线程的前一个线程执行完成后,当前线程才会进行发送处理,以此类推,实现数据处理部分多线程处理,发送部分顺序结束。
代码核心要素:前一个线程作为当前现成的参数、Lock、Condition。
以下是示例代码:
public void test1(List<String> list) throws Exception {
ExecutorService executor = Executors.newFixedThreadPool(10);
List<SendTask> sendTaskList = new ArrayList<>();
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
for (int i = 0; i < list.size(); ++i) {
String str = list.get(i);
if (i == 0) {
//第一个的前一个线程设置为null
sendTaskList.add(new SendTask(str, null, lock, condition));
} else {
//第二次开始,参数为前一个线程
sendTaskList.add(new SendTask(str, sendTaskList.get(sendTaskList.size() - 1), lock, condition));
}
}
executor.submit(sendTask);
}
线程处理代码:
public class SendTask implements Runnable {
private final String message;
private SendTask previousTask;
private Lock lock;
private Condition condition;
private volatile boolean isDone;
public SendTask(String message, SendTask previousTask, Lock lock, Condition condition) {
this.message = message;
//这里接收上一个线程
this.previousTask = previousTask;
this.lock = lock;
this.condition = condition;
}
private void send() {
// 在这里编写发送逻辑
System.out.println(message + "开始发送");
Integer time = 500;
try {
Thread.sleep(time);
} catch (InterruptedException e) {
e.printStackTrace();
}
// System.out.println(message + "发送时间" + time);
System.out.println(message + " 发送完成!");
}
@Override
public void run() {
try {
// System.out.printf("%s 开始处理", message);
Integer time = (new Random().nextInt(3) + 1) * 1000;
Thread.sleep(time);
// System.out.println(message + "处理时间" + time);
lock.lock();
//如果不是第一个线程,并且前一个线程未执行完成,则等待
while (previousTask != null && !previousTask.isDone()) {
condition.await();
}
send();
isDone = true;
//提醒可以抢锁了
condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//解锁
lock.unlock();
}
}
public boolean isDone() {
return isDone;
}
这种方案,就我自己的测试结果来说,是比普通方案要快的,并且顺序是能够得到保障的。但是需要注意的是,Conditon.await()和Lock可能会存在死锁的情况,真实业务代码里需要考虑到这方面,最好设置一个超时时间,避免死锁的发生。