在上一篇 同时处理多个请求,记录了同时处理多个请求的几种方式,本篇主要介绍多线程处理时,进行超时控制。也就是说超时了的任务扔掉,未超时的任务返回
在研究线程相关的API时,发现了future.get(timeout, unit)方法,意思是在指定的时间内会等待任务执行,超时则抛异常。激动之余赶紧试了下:
修改下ParallelService中的接口(三个请求的用时分别是1s、2s、10s):
@Slf4j @Service public class ParallelService { public String requestA() { try { TimeUnit.MILLISECONDS.sleep(1000); } catch (InterruptedException e) { log.info("requestA被打断"); } return "A"; } public String requestB() { try { TimeUnit.MILLISECONDS.sleep(2000); } catch (InterruptedException e) { log.info("requestB被打断"); } return "B"; } public String requestC() { try { TimeUnit.MILLISECONDS.sleep(10000); } catch (InterruptedException e) { log.info("requestC被打断"); } return "C"; } }
增加测试方法(超时时间是3s,也就是说超过3s的任务会被扔掉):
/** * 多线程请求(带超时时间) */ @GetMapping("/test4") public void test4() { long start = System.currentTimeMillis(); List<String> list = new ArrayList<>(); List<Future<String>> futureList = new ArrayList<>(); ExecutorService executor = Executors.newFixedThreadPool(3); // 开启3个线程 IntStream.range(0, 3).forEach(index -> { Future<String> task = executor.submit(() -> request(index)); futureList.add(task); }); for (int i = 0; i < futureList.size(); i++) { Future<String> future = futureList.get(i); try { // future.get(timeout, unit):在指定的时间内会等待任务执行,超时则抛异常。 String result = future.get(3, TimeUnit.SECONDS); log.info("结果:{}", result); list.add(result); } catch (TimeoutException e) { log.info("task{},超时", i); // 强制取消该任务 future.cancel(true); } catch (Exception e) { log.error(e.getMessage(), e); } } // 停止接收新任务,原来的任务继续执行 executor.shutdown(); log.info("多线程,响应结果:{},响应时长:{}", Arrays.toString(list.toArray()), System.currentTimeMillis() - start); }
发送请求,结果:
表面上看感觉效果达到了,时长超过3s的请求被扔掉了。但仔细看发现了个问题:响应时长为啥是5s左右,不应该是3s吗?
在future.get方法前后加上日志:
long start1 = System.currentTimeMillis(); // future.get(timeout, unit):在指定的时间内会等待任务执行,超时则抛异常。 String result = future.get(3, TimeUnit.SECONDS); log.info("结果:{},用时:{}", result, System.currentTimeMillis() - start1); list.add(result);
重新请求一次,结果:
好奇怪,为啥requestB的用时是1s呢?通过多次试验终于发现了真相:
future.get(timeout, unit):在指定的时间内会等待任务执行,超时则抛异常。任务执行的时间是获取到结果的时长。由于每个任务是同时执行的, 但是获取结果时,是阻塞的,也就是串行获取的,所以每个任务获取结果的时长 = 当前任务请求时长 - 上一个任务请求时长。
由此可以计算出任务a时长是1s,任务b是2-1=1s,任务c是10-2=8s。至于总时长5s = 任务b获取结果用时2s + 超时时间3s
结论:当只有一个任务时,超时时间有效,当多个任务执行时,超时时间无效
那如果将future.get(timeout, unit) 方法放在一个子线程中,异步去获取结果,能达到效果吗?拭目以待:
/** * 多线程请求(带超时时间) */ @GetMapping("/test5") public void test5() { long start = System.currentTimeMillis(); List<String> list = new ArrayList<>(); List<Future<String>> futureList = new ArrayList<>(); ExecutorService executor = Executors.newFixedThreadPool(3); // 开启3个线程 IntStream.range(0, 3).forEach(index -> { Future<String> task = executor.submit(() -> request(index)); futureList.add(task); }); for (int i = 0; i < futureList.size(); i++) { Future<String> future = futureList.get(i); ExecutorService executor2 = Executors.newSingleThreadExecutor(); int j = i; executor2.execute(() -> { try { // 在指定的时间内会等待任务执行,超时则抛异常 long start1 = System.currentTimeMillis(); String result = future.get(2, TimeUnit.SECONDS); log.info("结果:{},用时:{}", result, System.currentTimeMillis() - start1); list.add(result); } catch (TimeoutException e) { log.info("task{},超时", j); future.cancel(true); } catch (Exception e) { log.error(e.getMessage(), e); } }); } // 停止接收新任务,原来的任务继续执行 executor.shutdown(); while (true) { // 将future.get放入子线程后,由于不会阻塞,所以就直接运行到下面。需要通过判断所有线程是否结束来获取最终结果 if (executor.isTerminated()) { log.info("多线程,响应结果:{},响应时长:{}", Arrays.toString(list.toArray()), System.currentTimeMillis() - start); break; } } }
请求一次,结果:
完美达到我们的效果!再来压测一下:
从JMeter结果可以看到:平均响应时长:3438ms,最小响应时长:2415ms,最大响应时长:4707ms,TPS:8.1/sec
结论:虽然代码复杂一点,但是效果基本达到了,不过有一点,开启的线程的翻了一倍,对内存消耗比较大
再次研究api,找到了最终的大招,使用invokeAll(tasks, timeout, unit)方法:
/** * 多线程请求(带超时时间) */ @GetMapping("/test6") public void test6() { long start = System.currentTimeMillis(); List<String> list = new ArrayList<>(); ExecutorService executor = Executors.newFixedThreadPool(3); // 开启3个线程 List<Callable<String>> callableList = new ArrayList<>(); IntStream.range(0, 3).forEach(index -> { callableList.add(() -> request(index)); }); try { log.info("开始执行"); long start1 = System.currentTimeMillis(); // invokeAll会阻塞。必须等待所有的任务执行完成后统一返回,这里的超时时间是针对的所有tasks,而不是单个task的超时时间。 // 如果超时,会取消没有执行完的所有任务,并抛出超时异常 List<Future<String>> futureList = executor.invokeAll(callableList, 2, TimeUnit.SECONDS); log.info("执行完,用时:{}", System.currentTimeMillis() - start1); for (int i = 0; i < futureList.size(); i++) { Future<String> future = futureList.get(i); try { list.add(future.get()); } catch (CancellationException e) { log.info("超时任务:{}", i); } catch (Exception e) { log.error(e.getMessage(), e); } } } catch (InterruptedException e1) { log.info("线程被中断"); } // 停止接收新任务,原来的任务继续执行 executor.shutdown(); log.info("多线程,响应结果:{},响应时长:{}", Arrays.toString(list.toArray()), System.currentTimeMillis() - start); }
请求一次,结果:
完美,太完美了,再来压测一下:
从JMeter结果可以看到:平均响应时长:2114ms,最小响应时长:2007ms,最大响应时长:2485ms,TPS:13.3/sec
结论:代码很简洁,效率也很好