前两天当个搬运工,写了两篇超级超级烧脑的的文章,全是数学公式,一堆理论。今天就轻松点,写点实用性的。关于超时任务中断的问题。以下我在工作处理超时任务中断的用到的方法。如果读者有更好的方法,或者有用到什么现成的一些框架,欢迎在评论中指出,如果文章有什么错误、不足,也希望指点一点。
任务超时取消
超时任务中断在实际项目非常常见,什么上传下载,FTP连接,还有业务类型的等等。对于这种执行了半天的都跑不完的任务,我们希望能给用户看到反馈,而不是让用户干等着!一般处理这些超时的任务的都是先中断任务,重试,重试n次,才反馈执行失败。本文就只介绍中断任务。
方法一:使用join(long millis)
在启动任务线程(taskThread)中的依赖线程(可理解为主线程),调用join(n)方法,阻塞依赖线程,等待n秒后,打断任务线程的狗腿。
public class Timeout_demo1 {
public static void main(String[] args) throws InterruptedException {
System.out.println("go..........");
// 定义一个任务(执行的时间约为10s)
Thread t1 = new Thread(()->{
task("A",10);
});
t1.start(); // 启动任务线程
// 阻塞主线程3秒,3后就会形成主线程和任务线程并行的情况
// 相当于给任务3秒钟执行时间
try {
t1.join(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 干掉任务线程,实现任务超时取消
// 如果这是任务已经结束了,那执行这句也没什么影响
t1.interrupt();
System.out.println("end");
}
// 任务的内容
private static void task(String name ,int times){
for (int i = 0; i < times; ++i) {
System.out.println("task:[" + name + "]" + (i + 1) + " round");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println("Task["+name + "] is interrupted when calculating, will stop...");
return; // 注意这里如果不return的话,线程还会继续执行,所以任务超时后在这里处理结果然后返回
}
}
}
}
方法二:通过Future类中的get(long timeout, TimeUnit unit)
调用Future类中的get(long timeout, TimeUnit unit),即依赖线程等待一段时间后,尝试去获取线程的执行结果,如果线程还未未执行完成,就会抛出异常(TimeoutException),于是便可抓住这个异常后,调用Future类中的cancel(true)方法,再打断狗腿。
public class Timeout_demo2 {
public static void main(String[] args) {
ExecutorService service = Executors.newCachedThreadPool();
// 提交一个要执行10秒钟的任务
Future<Boolean> f1 = service.submit(()->{
return task("A",10);
});
try {
// future将在2秒之后取结果
// 如果这是任务未跑完的话,会抛出TimeoutException
if (f1.get(2, TimeUnit.SECONDS)) {
System.out.println("one complete successfully");
}
} catch (InterruptedException e) {
System.out.println("future在睡着时被打断");
service.shutdownNow();
} catch (ExecutionException e) {
System.out.println("future在尝试取得任务结果时出错");
service.shutdownNow();
} catch (TimeoutException e) {
System.out.println("future时间超时");
f1.cancel(true);
// executor.shutdownNow();
// executor.shutdown();
} finally {
service.shutdownNow();
}
}
private static boolean task(String name, int time){
for (int i = 0; i < time; ++i) {
System.out.println("task:[" + name + "]" + (i + 1) + " round");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println("Task["+name + "] is interrupted when calculating, will stop...");
return false; // 注意这里如果不return的话,线程还会继续执行,所以任务超时后在这里处理结果然后返回
}
}
return true;
}
}
稍稍瞄了一下cancel()方法的源码,其内部还是调用了interrupt(),思想有点类似后面提到的守护进程
方法三:通过ExecutorService.awaitTermination(long million, TimeUnit unit)
该方法会一直等待所有的任务都结束,或者超时时间到立即返回,若所有任务都完成则返回true,否则返回false;注意:这里是所有任务哦
public class Timeout_demo3 {
public static void main(String[] args) {
ExecutorService executor = Executors.newCachedThreadPool();
// 定义两个任务
Future<?> f1 = executor.submit(() -> {
task("A1", 4);
});
Future<?> f2 = executor.submit(() -> {
task("A2", 2);
});
// 将任务放到列表里便于后面遍历
List<Future<?>> futures = new ArrayList<Future<?>>();
futures.add(f1);
futures.add(f2);
try {
// 3秒后判断是否所有任务都执行完毕
if(executor.awaitTermination(3, TimeUnit.SECONDS)){
System.out.println("task finished");
}else{
System.out.println("task time out,will terminate");
for (Future<?> f : futures) {
// 还没跑完的任务就去死吧!!
if (!f.isDone()) {
f.cancel(true);
}
}
}
}catch (InterruptedException e) {
System.out.println("executor is interrupted");
} finally {
executor.shutdown();
}
}
public static void task(String name, int time) {
for (int i = 0; i < time; ++i) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println(name
+ " is interrupted when calculating, will stop...");
return; // 注意这里如果不return的话,线程还会继续执行,所以任务超时后在这里处理结果然后返回
}
System.out.println("task[" + name + "] " + (i + 1) + " round");
}
System.out.println("task[" + name + "] finished successfully");
}
}
方法四:通过守护进程实现
在Java的线程机制有两类线程:User Thread(用户线程)、Daemon Thread(守护线程)。
Daemon的作用是为其他线程的运行提供服务,比如说GC线程。其实User Thread线程和Daemon Thread守护线程本质上来说去没啥区别的,唯一的区别之处就在虚拟机的离开:如果User Thread全部撤离,那么Daemon Thread也就没啥线程好服务的了,所以虚拟机也就退出了。(下面举个栗子)
public static void main(String[] args) throws InterruptedException {
Thread task1 = new Thread(new TaskThead("A",10));
//将进程设置为守护进程
//task1.setDaemon(true);
task1.start();
Thread.sleep(3000);
System.out.println("主线程结束");
}
-
未将任务设置为守护线程
-
将任务设置为守护线程
可以看到任务为User Thread线程时,主线程结束后,任务线程还继续运行直至结束,单当任务线程为Daemon Thread 线程,主线程结束,任务线程也跟着结束,哪怕还有任务没完成
好了介绍了完了守护进程的概念后,就可以开始介绍我们的中断任务了。要通过守护线程来中断任务,我们可以在守护进程中维护一个任务进程表,当某个任务的运行时间到了就将其拖出去砍了。代码实现如下:
先定义一个任务线程类:
public class TaskThead extends Thread{
private String name; // 线程名
private int time; // 执行时间
public TaskThead(String s, int t) {
name = s;
time = t;
}
@Override
public void run(){
for (int i = 0; i < time; ++i) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println(name + " is interrupted when calculating, will stop...");
return; // 注意这里如果不return的话,线程还会继续执行,所以任务超时后在这里处理结果然后返回
}
System.out.println("task[" + name + "] " + (i + 1) + " round");
}
System.out.println("task[" + name + "] finished successfully");
}
}
定义守护线程类
public class DaemonThread extends Thread{
/**
* 内部维护的一个任务线程表
* 第一个key是线程运行的时间
* value是个hashMap -> key是线程名,value是线程本体
* (事实上这里将hashMap换成List也不错,因为这个线程名基本没用到)
*
* 即先将所有任务线程按照超时时间分类,在按名字分类
*/
public Map<Integer ,HashMap<String,Thread>> threadMap = new HashMap<>();
// 将任务加入到守护线程的表中
public void addTask(String name,int timeout,Thread th){
HashMap<String,Thread> name2Thread = threadMap.getOrDefault(timeout,null);
if(name2Thread==null){
HashMap<String, Thread> map = new HashMap<>();
map.put(name,th);
threadMap.put(timeout,map);
}else {
name2Thread.put(name,th);
}
}
@Override
// 守护线程每个一秒钟去牢房拖人
// 拖到一个就咔嚓一个
// 直到该跑的跑完了,该咔嚓的也咔嚓完,于是守护进程也就失业了
public void run(){
int time = 0;
while (true){
try {
time += 1000;
Thread.sleep(900);
HashMap<String,Thread> name2Thread = threadMap.getOrDefault(time,null);
if(name2Thread==null){
System.out.println("key["+time+"] == null");
continue;
}else{
for (Map.Entry<String,Thread> entry :name2Thread.entrySet()) {
Thread thread = entry.getValue();
thread.interrupt();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
客户端代码
public class Client {
public static void main(String[] args) {
// 创建多个任务
Thread task1 = new Thread(new TaskThead("A",10));
Thread task2 = new Thread(new TaskThead("B",8));
Thread task3 = new Thread(new TaskThead("C",2));
Thread task4 = new Thread(new TaskThead("D",7));
Thread task5 = new Thread(new TaskThead("E",6));
Thread task6 = new Thread(new TaskThead("F",5));
DaemonThread daemon = new DaemonThread();
daemon.setDaemon(true);
// 将任务加入到守护进程中,并设置超时时间
daemon.addTask("A",5000,task1);
daemon.addTask("B",5000,task2);
daemon.addTask("C",4000,task3);
daemon.addTask("D",2000,task4);
daemon.addTask("E",3000,task5);
daemon.addTask("F",4000,task6);
task1.start();
task2.start();
task3.start();
task4.start();
task5.start();
task6.start();
daemon.start();
}
}
运行结果如上,只会有一个任务能跑完,其他均被咔嚓了。
关于这几种方法嘛!!个人比较常用的就是方法二了!因为在项目中通常都是使用线程池来异步执行任务了。使用Future类的get()方法,这种方式实现起来比较方便,代码易读性也好。
本来嘛!文章到上面就已经结束了!但是在写到这时有萌生了另一个想法,大家看上述的通过守护线程去中断是不是可以联想到生产者和消费者模型呢?是不是我们可以在创建完了一个任务后就将任务扔进队列了(生产者),消费者就不停遍历队列找到超时的任务,拖出来咔嚓掉呢??
不是时间关系,我就不去写了带代码了。有兴趣的朋友就自行去完成吧!!我就,溜了溜了..........