Future.get()在配置RejectedExecutionHandler为ThreadPoolExecutor.DiscardPolicy策略时一直阻塞
1、先复现这种情况:
1 | package com.sdcuike.java11; |
1、先复现这种情况:
1 | package com.sdcuike.java11; |
线程的状态在java中有明确的定义,在java.lang.Thread.State中有6种。
首先抛几个问题:
JDK中的ThreadExecutor中的execute方法的处理逻辑应该都知道:
1.小于等于Coresize:创建线程执行;
2.大于CoreSize:加入队列;
3.队列满且小于maxSize:有空闲线程使用空闲线程执行,没有的话,创建线程执行;如果大于maxSize则拒绝策略执行。
这样会造成一个现象,如果设置的不恰当,队列使用LinkedBlockingQueue,那么线程数量是很不可能达到maximumPoolSize,因为线程数量到了corePoolSize之后,之后新的任务是添加到队列里面去了。
Tomcat中有一个StandardThreadExecutor线程池,该线程池execute执行策略是优先扩充线程到maximumPoolSize,再offer到queue,如果满了就reject。来看看StandardThreadExecutor是怎么实现的。
线程池使用FutureTask的时候如果拒绝策略设置为了DiscardPolicy和DiscardOldestPolicy并且在被拒绝的任务的Future对象上调用无参get方法那么调用线程会一直被阻塞。
问题复现
1 | public static void main(String[] args) throws Exception { |
关于线程池的介绍在http://blog.csdn.net/zero__007/article/details/43795287 ,http://blog.csdn.net/zero__007/article/details/44102239 已经介绍了,这里只是稍微补充补充。
RejectedExecutionHandler
ThreadPoolExecutor的构造函数中会有该参数,表示拒绝策略。当队列满且线程池大小>=maximumPoolSize时会触发驳回,因为这时线程池已经不能响应新提交的任务,驳回时就会回调这个接口rejectedExecution方法,JDK默认提供了4种驳回策略,可以根据业务场景来选择,线程池的默认策略是AbortPolicy。
ThreadPoolExecutor预留了以下三个方法,我们可以通过继承该类来做一些扩展,比如监控、日志等等。
1 | protected void beforeExecute(Thread t, Runnable r) { } |
先上两段代码:
1 | ExecutorService threadPool = Executors.newFixedThreadPool(1); |
1 | ExecutorService threadPool = Executors.newFixedThreadPool(1); |
运行结果发现,execute执行的时候抛出了预期的NullPointerException异常,而submit执行时什么都没有!
当单线程应用程序中的主线程抛出一个未捕获的异常时,因为控制台中会打印堆栈跟踪(也因为程序停止),所以很可能注意到。但在多线程应用程序中,尤其是在作为服务器运行并且不与控制台相连的应用程序中,线程死亡可能成为不太引人注目的事件,这会导致局部系统失败,从而产生混乱的应用程序行为。
编写得不正确的线程池会“泄漏”线程,直到最终丢失所有线程。大多数线程池实现通过捕获抛出的异常或重新启动死亡的线程来防止这一点,但线程泄漏的问题并不仅限于线程池,使用线程来为工作队列提供服务的服务器应用程序也可能具有这种问题。当服务器应用程序丢失了一个工作线程(worker thread)时,在较长时间内应用程序仍可能显得一切正常,这使得该问题的真实原因难以确定。
许多应用程序用线程来提供后台服务:处理来自事件队列的任务、从套接字读取命令或执行UI线程以外的长期任务。当由于抛出未捕获的 RuntimeException 或 Error ,或者只是停下来,等待阻塞的 I/O 操作(原本未预计到阻塞),从而引起这些线程之一死亡时,会发生什么呢?
有时,譬如当线程执行由用户启动的长期任务(如拼写检查)时,用户会注意到任务没有进展,他们可能会异常终止操作或程序。但其它时间,后台线程执行“清理维护”任务 ,它们可能消失很长时间而不被察觉。
ThreadLocal,线程本地存储,为变量在每个线程中都创建了一个副本,那么每个线程可以独立地改变和访问自己的副本变量,而不会影响其它线程所对应的副本变量。从线程的角度看,目标变量就像是线程的本地变量,这也是类名中“Local”所要表达的意思。
ThreadLocal不是用来解决对象共享访问问题的,而是提供了保持对象的方法和避免参数传递的对象访问方式。每个线程中都有一个自己的ThreadLocalMap类对象,可以将线程自己的对象保持到其中,各管各的,线程可以正确的访问到自己的对象。将一个共用的ThreadLocal静态实例作为key,将不同对象的引用保存到不同线程的ThreadLocalMap中,然后在线程执行的各处通过这个静态ThreadLocal实例的get()方法取得自己线程保存的那个对象。
在每个线程Thread内部有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals,这个 threadLocals就是用来存储实际的变量副本的,键值为当前ThreadLocal变量,value为变量副本。初始时,在Thread里面,threadLocals为空,当通过ThreadLocal变量调用get()方法或者set()方法,就会对 Thread类中的threadLocals进行初始化,并且以当前ThreadLocal变量为键值,以ThreadLocal要保存的副本变量为 value,存到threadLocals。然后在当前线程里面,如果要使用副本变量,就可以通过get方法在threadLocals里面查找。