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

面试官:线程池中的冗余线程是如何回收的?

时间:2023-04-02 09:56:02 Java

作者:kingsleylam\链接:https://cnblogs.com/kingsleyl...最近看了JDK线程池ThreadPoolExecutor的源码,对线程池中执行任务的过程有了一个大概的了解。其实这个过程也很通俗易懂,我就不赘述了,别人写的比我好多了。不过我比较感兴趣的是线程池是如何回收工作线程的,所以简单分析一下,加深对线程池的理解。下面以JDK1.8为例进行分析1、runWorker(Workerw)工作线程启动后,进入runWorker(Workerw)方法。里面是一个while循环,循环判断任务是否为空,如果不是,则执行任务;如果获取不到任务,或者出现异常,退出循环,执行processWorkerExit(w,completedAbruptly);在这个方法中,工作线程被移走了。有两个获取任务的来源。一个是firstTask,也就是工作线程第一次运行时执行的任务。最多只能执行一次,后面必须从getTask()方法中获取任务。看来getTask()是关键。在不考虑异常的场景下,返回null就意味着退出循环,结束线程。接下来我们要看看,在什么情况下getTask()会返回null。(限于篇幅,分段截取,省略中间执行任务的步骤)2.getTask()返回null。有两种情况会返回null,见红框。第一种情况,线程池的状态已经是STOP、TIDYING、TERMINATED或SHUTDOWN,工作队列为空;第二种情况,工作线程数大于最大线程数或当前工作线程超时,其他工作线程或任务队列为空。这个比较难理解,总之先记住,以后再用。下面用条件1和条件2分别指代两种情况的判断条件。3、分场景分析线程池回收工作线程3.1Shutdown()没有被调用,所有任务都在RUNNING状态执行,无需回收)。比如一个线程池,核心线程数为4,最大线程数为8。一开始有4个工作线程。当任务填满任务队列时,你不得不将工作线程增加到8个,当后面的任务快执行完,线程拿不到任务时,就会回收到4个工作线程的状态。(根据allowCoreThreadTimeOut的值,这里讨论默认值为false的情况,即核心线程不会超时,如果为true,工作线程可以全部销毁)。可以先排除上述条件1,线程池状态已经是STOP、TIDYING、TERMINATED或SHUTDOWN,工作队列为空。因为线程池一直在RUNNING,所以这个判断一直是false。在这种情况下,可以假设条件1不存在。下面分析当任务不能取出时线程是如何运行的。步骤1。从任务队列中取任务有两种方式,超时等待还是可以永远阻塞。决定因素是时间变量。变量被赋值之前。如果当前线程数大于核心线程数,则变量timed为true,否则为false(上面说了,这里只讨论allowCoreThreadTimeOut为false的情况)。显然,现在讨论的是timed为true的情况。keepAliveTime一般不设置,默认值为0,所以基本上可以认为是非阻塞的,取任务的结果会立即返回。线程等待唤醒超时后,发现无法取出任务,timeOut变为真,进入下一个循环。第2步。来到条件1的判断,线程池一直在RUNNING,没有进入代码块。第三步。来到条件2的判断,此时任务队列为空,条件为真。CAS减少了线程数。成功则返回null,否则重复step1。这里需要注意的是,多个线程同时通过条件2的判断是有可能的。是否会减少线程数而不是预期的核心线程数?比如当前线程数只有5个,此时两个线程同时被唤醒。经过条件2的判断,同时减少数量,剩下的线程数只有3个,与预期不符。其实没有。为了防止这种情况,compareAndDecrementWorkerCount(c)使用了CAS方法。如果CAS失败,则继续,进入下一轮循环,重新判断。像上面的例子,其中一个线程会CAS失败,然后重新进入循环,发现工作线程数只有4个,timed为false,这个线程不会被销毁,可以永远阻塞(workQueue.take())。在得出答案之前,我已经考虑了很长时间。我一直在思考如何保证核心线程数可以在没有任何锁的情况下被回收。原来是CAS的奥秘。从这里也可以看出,虽然有核心线程,但是线程不区分是核心还是非核心。先不创建核心,超过核心线程数后创建非核心。最终保留哪些线程是完全随机的。.3.2当shutdown()被调用时,所有任务都被执行。在这种场景下,不管是核心线程还是非核心线程,所有工作线程都会被销毁。调用shutdown()后,向所有空闲工作线程发送中断信号。最后传入false,调用下面的方法。可以看出,在发送中断信号之前,会先判断是否已经被中断,并获得工作线程的独占锁。当发出中断信号时,工作线程要么准备获取getTask()中的任务,要么正在执行任务,所以要等到当前任务执行完才会发出,因为工作线程还会添加工作线程正在执行任务时的任务。锁。工作线程执行完任务后,再次进入getTask()。所以我们只需要看看在getTask()中如何处理中断异常就可以了。getTask()中的工作线程有两种可能。3.2.1任务全部完成,线程阻塞等待。很简单,中断信号将其唤醒,进入下一个循环。当达到条件1时,若条件满足,则减少工作线程数,返回null,外层结束本线程。这里的decrementWorkerCount()是自旋型的,肯定会减1。3.2.2任务还没有执行完调用shutdown()后,必须执行完未完成的任务,pool才能结束。所以有可能此时线程还在工作。分两个stage来讨论stage1,task比较多,workerthreads可以拿到tasks。这不涉及线程退出。可以跳过,只分析线程收到中断信号后的性能。假设有线程A,正在通过getTask()获取任务。此时A被中断,获取任务时,无论是poll()还是take(),都会抛出中断异常。异常被捕获,重新进入下一个循环。只要队列不为空,就可以继续取任务。线程A中断了,重新去取任务,调用workQueue.poll()或者workQueue.take(),不会抛异常吗?任务能正常取回吗?这取决于workQueue的实现。workQueue是BlockingQueue类型。以常见的LinkedBlockingQueue和ArrayBlockingQueue为例,加锁时会调用lockInterruptibly(),响应中断。该方法调用AQS的acquireInterruptibly(intarg)。acquireInterruptibly(intarg),不管是在入口处判断中断异常,还是阻塞在parkAndCheckInterrupt()方法中,被中断唤醒判断中断异常,都会用到Thread.interrupted()。该方法会返回线程的中断状态,并重置中断状态!也就是说,线程不再处于中断状态,这样再次取任务时,就不会报错了。因此,这相当于为准备取任务的线程浪费了一个周期。这可能是线程中断的副作用。当然,不影响整体运行。分析到这里,不禁感叹,BlockingQueue只是在这里重置了中断状态,这怎么想出这么奇妙的设计呢?DougLeaOrz大师。2期任务马上就要执行了,任务快完成了。比如有4个工作线程,只剩下2个任务,那么可能有2个线程获取任务,2个线程阻塞。因为获取任务之前的判断没有加锁,会不会出现所有线程都通过了前面的校验,来到workQueue获取任务的地方,出现任务队列为空,所有线程都阻塞的情况呢?因为已经执行了shutdown(),不能再给线程发送中断信号,所以线程已经阻塞,无法回收。这不会发生。假设有A、B、C、D四个工作线程,同时通过条件1和条件2的判断,来到取任务的地方。那么,工作队列中至少有一个任务,至少有一个线程可以拿到这个任务。假设A和B得到任务,C和D被阻塞。A和B接下来的步骤是:步骤1.任务执行后,再次getTask()。此时满足条件1,返回null,线程准备好被回收。step2.processWorkerExit(Workerw,booleancompletedAbruptly)回收线程。回收就跟杀线程一样简单吗?让我们看一下processWorkerExit(Workerw,booleancompletedAbruptly)方法。如您所见,除了workers.remove(w)删除该行之外,还调用了tryTerminate()。第一个判断条件不满足任何子条件,跳过。第二种情况是工作线程还存在,那么随机中断一个空闲线程。那么问题来了,打断一个空闲线程并不等于打断正在阻塞的线程。如果A和B同时退出,有没有可能是A打断B,B打断A,AB互相打断,这样就没有线程去打断唤醒被阻塞的线程了?答案还是,想多了。。。假设A能到这里,说明A已经从工作线程集合workers中移除(processWorkerExit(Workerw,booleancompletedAbruptly)hasbeenremovedbeforetryTerminate())。然后A打断B,B来这里打断,A就不会在worker中被发现。也就是说,退出的线程不能互相中断。我退出收藏后,我打断你,你不能打断我,因为我已经退出收藏了,你只能打断别人。那么,即使N个线程同时退出,至少到最后,也会有一个线程打断剩下的阻塞线程。像多米诺骨牌一样,中断信号将被传播。被阻塞的C和D中的任意一个被中断唤醒后,会重复step1的动作,周而复始的循环,直到所有被阻塞的线程都被中断唤醒。这就是为什么在tryTerminate()中,如果传入false,只需要中断任何空闲线程即可。想到这里,我又一次对DougLea(粤语)产生了敬佩之情。它也经过精心设计。4.总结ThreadPoolExecutor回收工作线程,如果一个线程getTask()返回null,它就会被回收。有两种情况。1、没有调用shutdown(),所有任务都在RUNNING状态下执行的场景线程数大于corePoolSize,线程超时阻塞。超时唤醒后,CAS减少工作线程数。如果CAS成功,则返回null并回收线程。否则,进入下一个循环。当工作线程数小于等于corePoolSize时,可以一直阻塞。2、调用shutdown(),shutdown()会在所有任务执行完毕后向所有线程发送中断信号。这个时候,有两种可能。2.1)所有线程被阻塞中断唤醒,进入循环,全部满足第一个if判断条件,返回null,所有线程被回收。2.2)如果任务还没有执行完,至少会回收一个线程。在processWorkerExit(Workerw,booleancompletedAbruptly)方法中,调用tryTerminate()向任何空闲线程发送中断信号。所有阻塞的线程最终都会被一个一个唤醒并回收。近期热点文章推荐:1.1,000+Java面试题及答案(2021最新版)2.别在满屏的if/else中,试试策略模式,真的很好吃!!3.操!Java中xx≠null的新语法是什么?4、SpringBoot2.6正式发布,一大波新特性。.5.《Java开发手册(嵩山版)》最新发布,赶快下载吧!感觉不错,别忘了点赞+转发!