初探高并发—ExecutorCompletionService
为什么要引入高并发
众所周知,程序中的代码是从下往下顺序执行的,当我们需要在一个方法中同时执行多个耗时的任务时所消耗时间就会大于等于这些任务消耗的累加时间。那么有没有一种办法可以让这些耗时的任务同时执行呢?这时候就需要并发编程,让这些任务在不同的线程上分别执行,达到理论上的同步执行效果。
这里引入一下并发、并行、高并发的概念:
并发:指的是在同一时间间隔内,多个任务交替执行的能力。在计算机中,同时存在多个进程或线程,每个进程或线程都在执行各自的任务,这就是并发。在并发执行中,每个任务都会在一定时间内执行一部分,然后切换到下一个任务,如此往复,直到所有任务完成。
并行:指的是在同一时间间隔内,多个任务同时执行的能力。在计算机中,同时存在多个进程或线程,每个进程或线程都在执行各自的任务,这就是并行。在并行执行中,每个任务都会在不同的处理器或者核心上同时执行,因此可以同时完成多个任务。
高并发:指的是在同一时间内处理的请求或者任务数量非常大。在互联网应用中,高并发是一个非常重要的概念,因为当用户访问量非常大时,服务器需要同时处理大量的请求,如果服务器不能很好地处理高并发请求,就会导致系统崩溃或者响应变慢。
ExecutorCompletionService分析
ExecutorCompletionService实现了CompletionService接口,CompletionService的方法有以下:
- Future submit(Callable task):提交一个Callable类型任务,并返回该任务执行结果关联的Future;
- Future submit(Runnable task,V result):提交一个Runnable类型任务,并返回该任务执行结果关联的Future;
- Future take():从内部阻塞队列中获取并移除第一个执行完成的任务,阻塞,直到有任务完成;
- Future poll():从内部阻塞队列中获取并移除第一个执行完成的任务,获取不到则返回null,不阻塞;
- Future poll(long timeout, TimeUnit unit):从内部阻塞队列中获取并移除第一个执行完成的任务,阻塞时间为timeout,获取不到则返回null;
结构如下:
点开ExecutorCompletionService的源码,结构如下:
其中包含了三个私有属性:executor、aes、completionQueue。
关于ExecutorCompletionService这两个构造方法,源码如下:
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>>();
}
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;
}
也就是说新建ExecutorCompletionService实例对象的时候,可以自行指定阻塞队列的类型
阻塞队列:在Java多线程编程中,阻塞队列是一种特殊的队列,它可以在队列为空时阻塞获取元素的线程,也可以在队列已满时阻塞插入元素的线程。这种队列通常用于实现生产者-消费者模式,其中生产者线程向队列中插入任务,消费者线程从队列中取出任务并执行。Java中提供了多种类型的阻塞队列,包括:
-
ArrayBlockingQueue:基于数组实现的有界阻塞队列,按照先进先出的原则进行元素插入和移除。
-
LinkedBlockingQueue:基于链表实现的可选有界阻塞队列,按照先进先出的原则进行元素插入和移除。
-
PriorityBlockingQueue:基于优先级堆实现的无界阻塞队列,元素按照优先级顺序进行插入和移除。
-
SynchronousQueue:一个不存储元素的阻塞队列,每个插入操作必须等待一个相应的删除操作,反之亦然。
-
DelayQueue:一个基于优先级堆实现的延迟阻塞队列,其中的元素只有在其指定的延迟时间到达后才能被取出。
这些阻塞队列都是线程安全的,可以在多线程环境下使用。不同的阻塞队列适用于不同的场景,可以根据自己的需求选择合适的队列。
如果新建实例对象时不指定阻塞队列类型,默认使用的是LinkedBlockingQueue。
ExecutorCompletionService优势
为什么要使用ExecutorCompletionService而不是直接使用线程池进行任务提交?
原因是如果我们将任务直接提交到线程池中,通过Futrue类的get()方法,会造成堵塞,需要先等执行任务1的线程结束返回结果,才会进行获取下一个任务的执行的结果。而使用ExecutorCompletionService则不会有这样的情况,在ExecutorCompletionService内部维护了一个阻塞队列,提交的任务,先执行完的先进入队列,所以你通过 poll 或 take 获得的肯定是最先执行完的任务结果。
但是在实际生产中,如果说我们不需要返回结果的时候,可以自行选择,毕竟适合自己的才是最好的。
ExecutorCompletionService实操
这里直接上代码:
/**
* 获取用户信息sleep时间:1000ms
* 获取家庭信息sleep时间:3000ms
* 获取学校信息sleep时间:5000ms
*/
@SpringBootTest
class SpringbootParallelApplicationTests {
@Autowired()
private ClassService classService;
@Autowired()
private FamilyService familyService;
@Autowired()
private UserService userService;
void normal() {
StopWatch watch = new StopWatch();
watch.start();
UserInfoDTO userInfo = userService.getUserInfo("user01");
ClassDTO classInfo = classService.getClassDTO("class01");
FamilyDTO familyInfo = familyService.getFamilyInfo("family01");
watch.stop();
long millis = watch.getTotalTimeMillis();
System.out.println("=====================普通调用===========================");
System.out.println("用户信息:" + userInfo.toString());
System.out.println("班级信息:" + classInfo.toString());
System.out.println("家庭信息:" + familyInfo.toString());
System.out.println("程序耗时:" + millis + "毫秒");
}
void executor() throws ExecutionException, InterruptedException {
// 计时器
StopWatch watch = new StopWatch();
watch.start();
// 新建线程池
ExecutorService executor = Executors.newFixedThreadPool(3);
// 提交线程查询用户信息
Future<BaseRspDTO<Object>> submit1 = executor.submit(() -> {
UserInfoDTO userInfo = userService.getUserInfo("user01");
BaseRspDTO<Object> dto = new BaseRspDTO<>();
dto.setKey("userInfo");
dto.setData(userInfo);
return dto;
});
BaseRspDTO<Object> rspDTO1 = submit1.get();
UserInfoDTO user = (UserInfoDTO) rspDTO1.getData();
// 提交线程查询班级信息
Future<BaseRspDTO<Object>> submit2 = executor.submit(() -> {
ClassDTO classInfo = classService.getClassDTO("class01");
BaseRspDTO<Object> dto = new BaseRspDTO<>();
dto.setKey("classInfo");
dto.setData(classInfo);
return dto;
});
BaseRspDTO<Object> rspDTO2 = submit2.get();
ClassDTO classDto = (ClassDTO) rspDTO2.getData();
// 提交线程查询学校信息
Future<BaseRspDTO<Object>> submit3 = executor.submit(() -> {
FamilyDTO familyInfo = familyService.getFamilyInfo("family01");
BaseRspDTO<Object> dto = new BaseRspDTO<>();
dto.setKey("familyInfo");
dto.setData(familyInfo);
return dto;
});
BaseRspDTO<Object> rspDTO3 = submit3.get();
FamilyDTO family = (FamilyDTO) rspDTO3.getData();
watch.stop();
// 获取耗时
long millis = watch.getTotalTimeMillis();
System.out.println("=====================线程池调用===========================");
System.out.println("用户信息:" + user.toString());
System.out.println("班级信息:" + classDto.toString());
System.out.println("家庭信息:" + family.toString());
System.out.println("程序耗时:" + millis + "毫秒");
}
void executorWithOnly() {
// 计时器
StopWatch watch = new StopWatch();
watch.start();
// 新建线程池
ExecutorService executor = Executors.newFixedThreadPool(3);
// 提交线程查询用户信息
executor.submit(() -> {
UserInfoDTO userInfo = userService.getUserInfo("user01");
BaseRspDTO<Object> dto = new BaseRspDTO<>();
dto.setKey("userInfo");
dto.setData(userInfo);
return dto;
});
// 提交线程查询班级信息
executor.submit(() -> {
ClassDTO classInfo = classService.getClassDTO("class01");
BaseRspDTO<Object> dto = new BaseRspDTO<>();
dto.setKey("classInfo");
dto.setData(classInfo);
return dto;
});
// 提交线程查询学校信息
Future<BaseRspDTO<Object>> submit = executor.submit(() -> {
FamilyDTO familyInfo = familyService.getFamilyInfo("family01");
BaseRspDTO<Object> dto = new BaseRspDTO<>();
dto.setKey("familyInfo");
dto.setData(familyInfo);
return dto;
});
System.out.println("=====================单个结果的线程池调用===========================");
try {
BaseRspDTO<Object> baseRspDTO = submit.get();
FamilyDTO data = (FamilyDTO)baseRspDTO.getData();
System.out.println("家庭信息:" + data.toString());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
watch.stop();
// 获取耗时
long millis = watch.getTotalTimeMillis();
System.out.println("程序耗时:" + millis + "毫秒");
}
void special() {
// 计时器
StopWatch watch = new StopWatch();
watch.start();
// 新建线程池
ExecutorService executor = Executors.newFixedThreadPool(3);
// 新建ExecutorCompletionService实例
ExecutorCompletionService<BaseRspDTO> service = new ExecutorCompletionService<>(executor);
// 提交线程查询用户信息
service.submit(() -> {
UserInfoDTO userInfo = userService.getUserInfo("user01");
BaseRspDTO<Object> dto = new BaseRspDTO<>();
dto.setKey("userInfo");
dto.setData(userInfo);
return dto;
});
// 提交线程查询班级信息
service.submit(() -> {
ClassDTO classInfo = classService.getClassDTO("class01");
BaseRspDTO<Object> dto = new BaseRspDTO<>();
dto.setKey("classInfo");
dto.setData(classInfo);
return dto;
});
// 提交线程查询学校信息
service.submit(() -> {
FamilyDTO familyInfo = familyService.getFamilyInfo("family01");
BaseRspDTO<Object> dto = new BaseRspDTO<>();
dto.setKey("familyInfo");
dto.setData(familyInfo);
return dto;
});
UserInfoDTO userInfo = new UserInfoDTO();
ClassDTO classInfo = new ClassDTO();
FamilyDTO familyInfo = new FamilyDTO();
try {
for (int i = 0; i < 3; i++) {
// 获取ExecutorCompletionService内部中阻塞队列的已完成的Future
Future<BaseRspDTO> poll = service.poll(1, TimeUnit.MINUTES);
// 获取结果
BaseRspDTO dto = poll.get();
String key = dto.getKey();
if ("userInfo".equals(key)) {
userInfo = (UserInfoDTO) dto.getData();
} else if ("classInfo".equals(key)) {
classInfo = (ClassDTO) dto.getData();
} else if ("familyInfo".equals(key)) {
familyInfo = (FamilyDTO) dto.getData();
}
}
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
watch.stop();
// 获取耗时
long millis = watch.getTotalTimeMillis();
System.out.println("=====================通过ExecutorCompletionService调用===========================");
System.out.println("用户信息:" + userInfo.toString());
System.out.println("班级信息:" + classInfo.toString());
System.out.println("家庭信息:" + familyInfo.toString());
System.out.println("程序耗时:" + millis + "毫秒");
}
@Test
void contextLoads() throws ExecutionException, InterruptedException {
normal();
executor();
executorWithoutResult();
special();
}
}
结果
通过executor();executorWithOnly();这两个方法的耗时对比可以验证线程池get()会阻塞这个理论。
需要注意的问题
1、调用poll方法产生的空指针
调用限制时长的poll()方法时,需要合理的设定时间,否则会返回null,容易引发空指针问题
2、需要注意OOM
调用ExecutorCompletionService实例对象后,需要及时的进行take()或者poll()操作,否则执行的结果会不停的堆积在队列中,占用堆内存,最终导致oom