目录
写在前面
今天,在对某个接口返回的数据进行按照更新时间倒序优化时,遇到了下面的问题:
在进行代码逻辑的梳理过程中,发现原始数据集合中就是按照更新时间倒序排序的,但是该接口在面对某些请求时,返回的数据变成了无序的。
首先,看下大概的业务逻辑处理代码,如下:
xxxx
CountDownLatch latch = new CountDownLatch(issueInfos.size());
CopyOnWriteArrayList<JiraTaskInfo> list = new CopyOnWriteArrayList<>();
for (xxx data : dataList) {
executor.submit(() -> {
try {
// 具体业务逻辑处理
synchronized (LOCK) {
list.add(jjj);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
latch.countDown();
}
});
}
try {
latch.await(120, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
从上述代码中,可以看到,在面对大批量数据且复杂业务逻辑处理时,
为了保证高效、避免接口请求超时问题,同时尽量保证集合在循环遍历时,使用多线程处理后数据添加的安全性和顺序性,
同事使用了多线程 & 锁机制的处理方式进行解决。
问题总结
但是,在实际使用中发现:
- 在循环中使用多线程的情况下,不能保证任务的执行顺序与原来的顺序一致。因为多线程的任务在执行过程中是并行的,各个任务的执行完成时间是不确定的。在上述的代码中,使用了executor.submit()方法将任务提交给线程池执行。线程池会根据可用的线程数和任务队列的情况来决定任务的执行顺序。
- 尽管代码中使用了synchronized关键字对list进行了同步控制,但它仅保证了在同一时间只有一个线程可以进行list.add操作。即使保证了同步,不同线程执行完任务后,它们将按照任务完成的顺序尝试添加到list中,但由于多线程执行的不确定性,无法完全保证最终的添加顺序与数据遍历的顺序一致。
从上述解释中可以总结,使用多线程 + 锁机制,虽然保证了高效和数据添加的安全性,但是不能完全保证原始集合中的数据在多线程处理下仍按照循环顺序添加到其他集合中。
解决方案
第一种:使用普通方案--正常循环遍历
在面对数据量少的时候,是可行的,但是在数据量多且逻辑复杂的情况下,是不可行的,会造成请求超时。不适合当前场景。因此不做过多赘述。
第二种:使用异步编程任务 + Stream处理
使用Java8引入的用于处理异步任务的工具类:CompletableFuture。
它可以将异步操作、返回结果、异常处理和多个任务的组合等功能进行链式调用,简化了异步编程的复杂性,可以更加方便地完成异步任务的处理和组合。
同时结合Stream(能够以流式的方式处理集合数据)的函数式编程思想、延迟执行特性(懒加载)、并行处理能力,提高大数据量下的处理速度。
简要代码如下:
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
public class Main {
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
// 创建一个CompletableFuture的集合
List<CompletableFuture<Integer>> futures = list.stream()
.map(i -> CompletableFuture.supplyAsync(() -> process(i)))
.collect(Collectors.toList());
// 将futures转换为一个新的集合
List<Integer> result = futures.stream()
.map(CompletableFuture::join)
.collect(Collectors.toList());
System.out.println(result);
}
private static int process(int i) {
// 这里是你的处理逻辑
return i * i;
}
}
在这个代码中,我们首先创建了一个CompletableFuture对象的集合。每个CompletableFuture都会异步地处理输入列表中的一个元素。
然后,我们使用join方法,它会阻塞直到CompletableFuture完成,这样可以确保最终结果列表中元素的顺序与输入列表中的顺序一致。
注意:这种方式中,虽然每个元素的处理是异步的,但是最终结果的收集是按照原有遍历的顺序进行的,所以能够保证顺序性。
使用上述处理逻辑,完美解决问题!
【扩展】
异步任务和多线程都是用于实现并发编程的技术,但它们在实现方式和目的上有一些区别。
1、实现方式:
- 异步任务:异步任务是通过将任务的执行和等待结果的过程解耦来实现的。当执行一个异步任务时,任务会在后台线程或线程池中执行,而当前线程可以继续执行其他操作,不需要等待任务完成。
- 多线程:多线程是通过创建多个线程来同时执行多个任务的方式实现的。每个线程都独立执行任务,可以并行地执行不同的代码块。
2、目的和使用场景:
- 异步任务:异步任务主要用于提高系统的吞吐量和响应性能。它适用于涉及IO操作、远程调用、等待资源或其他耗时操作的场景。通过异步执行这些耗时操作,可以释放当前线程,提高系统的并发性和响应速度。
- 多线程:多线程主要用于并行处理任务和利用多核CPU的计算能力。它适用于需要同时执行多个计算密集型任务、并行处理大数据集、以及需要响应实时事件的场景。通过利用多线程,可以将任务分解并并行地处理,提高程序的并行度和处理能力。
3、编程模型和线程管理:
- 异步任务:异步任务通常由框架或库提供的API来支持,例如Java中的CompletableFuture
- 多线程:多线程需要开发者手动创建和管理线程,涉及线程的生命周期、并发控制、线程间通信等问题。开发者需要自己处理线程的创建、启动、同步、共享资源等方面的问题。
尽管异步任务和多线程都可以实现并发编程,但它们的设计思想和使用方式有所不同。异步任务更关注于提高系统的并发性和响应性能,而多线程更关注于任务并行和计算能力的提升。在选择使用哪种技术时,应根据具体的需求和场景来选择合适的方式。