恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

Java Thread类实战指南:生命周期、线程协作与高并发避坑

  • 首页
  • 资讯中心
  • /
  • Java Thread类实战指南:生命周期、线程协作与高并发避坑

相关资讯

建站工具如何兼顾SEO优化与移动端适配:从选型到验证全攻略 2026/9/10 1:45:01
ClickHouse v20.8.7.15 LTS 发布说明解析:SNI 安全接入支持与 11 项关键缺陷修复 2026/9/10 1:45:01
鼎捷E10培训PPT制作:从业务流程出发,告别功能说明书 2026/9/10 1:45:01

最新资讯

ML-For-Beginners 聚类作业实战:在尼日利亚音乐数据集上尝试 K-Means 之外的聚类方法
smic18工艺库文件全解析:类型、部署与避坑指南
纯C OCR引擎:Android端轻量高效文字识别方案
Carbon 仓库的 AI 助手工具规范:禁用传统 Shell 命令、改用语义化 API 工具
微信小程序智慧旅游平台开发实战:从需求到上线全流程解析
Kruskal-Wallis检验样本量影响:从统计功效到p值稳定性

今日推荐

AI搜索重构内容生态:企业从“流量争夺”转向“答案共建”
AI搜索的信任缺口:企业内容如何在答案时代自证可信
Spring Boot+Vue+Node.js售后服务系统开发实战

本周热门

超人会飞不算本事:系统稳定依赖清晰规则与边界设计
超人VS蜘蛛侠:拆解超级IP的影响力与传播方法论
基于CNN的调制信号识别:MATLAB实现时频图分类实战

本月精选

自研推理加速器Redwood:两周内实现PyTorch模型高效部署的实战教程
V4L2摄像头采集实战:从camera_client.rar到出图全流程解析
从“谁发明了钢琴键”到知识问答智能体:RAG与记忆工程实践

Java Thread类实战指南:生命周期、线程协作与高并发避坑

发布时间:2026/9/10 1:45:01
Java Thread类实战指南:生命周期、线程协作与高并发避坑 先声明一下这篇文章的定位。Thread类是Java多线程编程最基础、最常用的入口之一网上讲它的资料多如牛毛但大多数不是太偏理论就是只给了几个demo真遇到项目里的复杂场景照样抓瞎。我自己从写第一个多线程下载器到维护高并发接口跟这个类打了快十年的交道这篇文章会把Thread类的基本用法掰开揉碎讲清楚从生命周期、创建方式到线程协作再到坑位排查尽量让新手能落地、让老手能查漏补缺。很多人觉得Thread简单new一个Thread然后start()就完事了但真正用起来会发现一堆问题线程间怎么传数据、怎么优雅停止、子线程异常怎么处理、多个线程怎么协同工作……这些看似零散的问题本质上是没把Thread类的设计思路吃透。这篇文章不追求面面俱到只讲实际用得上、用得对的部分。1. 从线程模型说起Thread类到底解决了什么问题1.1 进程与线程的关系以及为什么需要线程先回到底层。一个Java程序运行起来就是一个JVM进程进程是操作系统分配资源的基本单位有自己的内存空间和文件句柄等。如果在进程内部只有一个执行流那么所有代码都得排队执行先读文件再算逻辑最后写数据库任何一步慢了后面全卡住。线程就是进程内部的多个执行流。它们共享进程的内存空间堆和方法区但每个线程有自己的虚拟机栈和程序计数器所以线程之间协作很方便但也正因为共享内存线程安全问题才会那么棘手。用生活化的类比来理解进程就像一家餐厅有自己的场地、厨房和食材库存线程就像餐厅里的厨师和服务员。多个厨师可以同时做菜共用同一个厨房共享内存但如果两个厨师同时用同一个灶台、同时改同一份菜谱就会出乱子。这就是线程安全问题的本质多个执行流同时读写共享资源。1.2 Java中线程的创建入口Thread类在Java的世界里创建一个线程就是创建一个Thread对象然后调用它的start()方法启动。JVM底层会调用操作系统的线程创建接口在Linux上是clone系统调用在Windows上是CreateThread把Java层的方法栈映射到操作系统线程上。所以Java线程本质上是对操作系统线程的一层封装这也是为什么Java线程被称为平台线程相对于JDK 19开始引入的虚拟线程。Thread类本身干了三件事封装了线程的元信息名字、优先级、是否守护线程、线程组等定义了线程的状态和生命周期提供了控制线程运行的核心方法start、join、interrupt、sleep、yield等搞清楚这三个层面Thread类基本就掌握一半了。1.3 线程生命周期与状态流转很多面试题会问Java线程有哪些状态但实际开发中真正有用的是对状态流转的直觉。JDK中Thread.State枚举定义了六种状态状态含义进入条件切换到其他状态的方式NEW新建new Thread()后start()RUNNABLE可运行start()后或从阻塞/等待中被唤醒被调度器调度执行或等待锁、等待条件BLOCKED阻塞试图进入synchronized同步块但锁被占用获得锁WAITING等待调用wait()、join()、LockSupport.park()被notify()/notifyAll()唤醒或join的线程执行完TIMED_WAITING限时等待调用sleep(ms)、wait(ms)、join(ms)、LockSupport.parkNanos()等待时间到或被唤醒TERMINATED终止run()方法执行完毕或抛出未捕获异常—这里有个常见的误解RUNNABLE状态同时包含正在运行和等待CPU时间片两种子状态Java层面不做区分。所以在jstack里看到线程是RUNNABLE不代表它一定在跑它可能只是在排队等CPU。实际开发中你需要重点注意的是BLOCKED和WAITING的区别BLOCKED是拿不到锁WAITING是拿到了锁但主动等条件满足。这两个状态调优的思路完全不一样BLOCKED多说明锁竞争激烈WAITING多说明条件等待逻辑有问题。2. 创建线程的四种姿势与选型思路2.1 继承Thread类最基础但最不推荐的写法直接上代码public class MyThread extends Thread { Override public void run() { System.out.println(线程执行中 Thread.currentThread().getName()); } public static void main(String[] args) { MyThread t new MyThread(); t.start(); System.out.println(主线程 Thread.currentThread().getName()); } }这段代码能跑但我强烈不建议在真实项目里这么写。原因有两点第一Java是单继承的你继承了Thread就不能继承别的类了扩展性很差。 第二继承Thread意味着把任务代码和线程控制逻辑耦合在同一个类里任务本身要做什么事和任务的载体哪个线程执行没有分开不利于代码复用。不过有一个例外场景当你需要重写Thread的run()之外的方法时继承才有意义。比如需要重写interrupt()方法做额外的资源清理这时候继承Thread是合理的。2.2 实现Runnable接口最推荐的入门写法public class Task implements Runnable { Override public void run() { System.out.println(任务执行中 Thread.currentThread().getName()); } public static void main(String[] args) { Thread t new Thread(new Task(), task-thread); t.start(); } }Runnable把要执行的任务从执行任务的线程中解耦出来。同一个Task实例完全可以传给多个Thread执行也可以配合线程池使用。这是生产环境最常用的任务定义方式。关于start()和run()新手最容易踩的坑就是直接调用run()。直接调用run()只是普通方法调用全程在主线程执行根本没有创建新线程。第一课就记住启动线程是start()不是run()。2.3 Callable与FutureTask需要返回结果怎么办Runnable的run()方法没有返回值也不能抛出受检异常。但如果你的任务需要返回结果比如并行计算求和就得用CallableCallableInteger callable new CallableInteger() { Override public Integer call() throws Exception { // 模拟耗时计算 Thread.sleep(1000); return 42; } }; FutureTaskInteger futureTask new FutureTask(callable); Thread t new Thread(futureTask); t.start(); // 主线程做其他事情... // 获取结果此时会阻塞直到任务完成 Integer result futureTask.get(); System.out.println(结果 result);FutureTask本质上是一个Runnable因为它实现了RunnableFuture接口Runnable和Future的子接口所以可以传给Thread或线程池。futureTask.get()会阻塞当前线程直到任务完成也可以传超时参数futureTask.get(3, TimeUnit.SECONDS)防止任务卡死导致主线程无限等待。还有个细节同一个FutureTask只能执行一次重复调用run()不会有任何效果因为FutureTask内部维护了任务状态。2.4 线程池生产环境真正该用的方式上面三种方式都是直接new Thread这在任务量少、生命周期短的场景下没问题但并发任务一多就有问题频繁创建销毁线程开销大线程数量失控会导致系统资源耗尽。生产环境推荐用线程池ExecutorService executor Executors.newFixedThreadPool(4); executor.submit(() - { System.out.println(线程池任务 Thread.currentThread().getName()); }); executor.shutdown();这里有个非常重要的点线程池里的线程是复用的任务执行完线程并不会销毁而是继续从阻塞队列里取下一个任务。这跟直接new Thread一次性用完就销毁有本质区别。不过我也提醒一句阿里开发规范明确禁止使用Executors的快捷方法来创建线程池因为newFixedThreadPool的队列是无界LinkedBlockingQueue任务堆积太多会导致OOMnewCachedThreadPool的线程数是Integer.MAX_VALUE极端情况下会创建大量线程导致系统崩溃。建议直接用ThreadPoolExecutor手动指定核心线程数、最大线程数、队列类型和拒绝策略。2.5 四种方式的对比与选型建议创建方式任务复用获取返回值异常处理生产环境适用度继承Thread否否通过UncaughtExceptionHandler低实现Runnable是搭配线程池否同上中Callable FutureTask是搭配线程池是可通过Future.get()捕获高ThreadPoolExecutor是是可通过Future.get()捕获最高我的建议是面试和基础学习阶段四种方式都要会写真实项目的业务代码里任务直接交给线程池去执行使用Runnable无返回值或Callable有返回值几乎不应该手动new Thread。3. 核心线程方法从命名到中断机制逐个拆解3.1 线程命名与优先级排查问题时名字就是救命的线程命名看起来是最不起眼的操作但到线上排查问题时就体现出价值了。两个线程同时在执行jstack把堆栈打出来如果线程名都是默认的Thread-0、Thread-1你根本分不清哪个是哪个如果命名为order-service-pool-1、kafka-consumer-2一眼就能定位到具体业务模块。设置线程名有几种方式// 构造器指定 Thread t1 new Thread(runnable, order-sync-task); // 创建后设置 Thread t2 new Thread(runnable); t2.setName(order-sync-task); // 线程池设置线程名可以用自定义ThreadFactory ThreadFactory namedThreadFactory new ThreadFactory() { private final AtomicInteger seq new AtomicInteger(1); Override public Thread newThread(Runnable r) { Thread t new Thread(r); t.setName(order-pool- seq.getAndIncrement()); t.setDaemon(true); return t; } }; ExecutorService executor Executors.newFixedThreadPool(4, namedThreadFactory);线程优先级用setPriority(int priority)设置取值1到10默认5。但我必须泼一盆冷水线程优先级在Windows上基本没用Linux上也只能算是一个建议值操作系统调度器不一定会听你的。不要指望靠提高优先级来解决性能问题更不要用优先级来控制业务逻辑顺序那绝对会踩坑。3.2 sleep、join、yield看似简单实则各有各的坑Thread.sleep(ms)让当前线程进入TIMED_WAITING状态释放CPU但不释放锁。注意sleep不释放synchronized锁这个和wait()有本质区别。如果某段代码持有锁又在锁内调用sleep其他线程只能干等着。我之前排查过一个性能问题一个线程持有数据库连接池的锁后sleep了5秒导致整个连接池的线程全部阻塞其实就是业务逻辑里误用了sleep做限流。join()让当前线程等待另一个线程执行完毕。join的内部实现是当线程终止时调用notifyAll来唤醒等待者所以实际上是一个wait/notify的封装。Thread worker new Thread(() - { System.out.println(worker开始干活); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(worker干完了); }); worker.start(); worker.join(); // 主线程在这里等待worker执行完 System.out.println(主线程继续执行);join有个容易忽略的点join会抛出InterruptedException调用join的线程如果被中断会抛出异常但worker线程本身不会受到影响。Thread.yield()让出CPU时间片让自己重新进入就绪队列与其他线程竞争。这个方法在实战中几乎用不到我用过的唯一场景是在自旋等待时配合使用减少CPU占用。比如while (!ready) { Thread.yield(); // 避免自旋导致CPU 100% }但从Java 9之后更推荐用Thread.onSpinWait()。3.3 interrupt机制中断不等于强制停止这是最容易搞错的地方先说结论Thread.stop()方法已经废弃了永远不要用。因为它会直接终止线程并释放所有锁导致共享数据可能处于不一致的状态。正确的做法是通过interrupt()协作式中断。interrupt()做的事情是设置线程的中断标志位为true而不是强制终止线程。线程的运行逻辑必须自己检查中断状态并作出响应调用Thread.interrupted()会清除中断标志或Thread.currentThread().isInterrupted()不会清除中断标志。比较典型的场景是处理循环任务class Worker implements Runnable { Override public void run() { // 使用Thread.currentThread().isInterrupted()检查中断标志 while (!Thread.currentThread().isInterrupted()) { doWork(); } System.out.println(检测到中断信号线程退出); // 做资源清理 } private void doWork() { try { // 模拟耗时阻塞操作 Thread.sleep(500); } catch (InterruptedException e) { // 关键点sleep等在阻塞时收到中断信号会清除中断标志并抛出InterruptedException // 所以这里必须重新设置中断标志否则上层循环检查不到中断状态 Thread.currentThread().interrupt(); } } }这个例子里的Thread.currentThread().interrupt()是这段代码的灵魂。因为在大多数阻塞方法sleep、wait、join里收到中断信号时它们会清除中断状态然后再抛异常如果catch里不手动重新设置中断标志上层while循环的isInterrupted()会一直返回false线程根本停不下来。一个团队里就发生过这种事故有人写了一个while循环在catch块里只打了日志没恢复中断标志结果运维想通过kill线程的方式优雅停服线程就是不停最后只能强杀JVM进程。interrupt中断阻塞状态时根据阻塞方法的不同反应也不同对于sleep、wait、join等可中断阻塞会抛InterruptedException对于synchronized等待锁的BLOCKED状态中断无效——线程不会因为interrupt()而从锁等待中退出对于LockSupport.park()JUC并发包的工具中断会使park立即返回但不会抛异常3.4 daemon线程守护线程的边界setDaemon(true)可以把一个线程设置为守护线程。JVM退出的条件是所有非守护线程都执行完毕守护线程会被强制终止且不执行finally块。这个特性意味着守护线程一般用来做辅助性、可以随时被丢弃的工作比如JVM自带的GC线程就是守护线程。但自己写业务代码时千万要小心别把重要的定时任务放在守护线程里。我曾经在一个项目里用ScheduledExecutorService配合守护线程跑定时清理任务正常运行时一切正常某个晚上主流程提前结束JVM直接退出守护线程瞬间没了定时任务没跑完数据库里残留了几万条脏数据。3.5 线程间共享数据的可见性volatile的实际价值这里要先澄清一个概念Thread类本身不提供任何线程安全机制。它只是提供了创建线程和控制线程的方法。真正保障线程安全的是Java内存模型JMM下的关键字和并发工具。在多线程环境下每个线程都有自己独立的工作内存相当于CPU的L1/L2缓存对共享变量的操作需要经过从主内存读取到工作内存、在工作内存中修改、再写回主内存三个步骤。如果没有同步机制一个线程对共享变量的修改另一个线程可能看不到这就是可见性问题。volatile关键字解决的就是可见性和有序性问题它保证了对volatile变量的读写操作都是直接读写主内存的。但要注意volatile不解决原子性问题。i这种读取-修改-写入复合操作用volatile修饰也无法保证线程安全必须用AtomicInteger或synchronized。实际应用中我常用volatile来标记线程执行状态class TaskRunner implements Runnable { private volatile boolean running true; Override public void run() { while (running) { doWork(); } } // 其他线程通过调用这个方法通知停止 public void stop() { running false; } }这里如果不加volatile另一个线程调stop()修改running后工作线程可能永远也看不到变更导致循环停不下来。4. 线程安全与共享数据那些年踩过的竞态坑4.1 竞态条件为什么会发生从i说起随便写一个最简单的例子两个线程同时对共享变量count执行10000次自增操作。运行结果几乎不可能等于20000这就是经典的竞态问题。i在字节码层面是多个指令从主内存读取count、将count压栈、执行1操作、将结果写回。两个线程可能在读取和写回之间被打断导致最后写回的是旧值1而不是真正应该的值。用更确切的语言说在线程的视角里i不是一个原子操作。要解决这个问题有三个方向第一用synchronized加锁保证同一时间只有一个线程执行i。 第二用AtomicInteger等原子类用CASCompare And Swap保证原子性。 第三用ThreadLocal让每个线程操作自己的变量副本互不干扰。4.2 synchronized的正确使用与锁对象选择synchronized是Java内置的锁机制用好了很简单用不好就是性能瓶颈。class Counter { private int count 0; // 方法级别锁锁的是当前对象this public synchronized void increment() { count; } }锁的粒度决定并发性能。方法级synchronized锁的是整个this对象如果一个类里多个不同业务方法都被synchronized修饰那么调用不同方法的线程也会互相阻塞因为它们抢的是同一把锁。这种场景建议将锁的粒度细化锁不同的私有成员变量。一个更隐蔽的问题是锁对象的选择。如果是静态方法上的synchronized锁的是Class对象如果锁的是字符串常量且两个地方用同一个字符串字面量会导致意外阻塞——JVM会复用字符串常量两个毫无关系的模块可能因为共享了同一个字符串锁而互相影响。4.3 ThreadLocal每个线程自己的变量副本ThreadLocal的核心思想是它为每个线程维护一份独立的变量副本每个线程只能访问自己的副本。private static final ThreadLocalSimpleDateFormat DATE_FORMAT ThreadLocal.withInitial(() - new SimpleDateFormat(yyyy-MM-dd HH:mm:ss));这个例子很经典。SimpleDateFormat不是线程安全的如果在多个线程中共享同一个实例会有并发问题每次都new一个又太浪费。用ThreadLocal让每个线程持有一个自己的SimpleDateFormat既安全又能复用。但ThreadLocal有一个必须警惕的问题内存泄漏。ThreadLocalMap中的key是WeakReference弱引用在GC时可能被回收但value是强引用如果线程长期存活比如线程池里的线程value就一直无法回收导致内存泄漏。解决方法是在使用完ThreadLocal后显式调用remove()。特别是Web应用中的请求处理线程requestContext、用户信息等存到ThreadLocal后如果不在finally块中remove高并发下很容易把堆内存耗尽。4.4 死锁多把锁嵌套时的致命陷阱死锁的发生需要四个条件同时满足互斥占用、持有并等待、不可剥夺、循环等待。实际编码中最常见的诱发因素是多个线程以不同顺序获取多把锁。// 线程A先拿锁1再拿锁2 synchronized (lock1) { synchronized (lock2) { // 业务逻辑 } } // 线程B先拿锁2再拿锁1 synchronized (lock2) { synchronized (lock1) { // 业务逻辑 } }当线程A拿到lock1等待lock2同时线程B拿到lock2等待lock1时两个线程互相等待谁也进不去。排查死锁的经验发生线上死锁时先别急着重启用jstack导出线程堆栈找到Found one Java-level deadlock字样它会直接告诉你哪两个线程持有哪把锁、在等待哪把锁、锁对象是什么类、在源码第几行。根据这个信息修复非常快。修复的核心思路是统一锁的获取顺序。4.5 线程池与Thread混用时的坑讲了这么多基础用法还有一个细节需要特别提醒不要在线程池的任务中调用thread.join()或thread.wait()等阻塞操作除非你非常确定线程池的线程数足够多。比如线程池固定2个线程提交3个任务任务1等待任务2的join结果而任务2正在队列中排队得不到执行任务1永远等不到任务2线程池就卡死了。这种问题在代码review时很难发现通常要等到线上出现任务积压才暴露。另外任务抛出的异常在execute()和submit()中的处理方式也不同execute()的方法会把异常打印到控制台或交给UncaughtExceptionHandler处理submit()则会把异常封装到Future里下次调用future.get()时抛出如果一直不调用get()异常会被静默吞掉。调优这类问题最痛苦的就是日志里没有异常但任务就是没执行其实就是异常被Future吞了。5. 让多线程协作基础同步工具的实战用法5.1 wait/notify的正确姿势wait和notify是Object类的方法必须在synchronized同步块内调用否则会抛IllegalMonitorStateException。它们配合synchronized实现线程间的等待和通知机制。class MessageQueue { private final LinkedListString queue new LinkedList(); public synchronized void put(String message) throws InterruptedException { while (queue.size() 10) { // 队列满了生产者等待 wait(); } queue.addLast(message); // 唤醒等待的消费者 notifyAll(); } public synchronized String take() throws InterruptedException { while (queue.isEmpty()) { wait(); } String msg queue.removeFirst(); notifyAll(); return msg; } }这里有两个关键点容易踩坑。第一个是条件判断必须用while循环而不是if因为wait()被唤醒后可能条件并没有满足可能是其他线程抢先消费了需要重新检查条件。第二个是notify和notifyAll的选择notify只能唤醒一个等待线程如果等待线程有多个且条件不同可能唤醒的线程条件仍不满足导致继续等待所以大多数情况下用notifyAll更稳妥。要特别注意wait()的释放锁机制。wait()会让出锁、让出CPU进入WAITING状态。这和sleep()有本质区别sleep()是让出CPU但不让出锁。5.2 CountDownLatch与普通Thread的区别热词里提到了CountDownLatch这里专门说一下。CountDownLatch是JUC包里的一个同步工具它和普通Threadjoin的协作方式有本质区别普通Thread的join是控制一个或多个线程等待目标线程执行完成但join没法解决主线程等待多个并发任务都完成后再继续的场景吗其实join也能做到ListThread threads new ArrayList(); for (int i 0; i 5; i) { Thread t new Thread(task); threads.add(t); t.start(); } for (Thread t : threads) { t.join(); } System.out.println(所有任务执行完成);这个写法能实现类似CountDownLatch的效果但有个明显的局限无法动态等待——你必须在启动所有线程之前就知道线程的数量并在代码里遍历join。CountDownLatch的用法更灵活它的核心是一个计数器初始化时设定计数值每次调用countDown()计数器减1调用await()的线程会一直阻塞直到计数器归零CountDownLatch latch new CountDownLatch(5); ExecutorService executor Executors.newFixedThreadPool(5); for (int i 0; i 5; i) { executor.submit(() - { try { // 执行任务 Thread.sleep(1000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { // 任务完成计数器减1 latch.countDown(); } }); } // 主线程等待所有任务完成 latch.await(); System.out.println(所有任务完成继续执行);对比一下两者的核心区别维度Thread.joinCountDownLatch适用场景等待特定线程执行完等待任意计数归零动态性需要提前知道线程列表可动态提交任务每个任务完成时countDown等待超时join(ms)支持await(ms)支持资源控制需要自己管理线程列表可配合线程池使用设计思想线程级别的等待事件计数级别的等待实际项目中我几乎全部用CountDownLatch而不用join原因很简单项目里没有直接new Thread的代码所有任务都走线程池线程池返回的Future只能用get()等待join根本用不上。而CountDownLatch可以在任意位置countDown可以与ExecutorService无缝配合这才是它真正实用的地方。CountDownLatch还有一个常见用法是模拟并发压测让多个线程同时起跑用CountDownLatch作为起跑发令枪CountDownLatch ready new CountDownLatch(10); CountDownLatch start new CountDownLatch(1); CountDownLatch end new CountDownLatch(10); for (int i 0; i 10; i) { new Thread(() - { try { ready.countDown(); start.await(); // 所有线程都ready后统一开始 System.out.println(线程开始执行 Thread.currentThread().getName()); // 模拟业务操作 Thread.sleep(500); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { end.countDown(); } }).start(); } ready.await(); // 等待所有线程就绪 start.countDown(); // 发令 end.await(); // 等待所有线程执行完 System.out.println(所有线程执行完成);这种写法的价值在于所有线程都在同一个起点竞争公平地模拟了并发压测的起跑时刻。如果只是for循环里直接start()线程创建和启动的时间差可能达到几十毫秒压测结果就失真了。5.3 用简单并发工具做性能测试的实战示例说一个实际的例子上线一个接口前我需要快速验证它的吞吐量又不想引入JMeter或wrk这类压测工具就用CountDownLatch写了一个简短的压力测试public class SimplePressureTest { public static void main(String[] args) throws InterruptedException { int concurrency 20; int totalRequests 200; CountDownLatch readyLatch new CountDownLatch(concurrency); CountDownLatch startLatch new CountDownLatch(1); CountDownLatch endLatch new CountDownLatch(totalRequests); AtomicInteger successCount new AtomicInteger(); AtomicInteger failCount new AtomicInteger(); ExecutorService executor Executors.newFixedThreadPool(concurrency); for (int i 0; i totalRequests; i) { executor.submit(() - { readyLatch.countDown(); try { startLatch.await(); // 模拟真实接口调用 boolean success callApi(); if (success) { successCount.incrementAndGet(); } else { failCount.incrementAndGet(); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); failCount.incrementAndGet(); } finally { endLatch.countDown(); } }); } readyLatch.await(); long startTime System.currentTimeMillis(); startLatch.countDown(); endLatch.await(); long cost System.currentTimeMillis() - startTime; executor.shutdown(); System.out.println(总耗时 cost ms); System.out.println(成功 successCount.get() 失败 failCount.get()); System.out.println(QPS (totalRequests * 1000 / cost)); } private static boolean callApi() { // 这里写接口调用逻辑 return true; } }这段代码虽然简单但思路能直接复用到生产环境做冒烟压测。注意readyLatch和startLatch的组合使用确保所有请求线程真正就绪后才开始计时避免线程的创建时间污染数据。6. 常见问题与异常排查实录6.1 exception in thread main java.lang.NoSuchMethodError这个异常热词特别经典先解释一下它是什么意思。NoSuchMethodError表示JVM在运行期找不到某个方法但它和NoSuchMethodException不同后者是反射调用时找不到方法通常可以捕获处理前者是JVM加载类时发现类文件中的方法签名不匹配。常见原因有三个第一个是依赖冲突。项目用到的不同jar包引入了同一个类的不同版本编译时的版本有某个方法运行时实际加载的版本没有该方法。尤其是用Maven/Gradle时依赖树里有传递依赖很容易出现两个版本并存classpath顺序靠前的那个版本赢了。排查方法是用mvn dependency:tree查看依赖树或者用arthas的sc命令查看某个类实际从哪个jar加载。第二个是代码热部署没清理干净。在IDEA中热部署或使用JRebel时旧版本的类没有被完全替换导致新旧类的字节码混在一起。出现这种异常时的第一反应是Clean、重新编译、重启应用。第三个是API版本升级后没有重新编译依赖方。比如A模块依赖B模块的某个方法B模块升级后删掉了这个方法但A模块没有重新编译还是用旧编译产物运行。6.2 主线程上做了不该做的事阻塞UI线程热词里有一条url loading of should not occur on this applications main thread这虽然不是Java服务端的典型问题但理念是通用的主线程不应做耗时操作。在Android开发里主线程也叫UI线程任何网络请求和耗时数据库操作都不允许在主线程执行否则会卡UI甚至触发ANR崩溃。在Java Web服务端也一样如果是简单的Servlet容器或Netty的EventLoop线程一旦在接收到请求的线程上执行耗时操作比如同步调用远程服务、执行慢SQL整个事件循环就卡住了其他所有请求都会排队等待。这类问题的排查思路是先看线程名和堆栈。如果主线程main或事件循环线程卡在某处执行耗时逻辑一定要把任务丢到独立的业务线程池去执行。6.3 线程池线程数怎么定一个不那么玄学的参考公式很多人问线程池的核心线程数该设置多少其实业界有个计算公式Brian Goetz在《Java Concurrency in Practice》中提出的线程数 N_CPU * (1 W/C)其中N_CPU是CPU核数W是等待时间IO操作、网络调用、锁等待C是计算时间。对于CPU密集任务W/C趋于0线程数约等于CPU核数对于IO密集任务W/C通常较大线程数可以适当增加。但公式只是起点还有几个关键因素要重点考虑JVM的GC线程本身会占用CPU资源物理容器上可能部署了多个应用共享CPU线程本身还有一个线程栈内存消耗默认1MB不能忽略过高线程数会导致频繁上下文切换抵消并发收益。所以很多人在真实场景里会基于公式算出理论值后再通过压测上下浮动调整。6.4 用jstack定位死锁和线程卡顿jstack是JDK自带的线程诊断工具线上排查问题它几乎是第一选择。用法很简单jstack pid thread_dump.txt看thread_dump.txt时重点看3个地方deadlock信息jstack会自动检测死锁找到Found one Java-level deadlock段落线程状态大量BLOCKED说明有激烈锁竞争大量WAITING说明可能有线程等待条件大量RUNNABLE且CPU飙高说明有大量计算或自旋线程名和堆栈定位结合日志里的线程名找到对应的业务代码位置有一次线上用户反馈响应变慢jstack一看发现80%的线程都在等同一个数据库连接池的连接但连接池已经耗尽。顺藤摸瓜找到原因一个事务里嵌套调用另一个服务那个服务又反向调回来形成了循环依赖连接被占用之后死等。这种问题不通过线程转储基本不可能定位。6.5 线程dump分析时最容易忽略的细节前两步操作没做对直接看线程dump几乎必翻车。第一触发dump之前先连续抓三次每次间隔5-10秒例如jstack pid dump1.txtsleep 10秒再抓第二次对比分析才能看出线程是偶尔卡顿还是持续阻塞。单次dump可能只是运气不好抓到某个线程刚好在GC或短暂等待。第二如果是高并发接口卡顿随时抓dump会扰动现场有条件的话配合top命令定位CPU占用最高的几个线程ID把线程ID转成十六进制再在dump里搜能直接锁定占用CPU的真凶。7. 我的实操心得与几个实用小技巧说几个实际工作中总结出来的经验。第一写多线程代码时先想清楚线程之间是什么协作关系是互相独立、互不干扰比如批量处理多条数据还是需要协作比如生产者消费者模式还是一个等待另一个的结果。关系确定了再决定用哪种工具独立任务用线程池加Future协作任务用CountDownLatch、Semaphore或BlockingQueue结果汇聚用Future或CompletableFuture。用错工具往往是面试里考你、线上坑你的第一步。第二给线程池和周期性任务做监控。这个很容易被忽略线程池运行正常的时候确实不需要关注但一旦出现任务积压你是想让线程池自己默默排队还是立即抛出异常生产环境我的建议是给线程池的BlockingQueue设置一个报警阈值对任务执行时间做监控快速发现慢任务。第三多线程代码review时特备容易出问题的地方线程池的拒绝策略默认AbortPolicy会直接抛RejectedExecutionException建议改成CallerRunsPolicy让调用线程自己执行保证任务不丢线程池的shutdown和shutdownNow的区别shutdownNow会中断正在执行的任务如果任务没有正确处理中断信号可能导致资源没被释放异常处理要区分execute和submit。第四能用JUC包里的高阶工具比如CompletableFuture、BlockingQueue就别想着自己用Threadwait/notify从零实现你踩过的坑前人大概率都踩过了。尤其是自认为自己实现一个线程安全队列这事我见过太多项目在这里翻车标准库的工具经过海量验证远比临时手写的稳。第五Thread类的底层原理值得去深入了解一下。比如start()方法为什么会触发线程的启动它在JDK源码里最终调用了native的start0()方法线程栈的大小可以通过-Xss参数调整默认在64位Linux下是1MB如果递归深度很深栈溢出时需要调大JVM退出时守护线程不执行finally块这些知识在遇到问题时能帮上大忙。最后再分享一个小经验写异步任务时日志里一定要带线程名。在日志配置中加上%thread占位符线上出了问题时顺着线程名和日志时间线能把整个调用链路拼出来定位效率能提升好几个量级。这个习惯坚持下来你会感谢当初的自己。

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号