当前位置: 首页 > 后端技术 > Java

线程池的一个bug,注意!!

时间:2023-04-01 19:35:50 Java

来源:https://segmentfault.com/a/11...问题描述前几天在帮同事排查生产线上偶尔出现的线程池错误。逻辑很简单。线程池执行异步任务。但是最近偶尔会报错:java.util.concurrent.RejectedExecutionException:Taskjava.util.concurrent.FutureTask@a5acd19rejectedfromjava.util.concurrent.ThreadPoolExecutor@30890a38[Terminated,poolsize=0,activethreads=0,queuedtasks=0,completedtasks=0】本文模拟代码已经在HotSpotjava8(1.8.0_221)版本下模拟&以下是模拟代码,通过Executors.newSingleThreadExecutor创建单线程线程池,然后在调用者处获取Future的结果for(inti=0;i<8;i++){newThread(newRunnable(){@Overridepublicvoidrun(){while(true){Futurefuture=threadPoolTest.submit();try{Strings=future.get();}catch(InterruptedExceptione){e.printStackTrace();}catch(ExecutionExceptione){e.printStackTrace();}catch(Errore){e.printStackTrace();}}}})。开始();}//子线程保持gc,模拟偶尔gcnewThread(newRunnable(){@Overridepublicvoidrun(){while(true){System.gc();}}}).start();}/***异步执行任务*@return*/publicFuturesubmit(){//重点,通过Executors.newSingleThreadExecutor创建单线程线程池ExecutorServiceexecutorService=Executors.newSingleThreadExecutor();FutureTaskfutureTask=newFutureTask(newCallable(){@OverridepublicObjectcall()throwsException{Thread.sleep(50);returnSystem.currentTi我米利斯()+“”;}});executorService.execute(futureTask);返回未来任务;}}Analysis&Questions首先要思考的问题是:线程池为什么要关闭?代码中没有手动关闭的地方。Executors.newSingleThreadExecotor的源码实现:publicstaticExecutorServicenewSingleThreadExecutor(){returnnewFinalizableDelegatedExecutorService(newThreadPoolExecutor(1,1,0L,TimeUnit.MILLISECONDS,newLinkedBlockingQueueExecutor())));}这里创建的是实际上是一个FinalizableDelegatedExecutor,这个包装类重写了finalize函数,也就是说这个类在被GC回收前会执行线程池的shutdown方法。问题在于GC只会回收无法访问的对象。在提交函数的栈帧执行弹出之前,executorService应该是可达的。更多多线程系列教程:https://www.javastack.cn/cate...对于这个问题,先抛出结论:当对象还存在于作用域(栈帧)中时,finalize也可能会执行oracle在jdk文档中对finalize有一个介绍:可访问对象是任何可以从任何活动线程在任何潜在的持续计算中访问的对象。可以设计程序的优化转换,将可达对象的数量减少到比天真地认为可达的对象数量少的数量。例如,Java编译器或代码生成器可能会选择将不再使用的变量或参数设置为null,以使此类对象的存储可能更快地被回收。是:可达对象是任何活动线程可以从任何潜在延续访问的任何对象;java编译器或者代码生成器可能会预先将不可访问的对象预空,让对象可以访问提前回收是指在jvm的优化下,可能会出现对象不可达然后清空回收提前。我们举个例子来验证一下。摘自:https://stackoverflow.com/que...classA{@Overrideprotectedvoidfinalize(){System.out.println(this+"wasfinalized!");}publicstaticvoidmain(String[]args)throwsInterruptedException{Aa=newA();System.out.println("创建"+a);for(inti=0;i<1_000_000_000;i++){如果(i%1_000_00==0)System.gc();}System.out.println("完成");}}//打印结果CreatedA@1be6f5c3A@1be6f5c3wasfinalized!//finalize方法输出done。从例子中可以看出,如果a在循环中完成后不再使用,则先执行finalize;虽然从对象的作用域来看,方法还没有执行,栈帧也没有出栈,但是还是会提前执行现在我们添加一行代码,在最后一行打印对象a,让编译器/代码生成器认为存在对对象a的引用...System.out.println(a);//打印结果创建了A@1be6f5c3done.A@1be6f5c3从结果来看,并没有执行finalize方法(因为执行完main方法后流程直接结束),不会出现earlyfinalize的问题。对象a被设置为null,最后print保留对象a的引用a=newA();System.out.println("Created"+a);a=null;//手动设置nullfor(inti=0;i<1_000_000_000;i++){if(i%1_000_00==0)System.gc();}System.out.println("done.");System.out.println(a);//打印结果CreatedA@1be6f5c3A@1be6f5c3wasfinalized!done.null从结果来看,手动设置null也会导致对象被提前回收。虽然最后还有引用,但是此时引用也为null。现在回到上面线程关于池的问题,根据上面介绍的机制,分析没有引用后,会提前finalize对象。上面的代码中,明明在return之前有引用了executorService.execute(futureTask),为什么也提前finalize了呢?猜测可能是在execute方法中,会调用threadPoolExecutor,创建并启动一个新的线程。这时候会发生主动线程切换,导致对象在主动线程中不可达。结合上面OracleJdk文档中的描述“可达对象(reachableobject)是可以从任何活动线程的任何潜在延续访问的任何对象”。可以认为是由于显式的线程切换导致对象被认为不可达,导致线程池提前终结。验证猜想://入口函数publicclassFinalizedTest{publicstaticvoidmain(String[]args){finalFinalizedTestfinalizedTest=newFinalizedTest();for(inti=0;i<8;i++){newThread(newRunnable(){@Overridepublicvoidrun(){while(true){TFutureTaskfuture=finalizedTest.submit();}}}).start();}newThread(newRunnable(){@Overridepublicvoidrun(){while(true){System.gc();}}}).start();}publicTFutureTasksubmit(){TExecutorServiceTExecutorService=Executors.create();TExecutorService.execute();返回空值;}}//Executors.java,模拟juc的ExecutorspublicclassExecutors{/***模拟Executors.createSingleExecutor*@return*/publicstaticTExecutorServicecreate(){returnnewFinalizableDelegatedTExecutorService(新的TThreadPoolExecutor());}staticclassFinalizableDelegatedTExecutorServiceextendsDelegatedTExecutorService{FinalizableDelegatedTExecutorService(TExecutorServiceexecutor){super(executor);}/***分析构造函数中执行shutdown,修改线程池状态*@throwsThrowable*/@Overrideprotectedvoidfinalize()throwsThrowable{super.shutdown();}}staticclassDelegatedTExecutorServiceextendsTExecutorService{protectedTExecutorServicee;publicDelegatedTExecutorService(TExecutorServiceexecutor){this.e=executor;}@Overridepublicvoidexecute(){e.execute();}@Overridepublicvoidshutdown(){e.shutdown();}}}//TThreadPoolExecutor.java,模拟juc的ThreadPoolExecutorpublicclassTThreadPoolExecutorextendsTExecutorService{/***线程池状态,false:未关闭,true是关闭的*/privateAtomicBooleanctl=newAtomicBoolean();@Overridepublicvoidexecute(){//启动一个新线程,模拟ThreadPoolExecutor.executenewThread(newRunnable(){@Overridepublicvoidrun(){}}).start();//模拟ThreadPoolExecutor,启动新线程后,循环查看线程池的状态,验证是否会在finalize中关闭//如果线程池提前关闭,则抛出异常(inti=0;i<1_000_000;i++){if(ctl.get()){thrownewRuntimeException("reject!!!["+ctl.get()+"]");}}}@Overridepublicvoidshutdown(){ctl.compareAndSet(false,true);}}执行一定时间后,报错:Exceptioninthread"Thread-1"java.lang.RuntimeException:reject!!![true]从报错的角度来看,"threadpool"也是阻塞了Shutdown提前,一定是新线程导致的吧?接下来将新线程改为Thread.sleep并测试://TThreadPoolExecutor.java,修改后的execute方法publicvoidexecute(){try{//显式sleep1ns,主动切换线程TimeUnit.NANOSECONDS.sleep(1);}catch(InterruptedExceptione){e.printStackTrace();}//模拟ThreadPoolExecutor,启动一个新的线程后,循环检查线程池的状态,验证是否会在finalize中关闭//如果线程池提前关闭,则抛出异常(inti=0;i<1_000_000;i++){if(ctl.get()){thrownewRuntimeException("reject!!!["+ctl.get()+"]");}}}execute结果是同样的错误Exceptioninthread"Thread-3"java.lang.RuntimeException:reject!!![true]由此可见,如果在执行过程中发生显式的线程切换,编译器将/代码生成器认为是外包装对象不可达提前设置null,或者线程切换导致“对象提前不可达”的情况,所以如果要在finalize方法中做点什么,必须在最后引用对象(toString/hashcode可以)以保持对象可达Reachable。官方文档不支持线程切换导致的对象不可达。这只是个人测试结果。如果您有任何问题,请指出。综上所述,这种回收机制不是JDK的bug,而是一种优化。策略是提前回收;但是在Executors.newSingleThreadExecutor的实现中,通过finalize自动关闭线程池的方法存在bug。优化后可能会导致线程池提前关闭,导致异常。这个线程池问题也是JDK论坛中一个开放但未解决的问题:https://bugs.openjdk.java.net...。不过在JDK11下,这个问题已经得到修复:JUCExecutors.FinalizableDelegatedExecutorServicepublicvoidexecute(Runnablecommand){}最后{reachabilityFence(this);}}近期热点文章推荐:1.1000+Java面试题及答案(2022最新版)2.厉害了!Java协程来了。..3.SpringBoot2.x教程,太全面了!4.不要用爆破爆满画面,试试装饰者模式,这才是优雅的方式!!5.《Java开发手册(嵩山版)》最新发布,赶快下载吧!感觉不错,别忘了点赞+转发!