5. 并发编程 (Concurrency Programming)


文档摘要

Java 并发编程 (Concurrency Programming) 核心指南 摘要:Java 并发编程是构建高性能、高响应现代应用的核心技术。本文深度解析 Java 多线程基础、并发与并行的本质区别、线程安全机制、死锁预防、线程池配置及并发集合的应用。通过掌握这些核心概念与实战技巧,开发者能够有效利用多核 CPU 优势,编写出健壮且高效的并发程序。 并发与并行的区别 在深入研究 Java 并发编程之前,准确理解“并发”与“并行”的本质区别至关重要,这决定了系统架构的设计方向。 并发 (Concurrency):指在一段时间内,多个任务看似同时执行。实际上,它们可能在时间上交错执行。例如,通过时间片轮转机制在单核 CPU 上模拟同时执行多个任务,核心在于任务切换与调度。

Java 并发编程 (Concurrency Programming) 核心指南

摘要:Java 并发编程是构建高性能、高响应现代应用的核心技术。本文深度解析 Java 多线程基础、并发与并行的本质区别、线程安全机制、死锁预防、线程池配置及并发集合的应用。通过掌握这些核心概念与实战技巧,开发者能够有效利用多核 CPU 优势,编写出健壮且高效的并发程序。

1. 并发与并行的区别

在深入研究 Java 并发编程之前,准确理解“并发”与“并行”的本质区别至关重要,这决定了系统架构的设计方向。

  • 并发 (Concurrency):指在一段时间内,多个任务看似同时执行。实际上,它们可能在时间上交错执行。例如,通过时间片轮转机制在单核 CPU 上模拟同时执行多个任务,核心在于任务切换与调度
  • 并行 (Parallelism):指在同一时刻,多个任务真正地同时执行。这必须依赖多个物理处理单元(如多核 CPU)来完成,核心在于同时处理

图示如下:

2. 线程 (Threads) 基础

线程是 Java 并发编程的基本执行单元。一个进程可以包含多个线程,每个线程拥有独立的执行栈和程序计数器,但共享进程的堆内存空间与方法区。

2.1 创建线程

Java 提供了多种创建线程的方式,最常用的两种基础方式如下:

  • 继承 Thread:创建子类并重写 run() 方法,该方法包含线程需执行的核心逻辑。
class MyThread extends Thread { @Override public void run() { System.out.println("Thread " + Thread.currentThread().getName() + " is running"); } public static void main(String[] args) { MyThread thread1 = new MyThread(); thread1.start(); // 启动线程,由JVM调度执行run() MyThread thread2 = new MyThread(); thread2.start(); } }
  • 实现 Runnable 接口:实现 run() 方法,并将实例传递给 Thread 对象。
class MyRunnable implements Runnable { @Override public void run() { System.out.println("Thread " + Thread.currentThread().getName() + " is running"); } public static void main(String[] args) { MyRunnable runnable = new MyRunnable(); Thread thread1 = new Thread(runnable); thread1.start(); Thread thread2 = new Thread(runnable); thread2.start(); } }

最佳实践:推荐实现 Runnable 接口(或使用 Callable)。这不仅避免了 Java 单继承的限制,还使任务逻辑与线程控制解耦,更利于后续接入线程池。

2.2 线程的生命周期

线程从创建到销毁,会经历严格的状态流转。Java 中 Thread.State 枚举定义了以下核心状态:

  • New (新建):线程对象已创建,但尚未调用 start()
  • Runnable (可运行):包含操作系统的 Ready 和 Running 状态,线程已准备好运行,正在等待或正在占用 CPU 时间片。
  • Blocked (阻塞):线程因等待获取排他锁(如进入 synchronized 块)而被阻塞。
  • Waiting (无限期等待):线程正在等待另一个线程执行特定操作(如调用 Object.wait()Thread.join())。
  • Timed Waiting (限期等待):线程在指定时间内等待(如调用 Thread.sleep() 或带超时参数的 wait())。
  • Terminated (终止):线程执行完成或因未捕获异常而退出。

线程状态流转图:

2.3 核心线程控制方法

  • start():启动线程,JVM 会为其分配资源并调度执行 run()
  • join():阻塞当前线程,直到目标线程执行完毕。
  • sleep(long millis):使当前线程休眠指定毫秒数,不释放已持有的锁
  • yield():提示调度器当前线程愿意让出 CPU,但调度器可忽略此提示。
  • interrupt():设置线程的中断标志位,用于优雅地通知线程停止,而非强制终止。

3. 线程安全 (Thread Safety)

线程安全是指在多线程环境下,多个线程访问共享资源时,程序仍能保证数据的一致性、可见性和正确性,无需外部同步干预。

3.1 竞态条件 (Race Condition)

当多个线程读写共享数据,且最终结果依赖于线程执行的时序时,就会引发竞态条件。

class Counter { private int count = 0; // 非线程安全:count++ 并非原子操作(包含读、改、写三步) public void increment() { count++; } public int getCount() { return count; } public static void main(String[] args) throws InterruptedException { Counter counter = new Counter(); Thread[] threads = new Thread[1000]; for (int i = 0; i < threads.length; i++) { threads[i] = new Thread(() -> { for (int j = 0; j < 1000; j++) { counter.increment(); } }); threads[i].start(); } for (Thread thread : threads) { thread.join(); } // 结果通常小于 1000000,发生数据丢失 System.out.println("Count: " + counter.getCount()); } }

3.2 解决线程安全问题的核心方案

1. 同步机制 (Synchronization)

通过加锁确保同一时刻只有一个线程能访问临界区。

  • synchronized 关键字:JVM 层面的内置锁,支持修饰方法或代码块,具备可重入性。
public synchronized void increment() { count++; // 保证原子性与可见性 }
  • Lock 接口 (如 ReentrantLock):JDK 提供的显式锁,支持公平锁、中断响应及超时获取等高级特性。
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; class SafeCounter { private int count = 0; private final Lock lock = new ReentrantLock(); public void increment() { lock.lock(); try { count++; } finally { lock.unlock(); // 必须在 finally 中释放锁 } } }

2. 原子类 (Atomic Classes)

利用 CAS (Compare-And-Swap) 硬件指令实现无锁并发,性能通常优于重量级锁。

import java.util.concurrent.atomic.AtomicInteger; class AtomicCounter { private AtomicInteger count = new AtomicInteger(0); public void increment() { count.incrementAndGet(); // 原子操作 } }

3. 内存可见性保障 (volatile)

当多个线程操作共享变量时,volatile 关键字可强制线程每次都从主内存读取变量,并立即将修改刷新回主内存,防止指令重排序。注意:volatile 不保证复合操作的原子性。

4. 其他策略

  • 不可变对象 (Immutable Objects):如 Stringfinal 修饰的类,天生线程安全。
  • ThreadLocal:通过空间换时间,为每个线程提供变量的独立副本,彻底避免共享冲突。

4. 死锁 (Deadlock) 分析与预防

死锁是指两个或多个线程互相持有对方所需的资源,导致所有线程无限期阻塞的致命状态。

4.1 死锁的四个必要条件

  1. 互斥 (Mutual Exclusion):资源一次只能被一个线程占用。
  2. 占有且等待 (Hold and Wait):线程持有至少一个资源,并请求获取其他被占用的资源。
  3. 不可剥夺 (No Preemption):线程已获得的资源在未使用完前不能被强行剥夺。
  4. 循环等待 (Circular Wait):存在一个线程-资源的环形等待链。

4.2 死锁代码示例与预防

public class DeadlockExample { private static final Object lock1 = new Object(); private static final Object lock2 = new Object(); public static void main(String[] args) { new Thread(() -> { synchronized (lock1) { System.out.println("Thread 1: Holding lock 1..."); try { Thread.sleep(10); } catch (InterruptedException e) {} System.out.println("Thread 1: Waiting for lock 2..."); synchronized (lock2) { System.out.println("Thread 1: Holding lock 1 & 2..."); } } }).start(); new Thread(() -> { synchronized (lock2) { System.out.println("Thread 2: Holding lock 2..."); try { Thread.sleep(10); } catch (InterruptedException e) {} System.out.println("Thread 2: Waiting for lock 1..."); synchronized (lock1) { System.out.println("Thread 2: Holding lock 1 & 2..."); } } }).start(); } }

预防与避免策略

  • 破坏循环等待:对所有锁进行全局排序,强制所有线程按固定顺序获取锁。
  • 使用超时机制:使用 ReentrantLock.tryLock(timeout),获取失败则主动释放已持有的锁并重试。
  • 减小锁粒度:尽量缩小同步代码块的范围,降低锁冲突概率。

5. 线程池 (Thread Pools) 深度解析

频繁创建和销毁线程会消耗大量系统资源。线程池通过复用线程、控制并发数、管理任务队列,显著提升了系统性能与稳定性。

5.1 Executor 框架体系

  • Executor:顶层接口,定义了 execute(Runnable) 方法。
  • ExecutorService:扩展了生命周期管理方法(如 shutdown()submit())。
  • ThreadPoolExecutor:核心实现类,提供高度可定制的线程池配置。
  • Executors:工具类,提供创建常见线程池的工厂方法(注:阿里 Java 规范建议直接使用 ThreadPoolExecutor 以明确运行规则,避免 OOM 风险)。

5.2 ThreadPoolExecutor 核心参数

配置一个健壮的线程池,必须深刻理解以下 7 个核心参数:

  1. corePoolSize:核心线程数。即使空闲也不会被回收(除非设置 allowCoreThreadTimeOut)。
  2. maximumPoolSize:最大线程数。线程池允许创建的线程上限。
  3. keepAliveTime:非核心线程的空闲存活时间。
  4. unitkeepAliveTime 的时间单位。
  5. workQueue:阻塞队列。用于存放等待执行的任务(如 ArrayBlockingQueue, LinkedBlockingQueue)。
  6. threadFactory:线程工厂。用于自定义创建线程的名称、守护线程属性等,便于问题排查。
  7. handler:拒绝策略 (RejectedExecutionHandler)。当线程池和队列均满时,处理新提交任务的策略(默认 AbortPolicy 抛出异常,还有 CallerRunsPolicyDiscardPolicyDiscardOldestPolicy)。

5.3 线程池实战示例

import java.util.concurrent.*; public class ThreadPoolConfigExample { public static void main(String[] args) { // 推荐自定义 ThreadPoolExecutor,避免 Executors 带来的隐藏 OOM 风险 ThreadPoolExecutor executor = new ThreadPoolExecutor( 5, // corePoolSize 10, // maximumPoolSize 60L, // keepAliveTime TimeUnit.SECONDS, // unit new LinkedBlockingQueue<>(100), // workQueue (指定容量) Executors.defaultThreadFactory(), // threadFactory new ThreadPoolExecutor.CallerRunsPolicy() // handler (拒绝策略) ); for (int i = 0; i < 20; i++) { int taskNumber = i; executor.submit(() -> { System.out.println("Task " + taskNumber + " executed by " + Thread.currentThread().getName()); try { Thread.sleep(500); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }); } executor.shutdown(); try { if (!executor.awaitTermination(1, TimeUnit.MINUTES)) { executor.shutdownNow(); } } catch (InterruptedException e) { executor.shutdownNow(); Thread.currentThread().interrupt(); } } }

6. 并发集合 (Concurrent Collections)

在多线程环境下,传统的 HashMapArrayList 会导致数据错乱或死循环。java.util.concurrent 包提供了高效的线程安全集合:

  • ConcurrentHashMap:高并发哈希表。JDK 1.8 后采用 Node数组 + 链表 + 红黑树 结构,利用 CAS 和 synchronized 锁定链表头节点,实现极高的并发读写性能。
  • CopyOnWriteArrayList:写时复制列表。每次写操作都会复制底层数组,适用于读多写少的场景(如监听器列表)。
  • BlockingQueue:阻塞队列(如 ArrayBlockingQueue)。支持在队列为空时阻塞获取,在队列满时阻塞插入,是生产者-消费者模型的核心组件。
import java.util.concurrent.ConcurrentHashMap; public class ConcurrentHashMapExample { public static void main(String[] args) { ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>(); new Thread(() -> { for (int i = 0; i < 1000; i++) { map.put("key" + i, i); } }).start(); new Thread(() -> { for (int i = 0; i < 1000; i++) { map.getOrDefault("key" + i, -1); } }).start(); } }

7. 异步编程:Future、Callable 与 CompletableFuture

传统的 Runnable 无法返回执行结果,也无法抛出受检异常。Java 引入了 CallableFuture 来支持异步结果获取。

  • Callable<V>:带有返回值和异常抛出的任务接口。
  • Future<V>:代表异步计算的结果,提供 isDone()cancel() 和阻塞的 get() 方法。
import java.util.concurrent.*; public class FutureExample { public static void main(String[] args) throws Exception { ExecutorService executor = Executors.newSingleThreadExecutor(); Callable<String> task = () -> { Thread.sleep(1000); return "Task completed successfully"; }; Future<String> future = executor.submit(task); System.out.println("Task submitted, doing other work..."); // 阻塞当前线程,直到异步任务完成并获取结果 String result = future.get(); System.out.println("Result: " + result); executor.shutdown(); } }

进阶:CompletableFuture
Futureget() 方法会阻塞主线程,且不支持任务编排。Java 8 引入的 CompletableFuture 提供了强大的异步编程能力,支持链式调用、任务组合(thenApply, thenCombine, allOf)及异常处理,是构建现代响应式非阻塞应用的利器。

8. 总结

Java 并发编程是打造高吞吐、低延迟企业级应用的基石。从理解并发与并行的底层逻辑,到熟练运用线程生命周期与同步机制;从规避死锁陷阱,到精细化配置 ThreadPoolExecutor 线程池,再到掌握 ConcurrentHashMapCompletableFuture 等高级并发工具,每一个环节都考验着开发者的技术深度。

在实际工程中,“避免共享、减少锁竞争、合理复用线程” 是并发设计的黄金法则。深入掌握本文所述的 Java 并发编程核心知识,将帮助您在应对高并发、大流量场景时游刃有余,构建出真正健壮、高效的后端系统。


发布者: 作者: 转发
评论区 (0)
U