1. 先搞清楚进程和线程各自是什么1.1 进程程序运行时的独立容器“进程和线程的区别”这个问题我一年能碰见几十次。面试里问工作群里问甚至有一次线上服务挂掉同事第一句话就是“那是进程挂了还是线程卡了”。想真正回答清楚光背那一句“进程是资源分配的基本单位线程是CPU调度的基本单位”远远不够。你得知道进程里到底发生了什么事线程和线程之间怎么协作出了问题之后怎么顺着进程找到线程、再顺着线程定位到代码。一台计算机上跑着的每个程序并不是直接撒在CPU上跑的。操作系统会先加载可执行文件给程序分配一块独立的虚拟地址空间准备好代码段、数据段、堆、栈再维护一张文件描述符表把这个正在运行的程序以及它手头占用的所有资源包成一个整体。这个整体就是进程。在进程的视角里整个世界是独占的它有自己的一套内存地址有自己的当前工作目录有自己的环境变量还能打开属于自己的文件。两个进程默认互不干扰A进程写坏内存B进程甚至都不知道发生了什么。这里要补一个操作系统基础概念进程不是一直都是“在运行”的状态。它会在新建、就绪、运行、阻塞、终止这些状态之间切换。比如一个进程发起磁盘读取磁盘还没返回数据时进程会进入阻塞状态让出CPU等数据到了又被唤醒进入就绪队列。很多教科书里让学生写“进程调度算法的模拟”其实就是模拟就绪队列里这些状态怎么切换。明白了状态机你再看任务管理器里的CPU占用就不会觉得数字是玄学。在Windows任务管理器或者Linux的ps -ef命令里每一个PID就对应一个进程。有一个很常见的误解看到Chrome在任务管理器里出现十几个条目就觉得“太多了要清理”。那不是同一个程序出现了十几个进程而是Chrome用了多进程隔离一个主进程加多个渲染进程、GPU进程。任务管理器里的“结束进程”也不是“卸载”它只是把当前这个运行实例终止掉。我有一次看到同事因为“进程太多”手动结束了一个文档进程结果未保存的内容全丢了所以大家在动进程之前先搞清楚它是不是系统关键进程。1.2 线程容器里真正干活的执行流进程把资源准备好了但真正让代码跑起来的是线程。一个进程启动之后默认会有一个主线程它从程序的入口函数开始执行。主线程跑到某个位置可以再创建更多线程让它们处理网络请求、批量计算、日志写入等任务。线程和进程最大的不同是同一进程里的所有线程共享这一份地址空间、全局变量、堆内存和打开的文件描述符。它们各自独立的只有调用栈、栈指针、程序计数器和一部分寄存器上下文。换句话说线程手里不带着“大件行李”它是一个很轻的执行单元。拿Java举例子一个JVM进程里通常并排跑着几十个线程GC线程、编译器线程、业务线程、定时任务线程、RMI通信线程。你用jstack去转一个Java进程能看到每个线程都有自己的名字、状态和栈信息。线程状态和进程状态类似但更细Java线程有NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED这几种。很多线上问题排查到最后都是“进程活着但某个线程卡在WAITING状态导致请求超时”。下次再遇到“Java进程没画面”这种说法你可以先怀疑是不是业务线程阻塞了。线程不是一个筐想装多少就装多少。创建线程要分配栈空间一般几MB的虚拟内存还要有内核线程控制块参与调度。线程越多上下文切换越频繁CPU大量时间花在保存和恢复现场上实际干活的比例反而下降。这也是为什么后面要聊线程池和虚拟线程。1.3 用“工厂班组”的比喻把两者串起来我习惯把进程比作一个车间。一个车间拥有自己的厂房、设备、原材料库房和产品清单这对应进程的地址空间、文件描述符和各类资源。车间里干活的人就是线程。同一个车间里的工人可以共享设备、共用材料工具随手就能传给旁边的人不用绕路打报告。可如果换了车间A车间的工人想把零件递给B车间那就得通过厂区的物流通道还得包装、登记、传输这个物流通道就是“进程间通信IPC”。这个比喻最直接的好处是把“资源分配”和“执行任务”拆开了。进程是仓库线程是搬运工仓库着火整个车间停产某个工人犯了错可能只是他手头的活儿出问题仓库还在运转其他工人还能继续。放到真实系统里进程崩溃操作系统会把这个进程占用的大部分资源回收其他进程基本不受影响线程非法越界或者抛出一个未处理异常有可能直接把整个进程拖垮。所以“线程比进程更轻”不等于“线程比进程更安全”反而因为共享资源线程之间的保护和隔离需要程序员自己设计。2. 进程和线程的核心区别一张表根本说不完2.1 资源归属谁拥有什么谁共享什么先用这张表把主要差异理清楚后面再逐条解释。面试时能把这几个维度讲透比背一百个名词都有用。维度进程线程地址空间每个进程独立同一进程的线程共享资源拥有完整的资源集合文件、信号、环境变量等共享进程资源只保留栈和寄存器上下文调度单位内核调度线程进程是资源容器线程是CPU调度的基本单位切换开销切换地址空间、刷新页表和TLB代价大同一进程内切换只需保存线程上下文通信方式只能靠管道、消息队列、共享内存、Socket等IPC直接读写共享变量配合锁和原子操作隔离性进程间相互隔离崩溃影响范围小线程出错可能影响整个进程命名空间那里进程和线程拥有的东西就差很远。一个进程维护着自己的页表它看到的每个虚拟地址都对应到物理内存的某个页。切换到另一个进程CPU要把页表基地址换掉把TLB快表里的映射清掉之前辛辛苦苦建立的内存缓存热点全没意义了。同一进程的线程之间切换地址空间没变页表也不用换自然快得多。很多人忽略的一点是进程拥有的不止是内存还有文件句柄。父进程fork子进程时子进程会继承父进程打开的文件描述符这是进程间共享资源的一种方式。但线程更直接它们连“哪个文件描述符属于谁”都不区分大家共享同一个文件描述符表。如果两个线程同时往同一个文件里写数据很可能互相覆盖所以多线程写文件也得加锁。2.2 调度和切换开销为什么线程切换比进程切换便宜这么多操作系统调度的直接对象是线程但进程内部有线程所以调度器也会考虑进程的概念。Linux的CFS调度器维护的运行队列里放的是可运行的线程不是进程。那为什么大家常说“进程调度算法”因为早期的系统确实以进程为单位调度而且很多模拟实验里简化了模型。现代系统中线程才是真正排队上CPU的那个人。线程切换到底便宜在哪假设CPU从线程A切到同一进程的线程B只需要保存A的寄存器、程序计数器、栈指针再加载B的这些状态然后继续执行。但是跨进程切换比如从进程P1的线程A切到进程P2的线程B除了要做上面那些还要切换地址空间更新页表基地址使TLB失效可能还会让CPU缓存里的数据大量失效。这个额外开销在极端高并发场景下会被放大到肉眼可见的程度。所以很多高性能服务宁可把一个进程内塞满线程也不轻易开多进程。不过要注意切换开销小是相对的。如果一台机器只有两个CPU核心你开一百个线程做计算大部分线程都在排队调度器来回切换照样能把系统拖垮。线程池存在的意义之一就是把并发线程数控制在合理范围内减少无意义的切换。2.3 通信机制共享内存最简单进程之间只能走IPC线程通信为什么简单因为它们住在一个地址空间里。线程A往全局变量写一个值线程B立刻能看到不需要任何拷贝也不需要内核帮忙。代价是这个操作本身不保证安全。如果没有锁或原子操作线程A写一半线程B可能读到一半的数据。更麻烦的是现代CPU和编译器带来的可见性问题线程A改了变量线程B不一定马上能看到这中间可能需要内存屏障。所以“共享内存 线程通信”只是表面的一句话背后要处理可见性、原子性、有序性三座大山。进程通信就完全不是这个思路。为了保证安全操作系统把各进程的地址空间隔离开进程之间不能随手读对方的变量。A进程想把数据给B进程只能走IPC管道、命名管道、消息队列、信号量、共享内存、Unix域套接字、TCP/UDP网络接口。这里面最接近线程通信效率的是共享内存两个进程通过mmap映射到同一块物理内存读写确实快但你必须自己做同步协议防止两边同时写坏数据。连“读”和“写”的顺序都要程序员操心复杂度一下就上来了。选择多进程方案时要把IPC设计当成重点。比如用Netty做多进程通信、用消息队列解耦、用共享内存做高频数据传输每种方案的吞吐量、延迟、易用性都不一样。我见过不少团队因为怕线程安全问题一律上多进程结果进程间数据同步比原来多线程加锁还痛苦。所以说“多进程更稳定”也得看场景通信成本可能是更大的坑。2.4 稳定性和隔离性线程出错容易拖垮全家进程之间隔离性好这是多进程最大的优势。一个进程因为非法内存访问崩溃操作系统只回收这这个进程的资源其他进程照常跑。Chrome愿意把每个标签页放一个进程就是希望某一个标签页崩溃后浏览器主界面和其他标签页还能活着。多进程天然适合“一个任务的故障不能影响其他任务”的场景。线程就不一样。线程之间共享地址空间某个线程往数组越界写了一段数据破坏的可能恰好是另一个线程正在使用的堆对象。更严重的是Java里一个用户线程抛出OutOfMemoryError虽然不会立刻杀死JVM但整个堆已经乱了其他线程再申请内存也会失败。C里线程写坏堆指针整个进程的内存都可能处于不可信状态崩溃只是时间问题。线上排查时经常遇到进程一直在重启翻崩溃日志才发现问题出在一个业务线程但它破坏的是全局数据结构。稳定性要求高的模块我倾向于拆成进程比如独立的worker进程、守护进程。进程崩了由supervisor自动拉起。而同一进程内多个线程更适合做细粒度的并发协作比如处理一批请求、并行计算一部分数据。隔离性和效率就像跷跷板哪个更重要直接决定你选哪条路。3. 多进程还是多线程开发选型里的真实门道3.1 计算密集和I/O密集决定了主线思路到底用多进程还是多线程最经典的衡量维度是任务类型。计算密集型任务的特点是CPU一直在干活几乎没有等待。这时候你开1000个线程并没有意义反而让调度器不停切换。更合理的做法是让并发线程数约等于CPU核心数让每个核心专注跑一个任务。Java里可以用ForkJoinPool默认并行度就是CPU核数 - 1比较适合计算密集任务。Python因为GIL的存在多线程跑纯计算任务几乎并行不了CPU密集场景直接用multiprocessing.Pool开进程池才是常见做法。I/O密集型任务刚好相反大量时间花在网络请求、磁盘读写、数据库等待上CPU其实闲着。这时开多个线程让它们在等待期间让出CPU其他线程继续跑能明显提高吞吐量。经典的经验公式是N_threads N_cpu * (1 wait_time / compute_time)如果等的时间占绝大部分线程数可以是CPU核数的很多倍。但公式只是起点真实系统还要考虑内存、连接数上限、对外部系统的压力盲目把线程数拉高最后往往是连接池先被占满。3.2 线程池和进程池为什么不能图省事直接new Thread线程不是不能直接创建但每次new Thread并start()都要走一次系统调用去创建内核线程用完还要销毁开销不小。如果请求来了就开一个线程请求结束就销毁系统在高并发下会不停地创建和销毁线程CPU大量时间都花在这上面业务响应速度反而变慢。线程池就是预先创建一批线程让它们循环处理任务任务执行完线程不销毁而是等待下一个任务。Java里最常用的是ThreadPoolExecutor。很多人直接叫它“内置线程池”其实它是一个通用实现Executors工具类可以包出四种不同配置的线程池但生产环境我更建议直接用ThreadPoolExecutor构造函数把参数显式控制在自己手里。同样地Python里的concurrent.futures.ThreadPoolExecutor和multiprocessing.Pool也是这个思路一个在线程层面复用一个在进程层面复用。进程池的好处不止复用进程还能利用多核并行。Python的典型写法是这样from multiprocessing import Pool def calc(x): return x * x if __name__ __main__: with Pool(4) as pool: results pool.map(calc, range(100))Pool(4)会创建4个子进程任务会分发给这4个进程并行执行。进程之间的通信由multiprocessing内部管理你只需要关心任务的提交和结果回收。缺点是每个任务都要序列化参数、传递结果有额外开销所以进程池适合任务本身计算量大的场景不适合高频小任务。3.3 Java线程池配置经验核心数、队列和拒绝策略怎么搭配先给一个我常用的Java线程池配置当作参考模板ThreadPoolExecutor executor new ThreadPoolExecutor( 4, 8, 60L, TimeUnit.SECONDS, new ArrayBlockingQueue(200), Thread::new, new ThreadPoolExecutor.CallerRunsPolicy() );第一参数corePoolSize4指的是线程池长期保持4个线程即使它们空闲。第二参数maximumPoolSize8指当任务队列满了之后线程池最多扩展到8个线程。第三第四个参数是空闲超过60秒的非核心线程会被回收。第五个参数我用ArrayBlockingQueue有界队列容量200这是很多教程不会细讲但特别关键的一点。为什么强调有界队列Executors默认的newFixedThreadPool使用无界LinkedBlockingQueue任务发布多少就堆积多少内存很容易被打爆。有界队列等于给系统加了一道防线队列满了之后新的任务会触发拒绝策略至少你能在入口处感知到压力而不是等OOM之后才追悔莫及。拒绝策略选CallerRunsPolicy也有讲究。它的含义是当线程池忙不过来时不丢弃任务而是让提交任务的当前线程自己去执行这个任务。这样等于把压力反向传递给调用方任务没有消失只是执行线程变了。副作用是调用方线程会阻塞在任务执行上可能拖慢请求响应。如果业务允许丢弃一些非核心任务选DiscardPolicy或直接抛异常也行但我更倾向于在关键链路用CallerRunsPolicy至少在极端情况下不会静默丢数据。线程池的阻塞队列选择总结成一句话复杂业务用有界ArrayBlockingQueue容量设多少要看任务堆积容忍度和内存如果任务一定要及时处理可以试试SynchronousQueue它不存任务直接把任务交给空闲线程没有空闲线程就创建新线程直到达到最大线程数。LinkedBlockingQueue无界版本虽然实现简单但风险太大不推荐在生产环境当默认选项。3.4 协程、虚拟线程和传统线程的关系聊到线程躲不开“虚拟线程原理”这个热点。Java 21正式发布了虚拟线程很多人把它当成超级“轻量级线程”。原理上它确实不一样传统Java线程是平台线程每个平台线程映射到一个操作系统线程线程切换由内核完成。虚拟线程则是用户态线程由JVM调度到少数几个平台线程载体线程上运行阻塞时不用占用内核线程所以可以创建成千上万个虚拟线程而不压垮操作系统。看这段Java虚拟线程的简单用法try (var executor Executors.newVirtualThreadPerTaskExecutor()) { executor.submit(() - System.out.println(Thread.currentThread().getName())); }程序里submit一个任务executor就创建一个虚拟线程去执行任务跑完虚拟线程自动结束。这就是M:N调度模型多个虚拟线程映射到少量平台线程。虚拟线程真正擅长的场景是I/O密集、高并发、大量等待的任务比如HTTP请求、数据库查询。但计算密集型任务不要指望虚拟线程帮你变快因为它最终还是要占平台线程的CPU时间。Go语言里的goroutine也是类似思路goroutine跑在用户态底层由runtime调度到少量操作系统线程上配合channel做通信写并发代码比Java传统线程轻松得多。理解了虚拟线程和goroutine就明白一个趋势现代后端越来越倾向“大量用户态执行流 少量内核线程”而不是“一个业务请求一个内核线程”。4. 线程安全和锁这里面的坑比很多人想象的深4.1 AtomicInteger线程安全先搞清楚它安全在哪“AtomicInteger线程安全吗”这个问题网上答案五花八门。准确说AtomicInteger的单个方法比如incrementAndGet、compareAndSet是线程安全的它们底层用CASCompare-And-Swap配合内存屏障实现原子更新。但如果你把多个方法组合在一起整体的复合逻辑不一定是安全的。举个例子AtomicInteger count new AtomicInteger(0); if (count.get() 0) { count.incrementAndGet(); }多个线程同时执行这段代码可能会在count.get()读到的都是0然后一起执行incrementAndGet最后结果不是1而是2。这个操作不是原子的。更好的做法是if (count.compareAndSet(0, 1)) { // 只有成功把0变成1的线程会进来 }所以判断一个并发工具安不安全不能只看某个API的原子性要看你的业务逻辑是否在同一个原子操作里完成。这就像锁锁本身保证互斥但如果你在持锁期间又去读一个没保护的变量锁就发挥不了作用。线程安全的起点是减少共享可变状态。能设计成不可变对象就设计成不可变对象能把状态收敛到单一类里就收敛起来。多线程不是“哪里加锁哪里就安全”而是“先把要保护的状态想清楚”。4.2 死锁的四个必要条件和一个真实排查案例死锁是面试里绕不开的考点也是实际写代码时很隐蔽的坑。死锁发生一般满足四个条件互斥、持有并等待、非剥夺、循环等待。互斥意味着资源同一时刻只能被一个线程占用持有并等待是线程拿着一个锁还去申请另一个锁非剥夺是锁不能被操作系统强行抢走循环等待是多个线程形成环形等待链。我第一次遇到死锁是在一个转账系统里。两个账户A和B业务逻辑需要同时给这两个账户加锁保证转账原子性。线程1先锁A再锁B线程2先锁B再锁A。当线程1拿到A、线程2拿到B时两边都在等对方手里的锁任务彻底僵住。那个时间段所有涉及这两个账户的请求全部超时数据库连接池很快被占满。排查的时候用的是jstack -l pid把线程转储打出来能看到类似“Found one Java-level deadlock”的字样以及每个线程持有哪些锁、正在等待哪些锁。那个现场信息非常直观。修复死锁的办法很简单所有线程都按账户ID排序先锁ID小的账户再锁ID大的账户循环等待就被打破了。也可以用tryLock加超时拿不到锁就释放已有锁重试或者回滚从机制上防止无限等待。死锁防范并不难难在代码规模大了之后哪些锁会形成环靠人眼很难看出来。所以项目里我会约定锁的层级关系比如“不允许在持有一个业务锁时去申请另一个业务锁”并且定期用静态检查工具或线程转储来扫描。4.3 线程中断与守护线程一个被忽视一个总被滥用Java的Thread.interrupt()并不会强制杀掉线程它是协作式的中断机制。线程在sleep()、wait()、join()期间收到中断会抛出InterruptedException并清除中断状态。如果你在代码里直接catch之后什么都不做上层就丢了“线程被要求停止”的信号。正确的做法通常是try { Thread.sleep(1000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); // 然后根据业务决定是否退出 }手动恢复中断标志让调用方还能感知到线程被打断过。线上任务要优雅退出经常用这个机制主线程调用工作线程的interrupt()工作线程在循环里检查Thread.currentThread().isInterrupted()发现中断就清理资源然后结束。这比stop()这种危险方法安全得多。守护线程也是一个容易踩坑的点。Java里setDaemon(true)表示这个线程是后台线程当JVM中只剩下守护线程时JVM会直接退出不会等待守护线程执行完。我喜欢拿它做后台状态上报、心跳检测但绝对不会拿它干“必须完成”的持久化任务因为主线程一停守护线程可能执行到一半就被终止。C#里IsBackground true也是类似的概念。线程池里的工作线程默认都是后台线程所以别以为线程池提交的任务一定会等主线程结束用了ExecutorService要明确调用shutdown()等工作。5. 高频问题排查记录从进程名改不掉到线程卡死5.1 Linux修改进程名超过15个字符会怎样Linux里有一个进程名叫comm就是ps命令默认显示的名字源码里定义的长度上限是TASK_COMM_LEN15字符。你用prctl(PR_SET_NAME, a-very-long-process-name)去设置进程名超过15个字符的部分会被截断。很多人在这里踩坑明明设置了长名字ps一看还是15个字符的短名。如果确实需要显示长进程名有两个思路。一个是用setproctitle库或手动覆盖argv[0]比如C程序可以修改自己的argv[0]并通过/proc/self/cmdline暴露出来很多监控平台看到的其实是命令行。另一个更简单改脚本或服务的可执行文件命名而不是依赖comm字段。线程名也类似pthread_setname_np同样有15字符限制。Java里Thread.setName不受这个限制但Linux JVM底层映射到线程名时可能也会截断。所以排查“进程名改了不生效”时先搞清楚问题出在comm还是cmdline。用ps -o pid,comm,cmd可以同时看到两个字段一目了然。5.2 终端进程启动失败conpty/winpty相关的处理方式在Windows上使用VS Code或其他编辑器时有时会看到错误提示“终端进程启动失败: 启动期间发生本机异常(无法启动 conpty)”。这里涉及两个概念ConPTY是Windows的伪终端接口让现代终端程序能够展现Linux风格的ANSI转义序列和交互行为winpty是早期的第三方兼容方案。错误提示里提到“已移除 winpty”说明当前环境改用了ConPTY但ConPTY没有正常启动。我遇到过的原因有几种一是Windows系统组件版本太旧ConPTY支持不完整二是终端配置被改坏默认Shell的启动命令异常三是安全软件拦截了终端进程初始化。处理顺序通常是先重启终端进程再检查默认Shell设置把“terminal.integrated.defaultProfile.windows”切换为PowerShell或CMD试试如果还不行清理终端缓存、确认为管理员身份执行或者升级操作系统补丁。这个问题的排查经验和进程线程关系不大但它本质上是进程启动阶段出了异常很多人看到“无法启动 conpty”就往网络、代理方向纠结反而忽略了Shell本身的问题。排查时先看日志里有没有具体崩溃信息再看启动命令是否生效别一上来就重装终端。5.3 后台进程异常占用msmpeng和msedgewebview2怎么处理Windows用户经常在任务管理器里看到几个疑似“系统进程”的东西比如msmpeng.exe和msedgewebview2.exe。msmpeng.exe是Windows Defender的反恶意软件服务进程CPU占用高通常是后台扫描大量文件导致的。如果电脑性能本身一般扫描时会明显卡顿。处理办法在Defender设置里把频繁读写的开发目录加入排除项或者把计划扫描时间调到空闲时段。不建议长期关掉实时保护尤其是经常下载文件的机器。msedgewebview2.exe是Edge WebView2运行时很多桌面应用靠它渲染内置网页界面比如Office、微信、各种聊天工具。看到好几个这个进程不代表中毒它们就是那些应用的后台进程实例。你没法通过“卸载”WebView2来彻底关掉它因为很多应用依赖这个组件。如果占用过高可以先确认是哪个应用在使用它任务管理器里切换到“详细信息”页签看父进程PID然后在对应应用里关闭硬件加速或后台权限。说到这里顺带提一下“误删了一个进程任务电脑黑屏怎么办”。如果你在任务管理器里结束了explorer.exe桌面、任务栏会消失屏幕上只剩壁纸。这不是系统完蛋了explorer.exe就是Windows的桌面外壳进程重新拉起来就能恢复。按CtrlShiftEsc打开新任务管理器然后“文件”菜单里“运行新任务”输入explorer.exe回车桌面就回来了。所以看到陌生进程先搜索确认别手快。5.4 进程等待、僵尸进程和线程卡死怎么定位Linux里有一个很容易被忽视的细节父进程必须调用wait()或waitpid()来回收子进程的退出状态。如果子进程已经退出但父进程一直没回收这个子进程就会变成僵尸进程出现在ps输出里状态是Z。僵尸进程不占用CPU但会占用进程表项积累多了会导致系统无法创建新进程。要清理只能让父进程调用wait或者杀掉父进程由init进程接管回收。这是我做进程管理时常说的你的service脚本里如果负责拉起子进程一定要处理子进程退出。否则子进程莫名其妙死了父进程还活着你只看到端口没有监听却不知道子进程的退出码是多少。写多进程守护逻辑时父进程循环里waitpid(-1, status, WNOHANG)是个常见套路。线程卡死的定位又不一样。如果是Java服务用jstack -l pid抓线程转储看哪些线程处于WAITING或BLOCKED状态再看它们持有哪些锁。如果是Linux下的C/C程序可以用gdb attach或者pstack打出线程栈。实时监控的话top -H -p pid能看到进程内部各线程的CPU使用率。遇到“进程没挂但服务不可用”多半就是某个线程卡在锁等待或IO等待上先从线程栈找答案。6. 最后分享一点我自己的使用习惯我自己写并发代码不会一上来就纠结“这地方该用多进程还是多线程”。我会先问三个问题任务之间需不需要共享大量状态单个任务的故障影响范围能不能被接受CPU密集还是I/O密集答案更偏“隔离优先”就用多进程更偏“吞吐优先”就用多线程两者之间选混搭也可以比如用进程做模块隔离进程内再用线程做细粒度并发。还有几个小习惯线程一定要起名字哪怕短一点也没关系将来抓线程栈时能看到“worker-pool-1”而不是“Thread-12”线程池一定要用有界队列并设置合理的拒绝策略进程退出时一定处理子进程回收避免僵尸进程堆积能跑虚拟线程或协程的I/O密集场景优先考虑它们而不是硬撑几千个内核线程。踩过几次坑之后最大的感受是进程和线程的区别不只是面试题它决定了你将来排查问题时的思路。遇到服务器异常先分清是进程层面出了问题还是线程层面出了问题排查路径完全不同。这篇文章写到的排查命令和配置都是我实际用过的你可以直接拿去参考。