Java并发编程实战:从多线程基础到线程池与多进程应用

发布时间:2026/7/15 6:38:21
Java并发编程实战:从多线程基础到线程池与多进程应用 1. 从零开始为什么Java开发者必须搞懂并发编程如果你是一名Java开发者或者正在学习Java那么“多线程”和“多进程”这两个词一定像幽灵一样时不时在你耳边响起。面试官爱问性能优化要用框架底层也离不开它。但说实话很多朋友对它的理解可能还停留在“继承Thread类”和“实现Runnable接口”的层面一旦遇到实际的生产问题比如线程池参数怎么调、死锁怎么排查、如何优雅地关闭线程就立刻抓瞎了。我干了十多年Java开发从早期的单核服务器到现在的云原生微服务并发编程一直是核心中的核心。它绝不仅仅是书本上的概念而是直接决定了你写的程序是“能用”还是“好用”是“稳定”还是“动不动就崩”。今天这篇长文我就想抛开那些教科书式的定义从一个一线开发者的视角跟你彻底聊透Java里的多进程与多线程。我会从最基础的概念讲起一直深入到高并发场景下的实战技巧和避坑指南。这篇文章的目标很明确让你不仅知道怎么用更明白为什么这么用以及怎么用得好、用得稳。简单来说多线程和多进程是程序“一心多用”的两种方式。多线程像是你一个人一个进程同时处理好几件家务多个线程比如一边听音乐线程A一边拖地线程B大脑CPU在它们之间快速切换。好处是资源共享方便都用一个家的东西沟通成本低坏处是万一拖地时绊倒了音响线一个线程崩溃可能整个音乐都停了进程崩溃。而多进程更像是你请了几个保洁阿姨多个进程来家里每人负责一个房间她们各自有独立的工具包内存空间。一个阿姨把水打翻了一个进程崩溃不会影响其他房间的阿姨工作隔离性好但让她们协作搬个大沙发进程间通信就比较麻烦得通过对讲机IPC机制来协调。在Java的世界里我们主要打交道的是多线程因为JVM本身就是一个进程。但理解多进程同样重要尤其是在需要调用外部程序、构建微服务架构或实现更高隔离性的场景时。接下来我们就一层层剥开它们的神秘面纱。2. 核心概念辨析多进程与多线程的本质区别在深入代码之前我们必须把地基打牢。很多人对多进程和多线程的区别模棱两可这直接导致了后续技术选型和问题排查时的困惑。2.1 资源与隔离性根本性的鸿沟这是两者最核心的区别我习惯用一个简单的比喻进程是“公司”线程是“部门”。进程公司拥有独立的“法人财产”。每个进程启动时操作系统都会为它分配独立的内存地址空间、文件描述符、环境变量等资源。一个进程崩溃通常不会直接影响其他进程因为它们的资源是隔离的。这带来了极高的稳定性。在Java中你启动一个java -jar命令就创建了一个JVM进程。如果你想运行另一个完全独立的Java程序就需要再启动一个JVM进程。线程部门共享“公司财产”。同一个进程内的所有线程共享该进程的堆内存、方法区、打开的文件等资源。它们有自己独立的栈空间用于存放局部变量、方法调用栈但堆是共用的。这意味着线程间通信非常高效直接读写共享变量即可但也带来了同步的复杂性。一个线程的非法内存访问如数组越界可能导致整个进程崩溃因为它们共享同一个内存空间。为什么这个区别如此重要假设你在开发一个Web服务器。如果用多进程模型如早期的Apache每个用户请求由一个独立进程处理那么即使某个请求的处理代码有内存泄漏甚至崩溃也只会干掉对应的那个进程服务器主进程和其他请求不受影响。但进程创建和销毁的开销公司注册、注销很大上下文切换成本也高。而用多线程模型如Tomcat的Connector所有请求处理线程共享同一个JVM进程的资源创建轻快通信高效能支持更高的并发。可一旦某个线程写坏了共享的缓存数据或者发生了死锁可能整个Web服务就卡死了。2.2 通信成本共享内存 vs. 进程间通信(IPC)由于资源隔离性的不同它们的通信方式天差地别。线程间通信简单直接但需谨慎。因为共享堆内存线程A只需修改一个对象的属性线程B下次读取时就能看到。这种基于共享内存的通信效率极高。但正是这种“方便”带来了并发编程中最经典的问题竞态条件。如果多个线程同时读写同一个变量而没有正确的同步结果将是不可预测的。这就需要用到synchronized、Lock、volatile等同步机制来“立规矩”。// 一个典型的共享资源竞态问题 public class Counter { private int count 0; // 线程不安全的方法 public void increment() { count; // 这行代码不是原子操作 } }count实际上包含读取、加1、写入三个步骤多个线程交错执行就会导致最终结果小于预期。这就是你必须使用同步的原因。进程间通信复杂但隔离。进程之间内存不共享必须通过操作系统提供的IPC机制来通信常见的有管道/命名管道单向字节流适合父子进程或有亲缘关系的进程。消息队列消息的链表存放在内核中进程可以独立地读写。共享内存映射一段能被多个进程访问的内存速度最快但需要自己处理同步。信号量用于进程间的同步不传递数据。套接字最通用的方式可以跨网络TCP/UDP都行。在Java中如果你用ProcessBuilder启动了一个外部进程想和它通信本质上就是通过管道获取Process对象的getInputStream()和getOutputStream()来实现IPC。2.3 创建与切换开销轻量级与重量级的代价创建开销启动一个新进程操作系统需要为其分配独立的内存空间、建立页表、初始化文件描述符等这是“重量级”操作。而创建一个线程主要是在现有进程内分配一个栈和线程控制块是“轻量级”操作。在Java中new Thread().start()比用ProcessBuilder启动一个全新的JVM要快得多。上下文切换开销CPU从一个进程切换到另一个进程需要保存和恢复整个内存映像、寄存器等开销大。而线程切换发生在同一进程内共享了内存空间只需要切换线程私有的栈和寄存器开销小得多。理解这些本质区别是你做出正确架构选择的基础。一般来说计算密集型任务且需要高隔离性、高稳定性的考虑多进程I/O密集型或需要大量共享状态、高并发响应的优先使用多线程。在现代Java开发中多线程是绝对的主流但多进程的知识在部署、运维和集成外部系统时不可或缺。3. Java多线程编程实战从基础API到并发工具包理论说再多不如一行代码。我们直接进入实战环节看看Java里怎么玩转多线程。3.1 线程创建的两种经典方式及其取舍你可能早就知道两种方式继承Thread类和实现Runnable接口。但为什么会有两种用哪种更好继承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(); // 正确方式启动新线程 // t.run(); // 错误这只是普通方法调用仍在main线程执行 } }优点写法简单直观直接调用start()即可。致命缺点Java是单继承的。一旦你的类继承了Thread就无法再继承其他业务类严重限制了代码的灵活性。这在设计上通常不被推荐。实现Runnable接口首选方式public class MyRunnable implements Runnable { Override public void run() { System.out.println(线程运行中: Thread.currentThread().getName()); } public static void main(String[] args) { Thread t new Thread(new MyRunnable()); t.start(); } }优点解耦更好任务Runnable与执行机制Thread分离。MyRunnable只是一个任务描述它可以被Thread执行也可以被提交给ExecutorService线程池执行灵活性极高。节省资源多个线程可以共享同一个Runnable实例如果设计得当适合处理同一份资源。符合组合优于继承原则你的类还可以自由地继承其他类。从Java 8开始有了更简洁的写法——Lambda表达式new Thread(() - { System.out.println(Lambda线程运行中); }).start();对于简单的任务这极大地简化了代码。实操心得在实际项目中我几乎从不使用继承Thread的方式。统一使用实现Runnable接口或者直接使用Lambda表达式。这为后续使用线程池等高级特性铺平了道路。记住start()方法是通知JVM去调度执行run()方法而直接调用run()仅仅是一个普通的方法调用不会启动新线程这是一个新手常踩的坑。3.2 线程的生命周期与状态控制线程不是一启动就运行到结束的。它有自己的生命周期理解状态转换对调试至关重要。Java线程的状态定义在Thread.State枚举中NEW新建new Thread()之后start()之前。RUNNABLE可运行调用start()后。注意这并不代表线程正在CPU上执行只是说它具备了运行条件可能在等待CPU时间片。这对应了操作系统层面的“就绪”和“运行”两种状态。BLOCKED阻塞线程等待获取一个监视器锁例如进入synchronized方法或代码块但锁被其他线程持有。这是同步场景下的被动等待。WAITING等待线程进入等待状态需要被其他线程显式唤醒。调用Object.wait()、Thread.join()或LockSupport.park()方法会进入此状态。TIMED_WAITING超时等待与WAITING类似但带有超时时间。如Thread.sleep(long millis)、Object.wait(long timeout)、Thread.join(long millis)。TERMINATED终止线程执行完毕run()方法结束或异常退出。如何控制线程除了start()还有几个关键方法Thread.sleep(long millis)让当前线程休眠指定毫秒数不释放锁。常用于模拟耗时或定时任务。Object.wait()/Object.notify()/Object.notifyAll()用于线程间协作的经典方法。wait()会释放对象锁并让线程进入WAITING状态直到其他线程调用同一对象的notify()/notifyAll()将其唤醒。必须在synchronized同步块内使用。Thread.join()等待目标线程执行完毕。比如在主线程中调用thread.join()主线程就会阻塞直到thread线程结束。Thread.interrupt()向线程发送一个中断信号。这不会强制停止线程只是设置一个中断标志。线程需要自己检查这个标志通过Thread.interrupted()或isInterrupted()并决定如何响应通常用于优雅地终止线程。注意事项Thread.stop(),suspend(),resume()这些方法早已被标记为Deprecated因为它们强制终止或挂起线程可能导致资源未释放、对象状态不一致等严重问题。绝对不要在生产代码中使用它们。正确的线程终止方式是使用中断机制。3.3 线程同步锁、原子类与并发容器当多个线程共享资源时同步是逃不开的话题。Java提供了多种同步机制。1. synchronized 关键字这是最古老也最基础的互斥锁。它可以修饰方法或代码块。// 同步实例方法锁是当前对象实例(this) public synchronized void syncMethod() { ... } // 同步静态方法锁是当前类的Class对象 public static synchronized void staticSyncMethod() { ... } // 同步代码块可以指定任意对象作为锁更灵活 public void someMethod() { synchronized (lockObject) { // 临界区代码 } }优点简单JVM原生支持。缺点性能开销相对较大但在现代JVM中已优化很多。不够灵活锁的获取和释放是隐式的且不可中断、不可尝试、非公平。容易造成死锁两个线程互相等待对方持有的锁。2. java.util.concurrent.locks.Lock 接口ReentrantLock是其主要实现类提供了比synchronized更丰富的功能。Lock lock new ReentrantLock(); try { lock.lock(); // 可以尝试加锁 lock.tryLock() 可以响应中断 lock.lockInterruptibly() // 临界区代码 } finally { lock.unlock(); // 务必在finally块中释放锁防止异常导致锁无法释放 }相比synchronized的优势可尝试获取锁tryLock()获取不到立即返回或等待指定时间避免死等。可中断lockInterruptibly()在等待锁的过程中可以响应中断。公平锁new ReentrantLock(true)可以创建公平锁按等待顺序获取虽然通常性能稍差。可以绑定多个条件一个Lock可以创建多个Condition对象实现更精细的线程等待/唤醒。3. volatile 关键字volatile确保变量的可见性和禁止指令重排序但不保证原子性。可见性当一个线程修改了volatile变量新值会立即被刷回主内存并导致其他线程工作内存中的该变量副本失效从而强制它们去主内存读取新值。禁止重排序防止JVM和处理器进行可能影响程序正确性的指令重排。它适用于状态标志的场景如一个线程循环检查volatile boolean flag来决定是否退出。4. java.util.concurrent.atomic 原子类这是解决简单共享变量原子操作的利器如AtomicInteger,AtomicLong,AtomicReference等。它们利用CASCompare-And-Swap操作实现无锁线程安全性能通常比锁高。AtomicInteger counter new AtomicInteger(0); counter.incrementAndGet(); // 原子性的 i对于简单的计数器、序列生成器等原子类是首选。5. java.util.concurrent 并发容器这是提升多线程程序性能和稳定性的法宝。不要再使用synchronizedList或synchronizedMap包装旧容器了。ConcurrentHashMap高并发下性能卓越的哈希表。JDK 8之后采用数组链表/红黑树CASsynchronized实现分段锁思想已进化。CopyOnWriteArrayList/CopyOnWriteArraySet写时复制的列表/集合。每次修改操作都会复制底层数组适合读多写少如监听器列表的场景。ConcurrentLinkedQueue无界非阻塞线程安全队列。BlockingQueue接口及其实现ArrayBlockingQueue,LinkedBlockingQueue,PriorityBlockingQueue,SynchronousQueue等这是实现生产者-消费者模型的绝佳工具。线程池的核心工作队列就是它。避坑指南同步的选择是一门艺术。我的经验法则是能用无锁原子类、并发容器就不用锁如果必须用锁优先考虑更灵活、功能更强的ReentrantLock只有在非常简单、明确的临界区或者锁竞争不激烈时才使用synchronized。另外务必警惕死锁。确保多个线程以固定的顺序获取锁是预防死锁最有效的方法之一。4. 线程池精讲为什么说“线程池是并发编程的基石”直接new Thread()然后start()在简单的Demo里没问题。但在真实的高并发服务器中这是灾难性的。线程的创建和销毁开销不小无限制地创建线程会耗尽系统资源。线程池就是为了解决这个问题而生的。4.1 线程池的核心参数与工作原理Java通过ExecutorService接口及其实现类主要是ThreadPoolExecutor提供了强大的线程池支持。理解其构造参数是正确使用的关键public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueueRunnable workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)corePoolSize核心线程数线程池中长期维持的线程数量即使它们空闲。除非设置了allowCoreThreadTimeOut。maximumPoolSize最大线程数线程池允许创建的最大线程数。keepAliveTime unit空闲线程存活时间当线程数超过核心线程数时多余的空闲线程在等待新任务时的最长存活时间。workQueue工作队列用于存放等待执行任务的阻塞队列。队列的选择至关重要。threadFactory线程工厂用于创建新线程。可以在这里设置线程名、优先级、守护线程等便于监控和排查问题。handler拒绝策略当线程池和队列都满了无法处理新任务时采取的拒绝策略。线程池的工作流程务必牢记提交一个任务。如果当前运行的线程数 corePoolSize则立即创建新线程执行任务。如果运行的线程数 corePoolSize则将任务放入workQueue。如果队列已满且运行的线程数 maximumPoolSize则创建新线程执行任务。如果队列已满且运行的线程数已达maximumPoolSize则触发拒绝策略。4.2 如何配置一个“靠谱”的线程池这是面试高频题也是实战难点。没有放之四海而皆准的配置但有以下原则CPU密集型任务计算为主很少I/O等待。建议线程数设置为CPU核心数 1。设置过多线程会导致频繁的上下文切换反而降低性能。可以通过Runtime.getRuntime().availableProcessors()获取CPU核心数。I/O密集型任务任务大部分时间在等待I/O如网络请求、数据库查询。此时CPU经常空闲可以设置更多线程。一个经验公式是线程数 CPU核心数 * (1 平均等待时间 / 平均计算时间)。这个比例I/O时间/CPU时间很难精确计算通常可以设置为CPU核心数 * 2或者更高并通过压测找到最佳值。工作队列的选择LinkedBlockingQueue无界队列FixedThreadPool使用它。任务会一直堆积可能耗尽内存。适用于任务量可预测、需要平滑处理峰值的场景。ArrayBlockingQueue有界队列需要指定队列大小。可以防止资源耗尽但队列满后会触发创建新线程或拒绝策略。SynchronousQueue不存储元素的队列CachedThreadPool使用它。来一个任务如果没有空闲线程就创建新线程。适用于短生命周期的异步任务。拒绝策略AbortPolicy默认直接抛出RejectedExecutionException异常。CallerRunsPolicy用调用者所在的线程来执行任务。这提供了一个简单的反馈机制降低新任务提交速度。DiscardPolicy默默丢弃无法处理的任务。DiscardOldestPolicy丢弃队列中最老的任务然后尝试重新提交当前任务。一个生产环境常用的自定义线程池示例// 获取CPU核心数 int cpuCores Runtime.getRuntime().availableProcessors(); ThreadPoolExecutor executor new ThreadPoolExecutor( cpuCores, // corePoolSize: 核心线程数等于CPU核心数 cpuCores * 2, // maximumPoolSize: 最大线程数为CPU核心数的2倍针对I/O密集型调整 60L, TimeUnit.SECONDS, // keepAliveTime: 非核心线程空闲60秒后回收 new ArrayBlockingQueue(200), // workQueue: 使用有界队列容量200防止无限制堆积 new ThreadFactory() { // threadFactory: 自定义线程工厂设置线程名前缀和守护状态 private final AtomicInteger threadNumber new AtomicInteger(1); Override public Thread newThread(Runnable r) { Thread t new Thread(r, MyApp-Thread- threadNumber.getAndIncrement()); t.setDaemon(false); // 设置为非守护线程防止主线程退出后线程池被JVM强制结束 return t; } }, new ThreadPoolExecutor.CallerRunsPolicy() // handler: 调用者运行策略让提交任务的线程自己去执行起到缓冲作用 );4.3 Executors工厂类的陷阱与正确使用JDK提供了Executors工具类来快速创建线程池但其中暗藏陷阱Executors.newFixedThreadPool(int nThreads)创建固定大小的线程池使用无界的LinkedBlockingQueue。风险如果任务提交速度持续高于处理速度队列会无限增长最终导致OutOfMemoryError。Executors.newCachedThreadPool()创建可缓存的线程池使用SynchronousQueue。风险理论上可以创建无限多的线程受Integer.MAX_VALUE限制可能耗尽系统线程资源。Executors.newSingleThreadExecutor()创建单线程的线程池。风险同样使用无界队列有内存溢出风险。Executors.newScheduledThreadPool(int corePoolSize)创建支持定时或周期性任务的线程池。核心建议在生产环境中尽量不要直接使用Executors的快捷工厂方法尤其是newFixedThreadPool和newCachedThreadPool。应该根据自己业务的特点CPU密集型还是I/O密集型任务峰值情况使用ThreadPoolExecutor的构造函数手动创建并指定有界队列和合理的拒绝策略。这是避免线上服务因线程池问题而崩溃的关键一步。5. Java中的多进程编程Process与ProcessBuilder详解虽然Java以多线程见长但多进程编程在某些场景下无可替代。比如你需要调用一个外部的命令行工具如FFmpeg处理视频、执行一个Shell脚本或者为了更高的隔离性将部分功能拆分为独立进程。5.1 启动与管理外部进程在Java中启动进程主要通过Runtime.exec()和ProcessBuilder类。ProcessBuilder是更现代、更推荐的方式它提供了更精细的控制。基础用法执行一个命令并等待结果public class ProcessDemo { public static void main(String[] args) throws IOException, InterruptedException { // 使用ProcessBuilder构建进程 ProcessBuilder pb new ProcessBuilder(ls, -la, /tmp); // 可以设置工作目录 // pb.directory(new File(/home/user)); // 可以合并或重定向错误流到标准输出流 // pb.redirectErrorStream(true); Process process pb.start(); // 启动进程 // 获取进程的输出流即命令的执行结果 try (BufferedReader reader new BufferedReader( new InputStreamReader(process.getInputStream()))) { String line; while ((line reader.readLine()) ! null) { System.out.println(输出: line); } } // 等待进程执行完毕并获取退出码 int exitCode process.waitFor(); System.out.println(进程退出码: exitCode); } }关键点解析ProcessBuilder.start()启动进程返回一个Process对象。进程是异步启动的调用start()后立即返回。process.getInputStream()获取进程的标准输出流。你需要读取这个流来获取命令的输出。process.getErrorStream()获取进程的标准错误流。命令执行出错的信息会从这里输出。如果不读取这个流当错误输出缓冲区满时进程可能会被阻塞。process.getOutputStream()获取进程的标准输入流。你可以向这个流写入数据作为命令的输入。process.waitFor()阻塞当前线程直到该进程执行完毕并返回进程的退出码。通常0表示成功非0表示错误。5.2 进程间通信与输入输出流处理与进程通信本质上就是通过管道处理它的三个标准流stdin,stdout,stderr。这里有一个非常重要的坑你必须及时消费输出流和错误流否则可能导致进程阻塞。为什么操作系统的管道缓冲区大小是有限的。如果子进程向stdout或stderr写入了大量数据而父进程你的Java程序没有及时读取缓冲区一旦被填满操作系统就会挂起block子进程的写操作导致子进程无法继续执行进而可能造成死锁。正确处理多进程I/O的示例使用线程异步消费public class SafeProcessExample { public static void main(String[] args) throws IOException, InterruptedException { ProcessBuilder pb new ProcessBuilder(some_long_running_command); Process process pb.start(); // 启动两个线程分别异步读取标准输出和错误输出 Thread stdoutThread new Thread(() - { try (BufferedReader reader new BufferedReader( new InputStreamReader(process.getInputStream()))) { String line; while ((line reader.readLine()) ! null) { System.out.println(STDOUT: line); } } catch (IOException e) { e.printStackTrace(); } }); Thread stderrThread new Thread(() - { try (BufferedReader reader new BufferedReader( new InputStreamReader(process.getErrorStream()))) { String line; while ((line reader.readLine()) ! null) { System.err.println(STDERR: line); } } catch (IOException e) { e.printStackTrace(); } }); stdoutThread.start(); stderrThread.start(); // 如果需要向进程输入数据 // try (BufferedWriter writer new BufferedWriter( // new OutputStreamWriter(process.getOutputStream()))) { // writer.write(input data); // writer.newLine(); // writer.flush(); // } // 等待进程结束 int exitCode process.waitFor(); // 等待输出消费线程结束 stdoutThread.join(); stderrThread.join(); System.out.println(进程结束退出码: exitCode); } }5.3 超时控制与进程销毁外部进程可能挂起或执行时间过长我们必须有超时控制机制。public class ProcessWithTimeout { public static boolean runCommandWithTimeout(ListString command, long timeout, TimeUnit unit) throws IOException, InterruptedException { ProcessBuilder pb new ProcessBuilder(command); Process process pb.start(); // ... (启动线程消费输出流同上例) ... // 使用waitFor的重载方法指定超时时间 boolean finished process.waitFor(timeout, unit); if (!finished) { // 超时强制销毁进程 process.destroy(); // 发送SIGTERM信号 // 如果进程不响应SIGTERM可以尝试更强制的方式慎用 // process.destroyForcibly(); // 发送SIGKILL信号JDK 8 System.err.println(命令执行超时已终止进程。); return false; } // 等待输出线程结束 // stdoutThread.join(); stderrThread.join(); int exitCode process.exitValue(); // 此时可以安全获取退出码 System.out.println(命令执行完毕退出码: exitCode); return exitCode 0; } }process.waitFor(long timeout, TimeUnit unit)带超时的等待。process.destroy()请求正常终止进程发送SIGTERM信号。process.destroyForcibly()强制终止进程发送SIGKILL信号。应在destroy()无效后使用。实操心得处理外部进程时一定要在独立的线程中消费其stdout和stderr这是避免阻塞的黄金法则。对于长时间运行或不可控的进程必须设置超时和销毁逻辑防止其成为“僵尸进程”拖垮你的应用。此外ProcessBuilder可以方便地设置环境变量(environment())、工作目录(directory())和重定向(redirectInput/Output/Error)比古老的Runtime.exec()强大和清晰得多。6. 高级主题与性能优化实战掌握了基础我们来看看如何让并发程序更健壮、更高效。6.1 并发工具类CountDownLatch、CyclicBarrier、Semaphore、Exchangerjava.util.concurrent包提供了一系列比底层锁更高级的同步工具。CountDownLatch倒计时闩锁允许一个或多个线程等待其他线程完成操作。构造时设定一个计数await()方法阻塞直到计数减为0countDown()方法将计数减1。// 模拟主线程等待多个子线程初始化完成 CountDownLatch latch new CountDownLatch(3); for (int i 0; i 3; i) { new Thread(() - { // 模拟初始化任务 try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() 初始化完成); latch.countDown(); }).start(); } latch.await(); // 主线程在此等待直到3个子线程都countDown System.out.println(所有线程初始化完成主线程继续执行);CyclicBarrier循环栅栏让一组线程互相等待直到所有线程都到达一个公共屏障点然后一起继续执行。可以重复使用。// 模拟多个线程到达某个阶段后再一起执行下一阶段 CyclicBarrier barrier new CyclicBarrier(3, () - { System.out.println(所有线程已到达屏障开始执行下一阶段); }); for (int i 0; i 3; i) { new Thread(() - { System.out.println(Thread.currentThread().getName() 到达屏障前); try { barrier.await(); // 等待其他线程 } catch (Exception e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() 冲破屏障后); }).start(); }Semaphore信号量控制同时访问特定资源的线程数量。常用于流量控制。// 模拟一个只有5个许可的资源池 Semaphore semaphore new Semaphore(5); ExecutorService executor Executors.newFixedThreadPool(20); for (int i 0; i 20; i) { executor.execute(() - { try { semaphore.acquire(); // 获取许可 // 访问受限资源 System.out.println(Thread.currentThread().getName() 正在使用资源); Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } finally { semaphore.release(); // 释放许可 System.out.println(Thread.currentThread().getName() 释放资源); } }); } executor.shutdown();6.2 CompletableFuture异步编程的利器在Java 8之前处理异步任务主要靠Future但它获取结果的方式是阻塞的get()。CompletableFuture是Future的增强版支持流式调用、组合异步计算、处理完成事件。// 1. 创建异步任务 CompletableFutureString future CompletableFuture.supplyAsync(() - { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } return Hello; }, executor); // 可以指定自定义的线程池默认使用ForkJoinPool.commonPool() // 2. 链式处理将上一步的结果作为输入进行转换 CompletableFutureString future2 future.thenApply(s - s World); // 3. 消费结果 future2.thenAccept(result - System.out.println(结果: result)); // 4. 组合多个Future CompletableFutureString futureA CompletableFuture.supplyAsync(() - Task A); CompletableFutureString futureB CompletableFuture.supplyAsync(() - Task B); CompletableFutureString combinedFuture futureA.thenCombine(futureB, (a, b) - a b); combinedFuture.thenAccept(System.out::println); // 输出: Task A Task B // 5. 异常处理 exceptionally CompletableFuture.supplyAsync(() - { if (true) throw new RuntimeException(出错啦); return Success; }).exceptionally(ex - { System.err.println(异常: ex.getMessage()); return Fallback Value; // 提供降级值 }).thenAccept(System.out::println); // 输出: Fallback ValueCompletableFuture极大地简化了异步编程模型是编写非阻塞、响应式代码的核心工具。6.3 线程安全设计模式与最佳实践不可变对象最简单的线程安全。如果一个对象在构造后其状态就不能被修改那么它天生就是线程安全的。String、Integer等包装类就是不可变的。对于自定义类将所有字段声明为final不提供setter方法。线程封闭不共享数据。例如使用ThreadLocal为每个线程创建变量的副本。Spring中常用它来存储当前请求的上下文信息。注意使用完ThreadLocal后一定要调用remove()方法清理尤其是在线程池环境中否则可能导致内存泄漏或数据错乱。写时复制CopyOnWriteArrayList采用此策略。每次修改时复制底层数组修改在新数组上进行最后替换引用。适合读多写少的场景。使用并发容器如前所述优先选择ConcurrentHashMap、ConcurrentLinkedQueue等。缩小同步范围只在必要的代码块上加锁减少锁的持有时间。使用读写锁ReentrantReadWriteLock允许多个读线程同时访问但写线程独占。在读远多于写的场景下能提升性能。7. 生产环境问题排查与性能调优指南理论最终要服务于实践。下面是我在多年运维中总结的一些常见问题和对策。7.1 死锁、活锁与资源耗尽死锁两个或以上线程互相等待对方持有的锁。排查工具jstack命令可以生成线程转储查看线程状态和持有的锁。在输出中搜索deadlock关键词。预防确保所有线程以固定的全局顺序获取锁。活锁线程没有阻塞但不断重复尝试某个失败的操作比如两个线程互相谦让资源导致谁都无法进行。通常需要引入随机退避机制。线程饥饿某些线程长期得不到CPU时间片或锁。检查线程优先级但Java线程优先级映射到操作系统并不精确依赖它不靠谱和锁的公平性。资源耗尽线程数爆炸无限制创建线程或CachedThreadPool使用不当。监控使用jconsole、VisualVM或Arthas监控线程数量。解决使用有界线程池和有界队列。内存泄漏线程局部变量ThreadLocal未清理、静态集合持续增长等。排查使用jmap、jhat或MAT分析堆内存快照。7.2 线程池监控与动态调参线上环境的流量是波动的固定的线程池参数可能无法适应。可以考虑实现动态调参。监控指标ThreadPoolExecutor.getPoolSize()当前池中的线程数。ThreadPoolExecutor.getActiveCount()正在执行任务的线程数。ThreadPoolExecutor.getQueue().size()队列中等待的任务数。ThreadPoolExecutor.getCompletedTaskCount()已完成的任务数。动态调整可以定时获取这些指标根据策略如队列持续增长且活跃线程数已达最大值动态调用executor.setCorePoolSize()和executor.setMaximumPoolSize()。注意调整时需要谨慎可能会引发正在执行任务线程的中断。7.3 性能瓶颈分析与工具使用CPU使用率高使用top -Hp [pid]查看进程中哪个线程的CPU占用高拿到线程ID十进制。再用jstack [pid]导出线程栈将十进制的线程ID转为十六进制在jstack输出中搜索nid0x[十六进制ID]就能定位到消耗CPU的代码行。锁竞争激烈使用jstack查看大量线程处于BLOCKED状态并等待同一个锁。可以考虑使用更细粒度的锁、无锁数据结构或减少锁的持有时间。上下文切换频繁使用vmstat或pidstat命令查看cscontext switch指标过高。过多的线程数会导致频繁的上下文切换消耗CPU。适当减少线程数或使用协程Java 19的虚拟线程。I/O等待高线程大量时间处于WAITING或TIMED_WAITING状态在jstack中显示为java.lang.Thread.State: WAITING (parking)或TIMED_WAITING (sleeping)可能是在等待网络或磁盘I/O。考虑使用异步I/ONIO或增加I/O密集型任务的线程数。我个人在实际项目中处理高并发问题的基本思路是先监控、再分析、后优化。不要凭感觉调参一定要有数据支撑。从应用日志、系统监控如PrometheusGrafana、APM工具如SkyWalking中收集数据定位到具体瓶颈是CPU、内存、I/O还是锁然后再有针对性地进行优化。并发编程的复杂性决定了没有银弹只有对原理的深刻理解和对场景的不断适配。