java使用默认线程池踩过的坑

网络 网络管理 网络运维
直接使用java默认的线程池调度task1和task2.由于外部txt的种种不可控原因,导致task2线程阻塞。现象就是task1和线程池调度器都正常运行着,但是task2迟迟没有动作。我们需要保证任务线程或者调度器的健壮性!

场景

一个调度器,两个调度任务,分别处理两个目录下的txt文件,某个调度任务应对某些复杂问题的时候会持续特别长的时间,甚至有一直阻塞的可能。我们需要一个manager来管理这些task,当这个task的上一次执行时间距离现在超过5个调度周期的时候,就直接停掉这个线程,然后再重启它,保证两个目标目录下没有待处理的txt文件堆积。

java使用默认线程池踩过的坑

问题

直接使用java默认的线程池调度task1和task2.由于外部txt的种种不可控原因,导致task2线程阻塞。现象就是task1和线程池调度器都正常运行着,但是task2迟迟没有动作。

当然,找到具体的阻塞原因并进行针对性解决是很重要的。但是,这种措施很可能并不能完全、彻底、全面的处理好所有未知情况。我们需要保证任务线程或者调度器的健壮性!

方案计划

线程池调度器并没有原生的针对被调度线程的业务运行状态进行监控处理的API。因为task2是阻塞在我们的业务逻辑里的,所以***的方式是写一个TaskManager,所有的任务线程在执行任务前全部到这个TaskManager这里来注册自己。这个TaskManager就负责对于每个自己管辖范围内的task进行实时全程监控!

java使用默认线程池踩过的坑

后面的重点就是如何处理超过5个执行周期的task了。

方案如下:

●一旦发现这个task线程,立即中止它,然后再次重启;

一旦发现这个task线程,直接将整个pool清空并停止,重新放入这两个task ——【task明确的情况下】;

方案实施

中止后重启

Task实现类

  1. class FileTask extends Thread { 
  2. private long lastExecTime = 0
  3. protected long interval = 10000
  4. public long getLastExecTime() { 
  5.     return lastExecTime; 
  6. public void setLastExecTime(long lastExecTime) { 
  7.     this.lastExecTime = lastExecTime; 
  8. public long getInterval() { 
  9.     return interval; 
  10. public void setInterval(long interval) { 
  11.     this.interval = interval; 
  12. }  
  13. public File[] getFiles() { 
  14.     return null; 

Override

  1. public void run() { 
  2. while (!Thread.currentThread().isInterrupted()) { 
  3. lastExecTime = System.currentTimeMillis(); 
  4. System.out.println(Thread.currentThread().getName() + " is running -> " + new Date()); 
  5. try { 
  6. Thread.sleep(getInterval() * 6 * 1000); 
  7. } catch (InterruptedException e) { 
  8. Thread.currentThread().interrupt(); 
  9. e.printStackTrace();    // 当线程池shutdown之后,这里就会抛出exception了 
  10.             } 
  11.         } 
  12.     } 
  13.     

TaskManager

  1. public class TaskManager  implements Runnable { 
  2. private final static Log logger = LogFactory.getLog(TaskManager .class); 
  3. public Set<FileTask> runners = new CopyOnWriteArraySet<FileTask>(); 
  4. ExecutorService pool = Executors.newCachedThreadPool(); 
  5. public void registerCodeRunnable(FileTask process) { 
  6. runners.add(process); 
  7. public TaskManager (Set<FileTask> runners) { 
  8. this.runners = runners; 

@Override

  1. public void run() { 
  2.        while (!Thread.currentThread().isInterrupted()) { 
  3.            try { 
  4.                long current = System.currentTimeMillis(); 
  5.                for (FileTask wrapper : runners) { 
  6.                    if (current - wrapper.getLastExecTime() > wrapper.getInterval() * 5) { 
  7.                        wrapper.interrupt(); 
  8.                        for (File file : wrapper.getFiles()) { 
  9.                            file.delete(); 
  10.                        } 
  11.                     wrapper.start();   
  12.                    } 
  13.                } 
  14.            } catch (Exception e1) { 
  15.                logger.error("Error happens when we trying to interrupt and restart a task "); 
  16.                ExceptionCollector.registerException(e1); 
  17.            } 
  18.            try { 
  19.                Thread.sleep(500); 
  20.            } catch (InterruptedException e) { 
  21.            } 
  22.        } 
  23.    } 
  24.     

这段代码会报错 java.lang.Thread IllegalThreadStateException。为什么呢?其实这是一个很基础的问题,您应该不会像我一样马虎。查看Thread.start()的注释, 有这样一段:

It is never legal to start a thread more than once. In particular, a thread may not be restarted once it has completed execution.

是的,一个线程不能够启动两次。那么它是怎么判断的呢?

  1. public synchronized void start() { 
  2.         /** 
  3.          * A zero status value corresponds to state "NEW".    0对应的是state NEW 
  4.          */ 

if (threadStatus != 0) //如果不是NEW state,就直接抛出异常!#p#

  1. throw new IllegalThreadStateException(); 
  2.         group.add(this); 
  3.         boolean started = false
  4.         try { 
  5.         start0();    // 启动线程的native方法 
  6.         started = true
  7.         } finally { 
  8.             try { 
  9.                 if (!started) { 
  10.                     group.threadStartFailed(this); 
  11.                 } 
  12.             } catch (Throwable ignore) { 
  13.             } 
  14.         } 
  15.     } 

恩,只有是NEW状态才能够调用native方法启动一个线程。好吧,到这里了,就普及也自补一下jvm里的线程状态:

所有的线程状态::

NEW —— 还没有启动过

RUNNABLE —— 正在jvm上运行着

BLOCKED —— 正在等待锁/信号量被释放

WAITING —— 等待其他某个线程的某个特定动作

TIMED_WAITING —— A thread that is waiting for another thread to perform an action for up to a specified waiting time is in this state.

TERMINATED —— 退出,停止

线程在某个时间点上只可能存在一种状态,这些状态是jvm里的,并不反映操作系统线程的状态。查一下Thread的API,没有对其状态进行修改的API。那么这条路是不通的吗?

仔细考虑一下……

如果把任务做成Runnable实现类,然后在把这个实现类丢进线程池调度器之前,利用此Runnable构造一个Thread,是不是这个Thread对象就能够控制这个runnable对象,进而控制在线程池中运行着的task了呢?非也!让我们看看Thread和ThreadPoolExecutor对Runnable的处理吧。

Thread

  1. /* What will be run. */ 
  2. private Runnable target; 

 

结合上面的start()方法,很容易猜出,start0()会把target弄成一个线程来进行运行。

ThreadPoolExecutor

  1. public void execute(Runnable command) { 
  2.         if (command == null) 
  3.             throw new NullPointerException(); 
  4.         int c = ctl.get(); 
  5.         if (workerCountOf(c) < corePoolSize) { 
  6.             if (addWorker(command, true)) 
  7.                 return; 
  8.             c = ctl.get(); 
  9.         } 
  10.         if (isRunning(c) && workQueue.offer(command)) { 
  11.             int recheck = ctl.get(); 
  12.             if (! isRunning(recheck) && remove(command)) 
  13.                 reject(command); 
  14.             else if (workerCountOf(recheck) == 0) 
  15.                 addWorker(null, false); 
  16.         } 
  17.         else if (!addWorker(command, false)) 
  18.             reject(command); 
  19. private boolean addWorker(Runnable firstTask, boolean core) { 
  20. … 
  21. boolean workerStarted = false
  22. boolean workerAdded = false
  23. Worker w = null
  24. try { 
  25. final ReentrantLock mainLock = this.mainLock; 
  26. w = new Worker(firstTask); 
  27. final Thread t = w.thread; 
  28. if (t != null) { 
  29. mainLock.lock(); 
  30. try { 
  31. int c = ctl.get(); 
  32. int rs = runStateOf(c); 
  33. if (rs < SHUTDOWN || 
  34. (rs == SHUTDOWN && firstTask == null)) { 
  35. if (t.isAlive()) // precheck that t is startable 
  36. throw new IllegalThreadStateException(); 
  37. workers.add(w); 
  38. int s = workers.size(); 
  39. if (s > largestPoolSize) 
  40. largestPoolSize = s; 
  41. workerAdded = true
  42. } finally { 
  43. mainLock.unlock(); 
  44. if (workerAdded) { 
  45. t.start(); 
  46. workerStarted = true
  47. } finally { 
  48. if (! workerStarted) 
  49. addWorkerFailed(w); 
  50. return workerStarted; 

那么Worker又是怎样的呢?

Worker

  1. private final class Worker 
  2. extends AbstractQueuedSynchronizer 
  3. implements Runnable 
  4. final Thread thread; 
  5. Runnable firstTask; 
  6. volatile long completedTasks; 
  7. Worker(Runnable firstTask) { 
  8. setState(-1); //调用runWorker之前不可以interrupt 
  9. this.firstTask = firstTask; 
  10. this.thread = getThreadFactory().newThread(this); 
  11. public void run() { 
  12. runWorker(this); 
  13. ……   
  14. ……. 
  15. void interruptIfStarted() { 
  16. Thread t; 
  17. if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) { 
  18. try { 
  19. t.interrupt(); 
  20. } catch (SecurityException ignore) { 

可见worker里既包装了Runnable对象——task,又包装了一个Thread对象——以自己作为初始化参数,因为worker也是Runnable对象。然后对外提供了运行与停止接口,run()和interruptIfStarted()。回顾上面使用Thread的例子不禁有了新的领悟,我们把一个Thread对象交给ThreadPoolExecutor执行后,实际的调用是对Thread(FileTask())对象,我们暂时称之为workerWrapper。那么我们在池外进行FileTask.interrupt()操作影响的是FileTask对象,而不是workerWrapper。所以可能上面对于start()方法二次调用不是特别适当。更恰当的应该是在fileTask.interrupt()的时候就跑出异常,因为从来没有对fileTask对象执行过start()方法,这时候去interrupt就会出现错误。具体如下图:

java使用默认线程池踩过的坑

分析到此,我们已经明确除了调用ThreadPoolExecutor了的interruptWorkers()方法别无其他途径操作这些worker了。

  1. private void interruptWorkers() { 
  2. final ReentrantLock mainLock = this.mainLock; 
  3. mainLock.lock(); 
  4. try { 
  5. for (Worker w : workers) 
  6. w.interruptIfStarted(); 
  7. } finally { 
  8. mainLock.unlock(); 

重启线程池

●TaskManager

  1. public class TaskManager  implements Runnable { 
  2. ….. 
  3. public TaskManager (Set<FileTask> runners) { 
  4. super(); 
  5. this.runners = runners; 
  6. executeTasks(runners); 
  7. private void executeTasks(Set<FileTask> runners) { 
  8. for (FileTask task : runners) { 
  9. pool.execute(task); 
  10. System.out.println(task.getClass().getSimpleName() + " has been started"); 

@Override

  1. public void run() { 
  2. while (!Thread.currentThread().isInterrupted()) { 
  3. try { 
  4. long current = System.currentTimeMillis(); 
  5. for (FileTask wrapper : runners) { 
  6. if (wrapper.getLastExecTime() != 0 && current - wrapper.getLastExecTime() > wrapper.getInterval() * 5 * 1000) {    // 开始忘了乘以1000 
  7. wrapper.interrupt(); 
  8. if (wrapper.getFiles() != null){ 
  9. for (File file : wrapper.getFiles()) { 
  10. file.delete(); 
  11. System.out.println("Going to shutdown the thread pool"); 
  12. List<Runnable> shutdownNow = pool.shutdownNow();    // 不等当前pool里的任务执行完,直接关闭线程池 
  13. for (Runnable run : shutdownNow) { 
  14. System.out.println(run + " going to be shutdown"); 
  15. while (pool.awaitTermination(1, TimeUnit.SECONDS)) {   
  16. System.out.println("The thread pool has been shutdown " + new Date()); 
  17. executeTasks(runners); // 重新执行 
  18. Thread.sleep(200); 
  19. } catch (Exception e1) { 
  20. e1.printStackTrace(); 
  21. try { 
  22. Thread.sleep(500); 
  23. } catch (InterruptedException e) { 
  24. public static void main(String[] args) { 
  25. Set<FileTask> tasks = new HashSet<FileTask>(); 
  26.         
  27. FileTask task = new FileTask(); 
  28. task.setInterval(1); 
  29. task.setName("task-1"); 
  30. tasks.add(task); 
  31.                
  32. FileTask task1 = new FileTask(); 
  33. task1.setInterval(2); 
  34. task.setName("task-2"); 
  35. tasks.add(task1); 
  36.         
  37. TaskManager  codeManager = new TaskManager (tasks); 
  38. new Thread(codeManager).start(); 
  39. }    

成功!把整个的ThreadPoolExector里所有的worker全部停止,之后再向其队列里重新加入要执行的两个task(注意这里并没有清空,只是停止而已)。这样做虽然能够及时处理task,但是一个很致命的缺点在于,如果不能明确的知道ThreadPoolExecutor要执行的task,就没有办法重新执行这些任务。#p#

定制线程池

好吧!停止钻研别人的东西!我们完全可以自己写一个自己的ThreadPoolExecutor,只要把worker暴露出来就可以了。这里是不是回想起前面的start问题来了,没错,我们即便能够直接针对Thread进行interrupt, 但是不能再次start它了。那么clone一个同样的Thread行不行呢?#p#

Thread

  1. @Override 
  2. protected Object clone() throws CloneNotSupportedException { 
  3. throw new CloneNotSupportedException(); 

 

答案显而易见,线程是不支持clone 的。我们需要重新new 一个Thread来重新运行。其实我们只需要将原来的Worker里的Runnable换成我们自己的task,然后将访问权限适当放开就可以了。还有,就是让我们的CustomThreadPoolExecutor继承Thread,因为它需要定时监控自己的所有的worker里Thread的运行状态。

CustomThreadPoolExecutor

  1. public class CustomThreadPoolExecutor extends ThreadPoolExecutor implements Runnable {  
  2. public void execute(Testask command) { 
  3. ….//将执行接口改为接收我们的业务类 
  4. … 
  5. … 
  6. private final class Worker 
  7. extends AbstractQueuedSynchronizer 
  8. implements Runnable 
  9. … 
  10. Testask firstTask; //将Runnable改为我们的业务类,方便查看状态 
  11. … 
  12. Worker(Testask firstTask) { 
  13. …//同样将初始化参数改为我们的业务类 
  14.  
  15. public static void main(String[] args) { 
  16. CustomThreadPoolExecutor pool = new CustomThreadPoolExecutor(0, Integer.MAX_VALUE, 
  17. 60L, TimeUnit.SECONDS, 
  18. new SynchronousQueue<Runnable>()); 
  19.          
  20. Testask task = new Testask(); 
  21. task.setInterval(1); 
  22. pool.execute(task); 
  23.          
  24. Testask task1 = new Testask(); 
  25. task1.setInterval(2); 
  26. pool.execute(task1); 
  27.          
  28. new Thread(pool).start(); 
  29.  
  30. @Override 
  31. public void run() { 
  32. while (!Thread.currentThread().isInterrupted()) { 
  33. try { 
  34. long current = System.currentTimeMillis(); 
  35. Set<Testask> toReExecute = new HashSet<Testask>(); 
  36. System.out.println("\t number is " + number); 
  37. for (Worker wrapper : workers) { 
  38. Testask tt = wrapper.firstTask; 
  39. if (tt != null) { 
  40. if (current - tt.getLastExecTime() > tt.getInterval() * 5 * 1000) { 
  41. wrapper.interruptIfStarted(); 
  42. remove(tt); 
  43. if (tt.getFiles() != null) { 
  44. for (File file : tt.getFiles()) { 
  45. file.delete(); 
  46. System.out.println("THread is timeout : " + tt + " " + new Date()); 
  47. toReExecute.add(tt); 
  48. if (toReExecute.size() > 0) { 
  49. mainLock.lock(); 
  50. try { 
  51. for (Testask tt : toReExecute) { 
  52. execute(tt);    // execute this task again 
  53. }  
  54. } finally { 
  55. mainLock.unlock(); 
  56. } catch (Exception e1) { 
  57. System.out.println("Error happens when we trying to interrupt and restart a code task "); 
  58. try { 
  59. Thread.sleep(500); 
  60. } catch (InterruptedException e) { 

Testask

  1. class Testask implements Runnable { 
  2. ….. 
  3.  
  4. @Override 
  5. public void run() { 
  6. while (!Thread.currentThread().isInterrupted()) { 
  7. lastExecTime = System.currentTimeMillis(); 
  8. System.out.println(Thread.currentThread().getName() + " is running -> " + new Date()); 
  9. try { 
  10. CustomThreadPoolExecutor.number++; 
  11. Thread.sleep(getInterval() * 6 * 1000); 
  12.                 System.out.println(Thread.currentThread().getName() + " after sleep"); 
  13. } catch (InterruptedException e) { 
  14. Thread.currentThread().interrupt(); 
  15. System.out.println("InterruptedException happens"); 
  16. System.out.println("Going to die"); 

最终方案

综上,最稳妥的就是使用JDK自带的ThreadPoolExecutor, 如果需要对池里的task进行任意时间的控制,可以考虑全面更新,全方面,360度无死角的定制自己的线程池当然是***的方案,但是一定要注意对于共享对象的处理,适当的处理好并发访问共享对象的方法。

 

鉴于我们的场景,由于时间紧,而且需要了解的task并不多,暂时选用全部重新更新的策略。上线后,抽时间把自己定制的ThreadPoolExecutor搞定,然后更新上去!

 

责任编辑:守望幸福 来源: 51CTO.com
相关推荐

2017-07-17 15:46:20

Oracle并行机制

2024-02-04 08:26:38

线程池参数内存

2024-04-01 08:05:27

Go开发Java

2018-01-10 13:40:03

数据库MySQL表设计

2019-10-30 14:44:41

Prometheus开源监控系统

2023-03-13 13:36:00

Go扩容切片

2018-09-11 09:14:52

面试公司缺点

2020-11-03 13:50:31

Redis缓存数据库

2018-01-10 06:17:24

2022-04-26 21:49:55

Spring事务数据库

2024-04-10 08:39:56

BigDecimal浮点数二进制

2019-12-12 14:32:26

SQL语句数据库

2021-09-11 15:26:23

Java多线程线程池

2019-02-19 09:46:58

美图容器化Kubernetes

2012-02-29 13:26:20

Java

2022-07-06 11:47:27

JAVAfor循环

2022-08-16 08:27:20

线程毁线程异步

2018-08-01 14:45:16

PHP编程语言

2017-10-16 09:56:16

2019-09-25 15:30:15

点赞
收藏

51CTO技术栈公众号