前言
在并发编程中经常用非阻塞模型,不论是继承thread类,还是实现runnable接口,都无法保证获取到之前的执行结果。通过实现Callback接口,并用Future可以来接收多线程的执行结果。
Future表示一个可能还没有完成的异步任务的结果,针对这个结果可以添加Callback以便在任务执行成功或失败后作出相应的操作。
java.util.concurrent.Future#get(long, java.util.concurrent.TimeUnit)方法可以在获取执行结果同时设置一个超时时间,如果超时当前线程会因为收到 TimeoutException 而被中断,线程池里继续执行。但是future的get()方法会堵塞当前线程的执行。
本文通过线程池多线程任务调用,串行阻塞式运行,虽然慢但稳妥,通过get()设置接口超时时间、要求单个线程运行时必须要有超时时间,超时则放弃,并运行下一个任务。
这里的实现仅能用,暂时不完善。
实现
拆解
我们假设要执行的任务work为:将某个UUID添加进list中,并在任务重打印当前执行的线程名字以区别,某个情况时,接口调用时间延长为2200ms,使接口调用超时,如下:
/**
* 要执行的任务 work
* @param list
* @return
*/
public static List<String> work(List<String> list, String s) throws Exception {
// 模拟某个情况时,接口调用时间延长为2200ms,使接口调用超时
if (s.equals("BBB")) {
Thread.sleep(1100);
}
list.add(UUID.randomUUID().toString() + " " + s);
System.out.println("当前执行线程名 : " + Thread.currentThread().getName());
return list;
}
我们初始化线程池并定义线程池参数和策略,这里我们给定两个线程。
后创建线程,多线程执行 work 任务:定义线程执行的任务work,并将其放入线程池中执行,并通过get()设置线程调用超时时间,执行完之后关闭手动线程池,并返回结果:
/**
* 多线程执行 work 任务
* 轮询阻塞式执行
* @param list
* @return List<String>
* @throws InterruptedException
*/
public static List<String> getExecutorService1(List<String> list) throws InterruptedException{
System.out.println("=========== 开始执行多线程 ==========");
// 线程池只给2个线程
ExecutorService executorService = new ThreadPoolExecutor(2, 2, 0L,
TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
// 循环取出notAllowedKeyWords中的词记为notAllowedKeyWord,通过线程池分配的线程,调用work接口,把单个notAllowedKeyWord放入list中
for (String notAllowedKeyWord : notAllowedKeyWords) {
// 中间变量tempList
System.out.println("线程准备中...");
FutureTask<List<String>> future = new FutureTask<> (
new Callable<List<String>>() {
@Override
public List<String> call() throws Exception {
return work(list, notAllowedKeyWord);
}
});
// 加载future
executorService.execute(future);
try {
// 线程池线程执行设置超时时间
future.get(1, TimeUnit.SECONDS);
} catch (Exception e) {
// 超时报个错
e.printStackTrace();
// 超时则放弃这个子线程的任务,让其他线程继续往下执行
future.cancel(Boolean.TRUE);
}
}
// 执行完,关闭线程池
executorService.shutdown();
// 执行完,返回结果
return list;
}
ExecutorService 实现多线程执行任务的方法:
- 方法名为 getExecutorService,接收一个 List 类型的参数 list,返回一个 List 类型的结果,并可能抛出 InterruptedException 异常。
- 初始化线程池:
创建一个 ThreadPoolExecutor 实例,参数分别为核心线程数(2)、最大线程数(2)、空闲线程存活时间(0 秒)、时间单位(毫秒)、以及一个无界阻塞队列 LinkedBlockingQueue 作为工作队列。 - 循环处理任务:
遍历 notAllowedKeyWords 列表中的每个元素 notAllowedKeyWord。 - 创建 FutureTask:
对于每个 notAllowedKeyWord,创建一个新的 FutureTask<List> 实例,其中的 Callable 实现类在 call() 方法中调用 work(list, notAllowedKeyWord) 方法。 - 提交任务到线程池:
使用 executorService.execute(future) 将 FutureTask 提交到线程池中执行。 - 获取结果并设置超时:
使用 future.get(1, TimeUnit.SECONDS) 获取 FutureTask 的结果,并设置超时时间为 1 秒。 - 处理超时异常:
如果在获取结果时发生超时异常,则捕获该异常,打印堆栈信息,并取消该 FutureTask。 - 关闭线程池:
在所有任务执行完毕后,调用 executorService.shutdown() 关闭线程池。 - 返回结果:最后返回处理后的 list 结果。
notAllowedKeyWords 列表中的每个元素,并将结果添加到 list 中。线程池限制了同时执行的任务数量为 2,如果某个任务执行超过 1 秒还未完成,则取消该任务并让其他任务继续执行。最后,关闭线程池并返回处理后的 list 结果。
Main.java
Main.java
完成代码脚本如下,我们定义了一个key字符串,将中的字符串字符拆解并放入notAllowedKeyWords 中,随后让线程获取notAllowedKeyWords 中的单词,执行work任务:
import java.util.*;
import java.util.concurrent.*;
public class Main {
// 20个
private static String key = "AAA|BBB|CCC|DDD|EEE|FFF|GGG|HHH|III|JJJ|KKK|LLL|MMM|NNN|OOO|PPP|QQQ|RRR|SSS|TTT";
private static List<String> notAllowedKeyWords = new ArrayList<>(0);
private static List<String> res = new ArrayList<>();
static {
String keyStr[] = key.split("\\|");
for (String str : keyStr) {
notAllowedKeyWords.add(str);
}
}
/**
* 要执行的任务 work
* @param list
* @return
*/
public static List<String> work(List<String> list, String s) throws Exception {
// 模拟某个情况时,接口调用时间延长为2200ms,使接口调用超时
if (s.equals("BBB")) {
Thread.sleep(1100);
}
list.add(UUID.randomUUID().toString() + " " + s);
System.out.println("当前执行线程名 : " + Thread.currentThread().getName());
return list;
}
/**
* 多线程执行 work 任务
* 轮询阻塞式执行
* @param list
* @return List<String>
* @throws InterruptedException
*/
public static List<String> getExecutorService1(List<String> list) throws InterruptedException{
System.out.println("=========== 开始执行多线程 ==========");
// 线程池只给2个线程
ExecutorService executorService = new ThreadPoolExecutor(2, 2, 0L,
TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
// 循环取出notAllowedKeyWords中的词记为notAllowedKeyWord,通过线程池分配的线程,调用work接口,把单个notAllowedKeyWord放入list中
for (String notAllowedKeyWord : notAllowedKeyWords) {
// 中间变量tempList
System.out.println("线程准备中...");
FutureTask<List<String>> future = new FutureTask<> (
new Callable<List<String>>() {
@Override
public List<String> call() throws Exception {
return work(list, notAllowedKeyWord);
}
});
// 加载future
executorService.execute(future);
try {
// 线程池线程执行设置超时时间
future.get(1, TimeUnit.SECONDS);
} catch (Exception e) {
// 超时报个错
e.printStackTrace();
// 超时则放弃这个子线程的任务,让其他线程继续往下执行
future.cancel(Boolean.TRUE);
}
}
// 执行完,关闭线程池
executorService.shutdown();
// 执行完,返回结果
return list;
}
/**
* 主程序
* 任务目的是传入 List<String> res 到 getExecutorService 接口中,
* getExecutorService 方法构造多线程,将多个 【randomUUID + " " + key中的各个单词】 组成元素,存入res中,最后返回res
* @param args
*/
public static void main(String[] args) throws InterruptedException {
System.out.println("notAllowedKeyWords: " + notAllowedKeyWords);
System.out.println("notAllowedKeyWords.size(): " + notAllowedKeyWords.size());
long startTime = System.currentTimeMillis();
try {
res = getExecutorService1(res);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 执行完,总时间打印
System.out.println("========== submit总共cost 时间:" + (System.currentTimeMillis() - startTime)/1000 + "秒 ===========");
System.out.println("res: " + res);
System.out.println("res.size(): " + res.size());
}
}
运行
运行结果,所有线程执行时打印所属线程号,可以看到是交替进行,最后list结果也打印出来。
可以看到最后虽然线程本应该是异步执行的,但是通过get()阻塞线程获取执行结果,因此变成串行执行。
可以看到如果存在线程超时,当前线程会因为收到 TimeoutException 而被中断,但是线程池里其它的线程却继续执行完毕。