Java 并发编程 (Concurrency Programming) 核心指南 摘要:Java 并发编程是构建高性能、高响应现代应用的核心技术。本文深度解析 Java 多线程基础、并发与并行的本质区别、线程安全机制、死锁预防、线程池配置及并发集合的应用。通过掌握这些核心概念与实战技巧,开发者能够有效利用多核 CPU 优势,编写出健壮且高效的并发程序。 并发与并行的区别 在深入研究 Java 并发编程之前,准确理解“并发”与“并行”的本质区别至关重要,这决定了系统架构的设计方向。 并发 (Concurrency):指在一段时间内,多个任务看似同时执行。实际上,它们可能在时间上交错执行。例如,通过时间片轮转机制在单核 CPU 上模拟同时执行多个任务,核心在于任务切换与调度。
摘要:Java 并发编程是构建高性能、高响应现代应用的核心技术。本文深度解析 Java 多线程基础、并发与并行的本质区别、线程安全机制、死锁预防、线程池配置及并发集合的应用。通过掌握这些核心概念与实战技巧,开发者能够有效利用多核 CPU 优势,编写出健壮且高效的并发程序。
在深入研究 Java 并发编程之前,准确理解“并发”与“并行”的本质区别至关重要,这决定了系统架构的设计方向。
图示如下:
线程是 Java 并发编程的基本执行单元。一个进程可以包含多个线程,每个线程拥有独立的执行栈和程序计数器,但共享进程的堆内存空间与方法区。
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 单继承的限制,还使任务逻辑与线程控制解耦,更利于后续接入线程池。
线程从创建到销毁,会经历严格的状态流转。Java 中 Thread.State 枚举定义了以下核心状态:
start()。synchronized 块)而被阻塞。Object.wait() 或 Thread.join())。Thread.sleep() 或带超时参数的 wait())。线程状态流转图:
start():启动线程,JVM 会为其分配资源并调度执行 run()。join():阻塞当前线程,直到目标线程执行完毕。sleep(long millis):使当前线程休眠指定毫秒数,不释放已持有的锁。yield():提示调度器当前线程愿意让出 CPU,但调度器可忽略此提示。interrupt():设置线程的中断标志位,用于优雅地通知线程停止,而非强制终止。线程安全是指在多线程环境下,多个线程访问共享资源时,程序仍能保证数据的一致性、可见性和正确性,无需外部同步干预。
当多个线程读写共享数据,且最终结果依赖于线程执行的时序时,就会引发竞态条件。
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()); } }
通过加锁确保同一时刻只有一个线程能访问临界区。
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 中释放锁 } } }
利用 CAS (Compare-And-Swap) 硬件指令实现无锁并发,性能通常优于重量级锁。
import java.util.concurrent.atomic.AtomicInteger; class AtomicCounter { private AtomicInteger count = new AtomicInteger(0); public void increment() { count.incrementAndGet(); // 原子操作 } }
volatile)当多个线程操作共享变量时,volatile 关键字可强制线程每次都从主内存读取变量,并立即将修改刷新回主内存,防止指令重排序。注意:volatile 不保证复合操作的原子性。
String 或 final 修饰的类,天生线程安全。死锁是指两个或多个线程互相持有对方所需的资源,导致所有线程无限期阻塞的致命状态。
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),获取失败则主动释放已持有的锁并重试。频繁创建和销毁线程会消耗大量系统资源。线程池通过复用线程、控制并发数、管理任务队列,显著提升了系统性能与稳定性。
Executor:顶层接口,定义了 execute(Runnable) 方法。ExecutorService:扩展了生命周期管理方法(如 shutdown()、submit())。ThreadPoolExecutor:核心实现类,提供高度可定制的线程池配置。Executors:工具类,提供创建常见线程池的工厂方法(注:阿里 Java 规范建议直接使用 ThreadPoolExecutor 以明确运行规则,避免 OOM 风险)。配置一个健壮的线程池,必须深刻理解以下 7 个核心参数:
corePoolSize:核心线程数。即使空闲也不会被回收(除非设置 allowCoreThreadTimeOut)。maximumPoolSize:最大线程数。线程池允许创建的线程上限。keepAliveTime:非核心线程的空闲存活时间。unit:keepAliveTime 的时间单位。workQueue:阻塞队列。用于存放等待执行的任务(如 ArrayBlockingQueue, LinkedBlockingQueue)。threadFactory:线程工厂。用于自定义创建线程的名称、守护线程属性等,便于问题排查。handler:拒绝策略 (RejectedExecutionHandler)。当线程池和队列均满时,处理新提交任务的策略(默认 AbortPolicy 抛出异常,还有 CallerRunsPolicy、DiscardPolicy、DiscardOldestPolicy)。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(); } } }
在多线程环境下,传统的 HashMap 或 ArrayList 会导致数据错乱或死循环。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(); } }
传统的 Runnable 无法返回执行结果,也无法抛出受检异常。Java 引入了 Callable 和 Future 来支持异步结果获取。
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(); } }
进阶:CompletableFutureFuture 的 get() 方法会阻塞主线程,且不支持任务编排。Java 8 引入的 CompletableFuture 提供了强大的异步编程能力,支持链式调用、任务组合(thenApply, thenCombine, allOf)及异常处理,是构建现代响应式非阻塞应用的利器。
Java 并发编程是打造高吞吐、低延迟企业级应用的基石。从理解并发与并行的底层逻辑,到熟练运用线程生命周期与同步机制;从规避死锁陷阱,到精细化配置 ThreadPoolExecutor 线程池,再到掌握 ConcurrentHashMap 与 CompletableFuture 等高级并发工具,每一个环节都考验着开发者的技术深度。
在实际工程中,“避免共享、减少锁竞争、合理复用线程” 是并发设计的黄金法则。深入掌握本文所述的 Java 并发编程核心知识,将帮助您在应对高并发、大流量场景时游刃有余,构建出真正健壮、高效的后端系统。