尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

简单聊聊线程池源码

简单聊聊线程池源码 简单聊聊线程池源码线程池是面试高频题,但大多数人只能背出核心线程、最大线程、队列、拒绝策略四个名词。与其背结论,不如自己按 JDKThreadPoolExecutor的核心执行流程手写一个迷你版MyThreadPool,一行一行对照源码,聊明白它到底在干什么。一. 线程池到底在解决什么问题一句话:避免频繁创建/销毁线程。new Thread().start()是一次系统调用,底层要分配内核资源,创建快则几十微秒、慢则毫秒级;如果任务很短,线程创建销毁的开销甚至比任务本身还大。线程池的思路是:线程复用——一批工作线程长期存活,反复执行任务;任务缓冲——任务太多、线程不够时,先排进队列;流量控制——核心数 / 最大数 / 队列 / 拒绝策略,四件套把可接受的并发度框住。MyThreadPool完整复刻了这三件事。下面按一个任务从提交到执行完的完整链路讲。二. 全景图:execute() 的三步提交任务提交只有一个入口,JDK 里的核心逻辑是著名的三步提交(对应源码的execute方法):publicvoidexecute(Runnablecommand){...intcctl.get();// 步骤1线程数 核心线程数 → 直接新建线程干活if(workerCountOf(c)corePoolSize){if(addWorker(command,true))return;cctl.get();}// 步骤2线程数够了 → 尝试把任务放队列if(isRunning(c)workQueue.offer(command)){intrecheckctl.get();// 双重检查if(!isRunning(recheck)remove(command))reject(command);elseif(workerCountOf(recheck)0)addWorker(null,false);}// 步骤3队列也满了 → 新建非核心线程还不行就拒绝elseif(!addWorker(command,false)){reject(command);}}画成流程图就是:提交任务 │ ┌─────▼─────┐ │线程数核心数?│──是──▶ 新建核心线程执行 ══▶ 结束 └─────┬─────┘ │否 ┌─────▼─────┐ │池在运行? │──是──▶ 任务入队offer └─────┬─────┘ │ │否/入队失败 ├─ 入队后状态变了→ 撤销拒绝 │ └─ 入队成功但没线程了→ 补一个兜底线程 ┌─────▼─────┐ │新建非核心线程? │──成功──▶ 执行 └─────┬─────┘ │失败 ┌─────▼─────┐ │ 拒绝策略 │ └───────────┘两个细节值得停下来看:步骤2 的双重检查:任务offer进队列之后、还没来得及返回,线程池可能被shutdown了。所以重新读一次ctl,如果状态变了,把刚入队的任务撤销并走拒绝策略;如果状态没变但workerCount 0(核心线程全部退化为 0,比如被异常弄死了),补一个空任务的线程去把队列消费掉。步骤2 的入队先于建线程:线程数够了之后,先入队,而不是先建线程——这就是队列是缓冲的体现,只有队列满才动最大线程。三. ctl:一个 int 打包状态 线程数线程池有状态和线程数两个会并发变化的东西。JDK 没有用两个volatile变量,而是用一个AtomicInteger打包:// 高 3 位存状态低 29 位存线程数privatestaticfinalintCOUNT_BITSInteger.SIZE-3;// 29privatestaticfinalintCAPACITY(1COUNT_BITS)-1;privatestaticfinalintRUNNING-1COUNT_BITS;// 负数privatestaticfinalintSHUTDOWN0;// 0privatestaticfinalintSTOP1COUNT_BITS;// 正数privatefinalAtomicIntegerctlnewAtomicInteger(ctlOf(RUNNING,0));为什么这么干?原子性:状态和线程数经常要一起改(比如线程数 1 且状态不变只是一次 CAS)。拆成两个变量就得保证跨变量的原子性,麻烦还容易出 bug;打成一个 int,一次compareAndSet同时搞定;状态比较方便:让RUNNING SHUTDOWN STOP数值递增,于是是否还在运行一行位运算就能判断:privatestaticintrunStateOf(intc){returnc~CAPACITY;}// 取高3位privatestaticintworkerCountOf(intc){returncCAPACITY;}// 取低29位privatestaticbooleanisRunning(intc){returncSHUTDOWN;}// 负数运行中迷你版只留了三个状态;JDK 还有 TIDYING、TERMINATED,用于优雅关闭。四. addWorker:带着 retry 标签的双重自旋新建线程是先抢名额、再真建线程两段式。抢名额这段,JDK 写得极其讲究,是一个双 for 循环 retry 标签的结构:privatebooleanaddWorker(RunnablefirstTask,booleancore){retry:for(;;){// 外层检查状态intcctl.get();intrsrunStateOf(c);if(rsSHUTDOWN!(rsSHUTDOWNfirstTasknull!workQueue.isEmpty())){returnfalse;// 状态闸门}for(;;){// 内层CAS 抢线程名额intwcworkerCountOf(c);if(wcCAPACITY||wc(core?corePoolSize:maximumPoolSize)){returnfalse;// 到上限了}if(ctl.compareAndSet(c,ctlOf(rs,wc1))){breakretry;// 抢到了跳出两层}cctl.get();// CAS 失败重读if(runStateOf(c)!rs){continueretry;// 状态变了回外层重新判断}}}WorkerwnewWorker(firstTask);// 抢到名额后才真正建线程...w.thread.start();returntrue;}为什么需要两层?内层 CAS抢线程数名额,并发下必然有失败,失败了要重试;重试时状态可能已经变了(比如期间被 shutdown),如果还按老状态抢,就白抢甚至抢错,所以要continue retry回外层重新读状态;retry 标签就是跳出内层循环回到外层的 goto 式跳转,break retry则是抢成功后直接跳出两层。状态闸门那行(rs SHUTDOWN !(rs SHUTDOWN firstTask null !workQueue.isEmpty()))是整个线程池最精妙的一行,它是拒绝新任务,但允许清尾:池已进入 SHUTDOWN 之后,一律不再接受带任务的线程(firstTask ! null就是收新活);唯一放行的例外:处于SHUTDOWN 且不带新任务、且队列里还有任务的补位线程——用来把 shutdown 前入队的任务清完。五. Worker 与 runWorker:线程复用工作线程不是一个任务一个线程,而是一个Worker包一个Thread,runWorker里while 循环反复拉任务:privatefinalclassWorkerimplementsRunnable{finalThreadthread;RunnablefirstTask;// 第一个任务可能为 null纯补位线程Worker(RunnablefirstTask){this.firstTaskfirstTask;this.threadthreadFactory.newThread(this);// 把自身当 Runnable 交给工厂}publicvoidrun(){runWorker(this);}}finalvoidrunWorker(Workerw){Runnabletaskw.firstTask;w.firstTasknull;while(task!null||(taskgetTask())!null){// 先干首任务再循环拉队列try{task.run();// 注意是 run()不是 start()}catch(Exceptione){e.printStackTrace();}finally{tasknull;// 清空引用进下一次循环}}workers.remove(w);// 没任务了线程退出decrementWorkerCount();}关键点:task.run()是直接调用方法,不是start()——所以这就是复用一个线程的本质:同一个Thread,通过循环把多个Runnable一个个run掉;循环退出条件 getTask()返回 null,即拿不到任务了;线程退出前把Worker从集合里移除、线程数减一——否则会死线程泄漏。六. getTask:核心线程阻塞,非核心线程超时回收getTask()决定一个线程等任务还是退出:privateRunnablegetTask(){booleantimedOutfalse;for(;;){intcctl.get();intrsrunStateOf(c);if(rsSTOP||(rsSHUTDOWNworkQueue.isEmpty())){decrementWorkerCount();// 池停了 → 线程退出returnnull;}intwcworkerCountOf(c);booleantimedwccorePoolSize;// 超过核心数的线程走超时if((wcmaximumPoolSize||(timedtimedOut))(wc1||workQueue.isEmpty())){if(ctl.compareAndSet(c,ctlOf(rs,wc-1))){returnnull;// 超时无任务 → 回收}continue;}try{Runnablertimed?workQueue.poll(keepAliveTime,TimeUnit.MILLISECONDS)// 带超时等:workQueue.take();// 一直阻塞等if(r!null)returnr;timedOuttrue;// poll 超时返回 null标记该回收了}catch(InterruptedExceptione){timedOutfalse;}}}三个设计点:核心 vs 非核心的差别就在一个timed标志:wc corePoolSize的线程用poll(keepAliveTime)等,超过存活时间没任务就返回 null → 线程退出被回收;核心线程用take()无限阻塞,所以核心线程默认不回收;timedOut标志配合keepAliveTime的两次轮询:poll超时返回 null 后不会立刻退出,而是置timedOut true,下一次循环判断timed timedOut成立才回收——这是keepAliveTime语义的精确还原;最后保留一个线程:回收时加了(wc 1 || workQueue.isEmpty())保护——哪怕该回收,也不能把最后一个线程也收掉,否则队列里的任务没人消费。七. 参数与拒绝策略构造函数七个参数,对应调优的全部旋钮:参数作用面试常问corePoolSize核心线程数,不回收核心线程会被回收吗?——不会(默认),除非allowCoreThreadTimeOut(true)maximumPoolSize最大线程数什么时候动它?——队列满之后才建非核心线程keepAliveTime非核心线程空闲存活时间回收条件就是 getTask 里的超时逻辑workQueue任务缓冲队列选型:有界 vs 无界,ArrayBlockingQueuevsLinkedBlockingQueuevsSynchronousQueuethreadFactory线程工厂一定要起名字,否则排查问题抓瞎handler拒绝策略见下JDK 自带四种拒绝策略:策略行为适用AbortPolicy(默认)直接抛RejectedExecutionException宁丢任务也不静默,适合明确失败CallerRunsPolicy让提交者自己跑天然背压,不丢任务DiscardPolicy静默丢弃允许丢DiscardOldestPolicy丢弃队头最老任务,再试提交偏向新鲜任务八. 诚实说明:迷你版和 JDK 的差距MyThreadPool复刻的是执行主路径,有意识地砍掉了一些东西:没有完整的关闭状态机(shutdown()/awaitTermination()、TIDYING/TERMINATED、中断逻辑);没有beforeExecute/afterExecute钩子(JDK 用它做监控、清理 ThreadLocal);没有对Worker加锁(JDK 的 Worker 还带锁,用于中断安全);异常处理简化成 printStackTrace(生产应该记录日志并打点)。但核心执行路径——三步提交、ctl 打包、addWorker 自旋、runWorker 复用、getTask 超时回收——与 JDK 完全一致。把这五个点讲清楚,再去啃 JDK 源码,剩下的都是增量。九. 结语线程池看似是一个池,本质是**“状态机 队列 工作线程循环”**三件事的协作:ctl用位运算把状态和线程数压进一个 int,换来原子性和可比较性;execute用三步提交实现先线程、再队列、最后扩容的渐进策略;Worker runWorker用 while 循环实现线程复用;getTask用 take/poll 区分核心与非核心线程的生命周期。把这四个点吃透,线程池的源码面试基本就过关了。完整代码见MyThreadPool.java,也可以对照 JDK 的ThreadPoolExecutor逐行读,体会 Doug Lea 的位运算功力。
返回列表