1. 引言
在当今多核处理器普及的时代,多线程编程已经成为 Java 开发者必须掌握的核心技能之一。无论是提升程序性能、优化用户体验,还是处理高并发业务场景,多线程都扮演着至关重要的角色。本文将带你从多线程的基础原理出发,逐步深入到实战应用,帮助你系统性地掌握 Java 多线程编程。
2. 线程基础概念
2.1 什么是线程
线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
2.2 进程与线程的区别
- 进程:操作系统资源分配的基本单位,拥有独立的地址空间。
- 线程:CPU 调度的基本单位,共享进程的地址空间和资源。
简单来说,进程是"资源容器",线程是"执行单元"。多个线程共享同一个进程的内存空间,因此线程间的通信比进程间通信更高效,但也带来了数据同步的挑战。
2.3 线程的生命周期
Java 线程在其生命周期中会经历以下几种状态:
- 新建(NEW):线程对象创建后,尚未启动。
- 就绪(RUNNABLE):线程已启动,等待 CPU 调度。
- 阻塞(BLOCKED):线程等待获取锁。
- 等待(WAITING):线程无限期等待其他线程。
- 超时等待(TIMED_WAITING):线程在指定时间内等待。
- 终止(TERMINATED):线程执行完毕。
下面是线程六种状态之间的转换关系图:
#mermaid-svg-2D6NBrD2PqyjNKp6{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-2D6NBrD2PqyjNKp6 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-2D6NBrD2PqyjNKp6 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-2D6NBrD2PqyjNKp6 .error-icon{fill:#552222;}#mermaid-svg-2D6NBrD2PqyjNKp6 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-2D6NBrD2PqyjNKp6 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-2D6NBrD2PqyjNKp6 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-2D6NBrD2PqyjNKp6 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-2D6NBrD2PqyjNKp6 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-2D6NBrD2PqyjNKp6 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-2D6NBrD2PqyjNKp6 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-2D6NBrD2PqyjNKp6 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-2D6NBrD2PqyjNKp6 .marker.cross{stroke:#333333;}#mermaid-svg-2D6NBrD2PqyjNKp6 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-2D6NBrD2PqyjNKp6 p{margin:0;}#mermaid-svg-2D6NBrD2PqyjNKp6 defs #statediagram-barbEnd{fill:#333333;stroke:#333333;}#mermaid-svg-2D6NBrD2PqyjNKp6 g.stateGroup text{fill:#9370DB;stroke:none;font-size:10px;}#mermaid-svg-2D6NBrD2PqyjNKp6 g.stateGroup text{fill:#333;stroke:none;font-size:10px;}#mermaid-svg-2D6NBrD2PqyjNKp6 g.stateGroup .state-title{font-weight:bolder;fill:#131300;}#mermaid-svg-2D6NBrD2PqyjNKp6 g.stateGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-2D6NBrD2PqyjNKp6 g.stateGroup line{stroke:#333333;stroke-width:1;}#mermaid-svg-2D6NBrD2PqyjNKp6 .transition{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-2D6NBrD2PqyjNKp6 .stateGroup .composit{fill:white;border-bottom:1px;}#mermaid-svg-2D6NBrD2PqyjNKp6 .stateGroup .alt-composit{fill:#e0e0e0;border-bottom:1px;}#mermaid-svg-2D6NBrD2PqyjNKp6 .state-note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-2D6NBrD2PqyjNKp6 .state-note text{fill:black;stroke:none;font-size:10px;}#mermaid-svg-2D6NBrD2PqyjNKp6 .stateLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-2D6NBrD2PqyjNKp6 .edgeLabel .label rect{fill:#ECECFF;opacity:0.5;}#mermaid-svg-2D6NBrD2PqyjNKp6 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-2D6NBrD2PqyjNKp6 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-2D6NBrD2PqyjNKp6 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-2D6NBrD2PqyjNKp6 .edgeLabel .label text{fill:#333;}#mermaid-svg-2D6NBrD2PqyjNKp6 .label div .edgeLabel{color:#333;}#mermaid-svg-2D6NBrD2PqyjNKp6 .stateLabel text{fill:#131300;font-size:10px;font-weight:bold;}#mermaid-svg-2D6NBrD2PqyjNKp6 .node circle.state-start{fill:#333333;stroke:#333333;}#mermaid-svg-2D6NBrD2PqyjNKp6 .node .fork-join{fill:#333333;stroke:#333333;}#mermaid-svg-2D6NBrD2PqyjNKp6 .node circle.state-end{fill:#9370DB;stroke:white;stroke-width:1.5;}#mermaid-svg-2D6NBrD2PqyjNKp6 .end-state-inner{fill:white;stroke-width:1.5;}#mermaid-svg-2D6NBrD2PqyjNKp6 .node rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-2D6NBrD2PqyjNKp6 .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-2D6NBrD2PqyjNKp6 #statediagram-barbEnd{fill:#333333;}#mermaid-svg-2D6NBrD2PqyjNKp6 .statediagram-cluster rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-2D6NBrD2PqyjNKp6 .cluster-label,#mermaid-svg-2D6NBrD2PqyjNKp6 .nodeLabel{color:#131300;}#mermaid-svg-2D6NBrD2PqyjNKp6 .statediagram-cluster rect.outer{rx:5px;ry:5px;}#mermaid-svg-2D6NBrD2PqyjNKp6 .statediagram-state .divider{stroke:#9370DB;}#mermaid-svg-2D6NBrD2PqyjNKp6 .statediagram-state .title-state{rx:5px;ry:5px;}#mermaid-svg-2D6NBrD2PqyjNKp6 .statediagram-cluster.statediagram-cluster .inner{fill:white;}#mermaid-svg-2D6NBrD2PqyjNKp6 .statediagram-cluster.statediagram-cluster-alt .inner{fill:#f0f0f0;}#mermaid-svg-2D6NBrD2PqyjNKp6 .statediagram-cluster .inner{rx:0;ry:0;}#mermaid-svg-2D6NBrD2PqyjNKp6 .statediagram-state rect.basic{rx:5px;ry:5px;}#mermaid-svg-2D6NBrD2PqyjNKp6 .statediagram-state rect.divider{stroke-dasharray:10,10;fill:#f0f0f0;}#mermaid-svg-2D6NBrD2PqyjNKp6 .note-edge{stroke-dasharray:5;}#mermaid-svg-2D6NBrD2PqyjNKp6 .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-2D6NBrD2PqyjNKp6 .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-2D6NBrD2PqyjNKp6 .statediagram-note text{fill:black;}#mermaid-svg-2D6NBrD2PqyjNKp6 .statediagram-note .nodeLabel{color:black;}#mermaid-svg-2D6NBrD2PqyjNKp6 .statediagram .edgeLabel{color:red;}#mermaid-svg-2D6NBrD2PqyjNKp6 #dependencyStart,#mermaid-svg-2D6NBrD2PqyjNKp6 #dependencyEnd{fill:#333333;stroke:#333333;stroke-width:1;}#mermaid-svg-2D6NBrD2PqyjNKp6 .statediagramTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-2D6NBrD2PqyjNKp6 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 创建线程对象
调用 start()
等待获取锁(synchronized)
获取到锁
调用 wait()/join()/LockSupport.park()
被 notify()/notifyAll()/join 完成/unpark()
调用 sleep()/wait(timeout)/join(timeout)/LockSupport.parkNanos()
超时或被唤醒
run() 执行完毕或抛出未捕获异常
带超时等待
取消超时等待
NEW
RUNNABLE
BLOCKED
WAITING
TIMED_WAITING
TERMINATED
各状态转换的触发条件说明:
- NEW → RUNNABLE :调用线程对象的
start()方法后,线程进入就绪状态,等待 CPU 调度执行。 - RUNNABLE → BLOCKED :线程尝试进入
synchronized代码块或方法时,若锁被其他线程持有,则进入阻塞状态等待获取锁。 - BLOCKED → RUNNABLE:持有锁的线程释放锁后,阻塞线程竞争到锁,重新回到就绪状态等待 CPU 调度。
- RUNNABLE → WAITING :调用
wait()、join()或LockSupport.park()时,线程无限期等待其他线程的通知或完成。 - WAITING → RUNNABLE :被
notify()/notifyAll()唤醒、join()的线程执行完毕,或LockSupport.unpark()解除挂起。 - RUNNABLE → TIMED_WAITING :调用
sleep(ms)、wait(timeout)、join(timeout)或LockSupport.parkNanos()时,线程在指定时间内等待。 - TIMED_WAITING → RUNNABLE:等待超时或被其他线程唤醒后,线程重新回到就绪状态。
- RUNNABLE → TERMINATED :
run()方法正常执行完毕,或抛出未捕获的异常导致线程终止。
3. 创建线程的三种方式
3.1 继承 Thread 类
java
public class MyThread extends Thread {
@Override
public void run() {
System.out.println("线程运行中:" + Thread.currentThread().getName());
}
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start();
}
}
3.2 实现 Runnable 接口
java
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("线程运行中:" + Thread.currentThread().getName());
}
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnable());
thread.start();
}
}
3.3 使用 Callable 和 Future
java
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
public class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
return "线程执行结果";
}
public static void main(String[] args) throws Exception {
FutureTask<String> task = new FutureTask<>(new MyCallable());
Thread thread = new Thread(task);
thread.start();
String result = task.get();
System.out.println("获取结果:" + result);
}
}
推荐使用 Runnable 或 Callable 方式,因为 Java 是单继承,实现接口可以保留类的继承能力,同时更符合面向对象的设计原则。
4. 线程同步与锁机制
4.1 为什么需要线程同步
当多个线程同时访问共享数据时,可能会产生数据不一致的问题。例如,多个线程同时对同一个变量进行自增操作,由于操作不是原子性的,最终结果可能小于预期值。
4.2 synchronized 关键字
synchronized 是 Java 内置的同步机制,可以修饰方法或代码块:
java
public class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
4.3 Lock 接口
Java 5 之后提供了更灵活的 Lock 接口:
java
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class CounterWithLock {
private int count = 0;
private final Lock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
}
4.4 volatile 关键字
volatile 保证变量的可见性,但不保证原子性:
java
public class FlagExample {
private volatile boolean flag = false;
public void setFlag() {
flag = true;
}
public boolean isFlag() {
return flag;
}
}
5. 线程通信
5.1 wait/notify 机制
java
public class ProducerConsumer {
private final List<Integer> queue = new ArrayList<>();
private final int MAX_SIZE = 10;
public synchronized void produce(int value) throws InterruptedException {
while (queue.size() == MAX_SIZE) {
wait();
}
queue.add(value);
notifyAll();
}
public synchronized int consume() throws InterruptedException {
while (queue.isEmpty()) {
wait();
}
int value = queue.remove(0);
notifyAll();
return value;
}
}
5.2 Condition 接口
使用 Condition 可以实现更精细的线程控制:
java
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
import java.util.LinkedList;
import java.util.Queue;
public class ConditionProducerConsumer {
private final Queue<Integer> queue = new LinkedList<>();
private final int MAX_SIZE = 5;
private final ReentrantLock lock = new ReentrantLock();
// 使用两个独立的 Condition,实现精确唤醒
private final Condition notFull = lock.newCondition(); // 队列未满条件
private final Condition notEmpty = lock.newCondition(); // 队列非空条件
// 生产者:向队列中添加数据
public void produce(int value) throws InterruptedException {
lock.lock();
try {
// 队列满时,生产者等待 notFull 条件
while (queue.size() == MAX_SIZE) {
System.out.println("队列已满,生产者等待...");
notFull.await(); // 只挂起生产者线程
}
queue.offer(value);
System.out.println("生产者生产:" + value + ",当前队列大小:" + queue.size());
// 生产成功后,精确唤醒一个消费者线程
notEmpty.signal();
} finally {
lock.unlock();
}
}
// 消费者:从队列中取出数据
public int consume() throws InterruptedException {
lock.lock();
try {
// 队列空时,消费者等待 notEmpty 条件
while (queue.isEmpty()) {
System.out.println("队列为空,消费者等待...");
notEmpty.await(); // 只挂起消费者线程
}
int value = queue.poll();
System.out.println("消费者消费:" + value + ",当前队列大小:" + queue.size());
// 消费成功后,精确唤醒一个生产者线程
notFull.signal();
return value;
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
ConditionProducerConsumer pc = new ConditionProducerConsumer();
// 启动生产者线程
Thread producer = new Thread(() -> {
for (int i = 1; i <= 10; i++) {
try {
pc.produce(i);
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}, "生产者线程");
// 启动消费者线程
Thread consumer = new Thread(() -> {
for (int i = 1; i <= 10; i++) {
try {
pc.consume();
Thread.sleep(200);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}, "消费者线程");
producer.start();
consumer.start();
}
}
Condition 相比 wait/notify 的优势:
- 多条件精确唤醒 :Condition 可以为不同的等待条件创建独立的 Condition 对象(如
notFull和notEmpty),生产者只等待notFull,消费者只等待notEmpty,唤醒时也精确针对目标线程。而wait/notify只有一个等待队列,notifyAll()会唤醒所有线程,存在大量无效唤醒和竞争。 - 支持中断与超时 :Condition 的
await(long time, TimeUnit unit)支持超时等待,awaitUninterruptibly()支持不可中断等待,比wait更灵活。 - 与 Lock 配合更强大 :Condition 必须与
ReentrantLock配合使用,可以结合tryLock、公平锁等高级特性,实现更精细的并发控制。
6. 线程池
6.1 为什么使用线程池
频繁创建和销毁线程会带来较大的性能开销。线程池通过复用线程,减少了线程创建和销毁的开销,同时可以控制并发线程数量,避免资源耗尽。
6.2 创建线程池
java
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolExample {
public static void main(String[] args) {
// 创建固定大小的线程池
ExecutorService executor = Executors.newFixedThreadPool(5);
for (int i = 0; i < 10; i++) {
executor.execute(() -> {
System.out.println("任务执行:" + Thread.currentThread().getName());
});
}
executor.shutdown();
}
}
6.2.1 四种常用线程池对比
Executors 工具类提供了多种便捷的线程池创建方法,下面通过表格对比四种最常用的线程池:
| 线程池类型 | 适用场景 | 核心参数 | 注意事项 |
|---|---|---|---|
| newFixedThreadPool(n) | 任务数量固定、负载稳定的场景,如固定数量的后台任务 | 固定线程数 n,无最大线程数限制(等于核心线程数),使用无界 LinkedBlockingQueue | 队列无界,任务积压时可能占用大量内存;线程数固定,无法弹性伸缩 |
| newCachedThreadPool() | 大量短期异步任务、任务执行时间短的场景 | 核心线程数为 0,最大线程数为 Integer.MAX_VALUE,空闲线程 60 秒回收,使用 SynchronousQueue | 线程数可无限增长,突发高并发时可能创建过多线程导致资源耗尽 |
| newSingleThreadExecutor() | 需要保证任务顺序执行、串行处理的场景 | 单线程,使用无界 LinkedBlockingQueue | 只有一个线程,若任务阻塞则后续任务全部排队等待;队列无界同样存在内存风险 |
| newScheduledThreadPool(n) | 需要定时执行、延迟执行或周期性任务的场景 | 固定核心线程数 n,使用 DelayedWorkQueue | 适合调度类任务,但若任务执行时间超过调度周期,会阻塞后续调度 |
为什么推荐使用 ThreadPoolExecutor 手动创建线程池?
Executors 提供的便捷方法虽然使用简单,但存在以下隐患:
- 无界队列风险:newFixedThreadPool 和 newSingleThreadExecutor 使用无界 LinkedBlockingQueue,当任务积压过多时会占用大量内存,甚至引发 OOM。
- 线程数失控风险:newCachedThreadPool 的最大线程数为 Integer.MAX_VALUE,高并发下可能创建海量线程,导致资源耗尽。
- 参数不可控:Executors 封装了内部参数,开发者无法自定义核心线程数、队列类型、拒绝策略等关键配置。
因此,推荐直接使用 ThreadPoolExecutor 手动创建线程池,通过显式指定核心线程数、最大线程数、队列容量和拒绝策略,可以更精细地控制线程池行为,避免上述隐患,也更符合生产环境的可靠性要求。
6.3 ThreadPoolExecutor 详解
推荐直接使用 ThreadPoolExecutor 来创建线程池,以便更精细地控制参数:
java
import java.util.concurrent.*;
public class ThreadPoolExecutorExample {
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, // 核心线程数
5, // 最大线程数
60L, // 空闲线程存活时间
TimeUnit.SECONDS, // 时间单位
new ArrayBlockingQueue<>(10), // 任务队列
new ThreadPoolExecutor.AbortPolicy() // 拒绝策略
);
executor.execute(() -> System.out.println("任务执行中"));
executor.shutdown();
}
}
四种拒绝策略对比:
当线程池的任务队列已满且线程数达到最大值时,新提交的任务会触发拒绝策略。ThreadPoolExecutor 提供了四种内置的拒绝策略:
| 拒绝策略 | 行为 | 适用场景 | 注意事项 |
|---|---|---|---|
| AbortPolicy(默认) | 直接抛出 RejectedExecutionException 异常 |
任务不允许丢失、需要及时感知提交失败的场景 | 默认策略,异常需调用方捕获处理,否则可能导致任务中断 |
| CallerRunsPolicy | 由提交任务的线程(调用者)自己执行该任务 | 希望降低任务丢弃率、且能接受任务在调用线程中执行的场景 | 会阻塞调用线程,可能影响调用方的响应速度,但不会丢失任务 |
| DiscardPolicy | 静默丢弃被拒绝的任务,不抛异常 | 允许丢弃部分任务、对任务完整性要求不高的场景 | 任务被静默丢弃,无任何提示,可能造成数据丢失 |
| DiscardOldestPolicy | 丢弃队列中最旧(队首)的任务,然后重新尝试提交新任务 | 追求最新任务优先执行、可接受丢弃旧任务的场景 | 会丢弃队列中等待最久的任务,适合消息时效性要求高的场景 |
代码示例:
java
import java.util.concurrent.*;
public class RejectedPolicyExample {
public static void main(String[] args) {
// 1. AbortPolicy:默认策略,队列满时直接抛出异常
ThreadPoolExecutor abortExecutor = new ThreadPoolExecutor(
1, 1, 0L, TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<>(1),
new ThreadPoolExecutor.AbortPolicy()
);
try {
abortExecutor.execute(() -> System.out.println("任务1执行"));
abortExecutor.execute(() -> System.out.println("任务2执行"));
abortExecutor.execute(() -> System.out.println("任务3执行")); // 触发拒绝
} catch (RejectedExecutionException e) {
System.out.println("AbortPolicy:任务被拒绝,抛出异常:" + e.getMessage());
} finally {
abortExecutor.shutdown();
}
// 2. CallerRunsPolicy:由提交任务的线程(主线程)自己执行
ThreadPoolExecutor callerRunsExecutor = new ThreadPoolExecutor(
1, 1, 0L, TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<>(1),
new ThreadPoolExecutor.CallerRunsPolicy()
);
for (int i = 1; i <= 3; i++) {
final int taskId = i;
callerRunsExecutor.execute(() ->
System.out.println("CallerRunsPolicy:任务" + taskId + "由" + Thread.currentThread().getName() + "执行"));
}
callerRunsExecutor.shutdown();
// 3. DiscardPolicy:静默丢弃被拒绝的任务
ThreadPoolExecutor discardExecutor = new ThreadPoolExecutor(
1, 1, 0L, TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<>(1),
new ThreadPoolExecutor.DiscardPolicy()
);
for (int i = 1; i <= 3; i++) {
final int taskId = i;
discardExecutor.execute(() ->
System.out.println("DiscardPolicy:任务" + taskId + "执行"));
}
discardExecutor.shutdown();
System.out.println("DiscardPolicy:被拒绝的任务被静默丢弃,无异常抛出");
// 4. DiscardOldestPolicy:丢弃队列中最旧的任务,再尝试提交新任务
ThreadPoolExecutor discardOldestExecutor = new ThreadPoolExecutor(
1, 1, 0L, TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<>(1),
new ThreadPoolExecutor.DiscardOldestPolicy()
);
for (int i = 1; i <= 3; i++) {
final int taskId = i;
discardOldestExecutor.execute(() -> {
try {
Thread.sleep(100);
System.out.println("DiscardOldestPolicy:任务" + taskId + "执行");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
discardOldestExecutor.shutdown();
}
}
选择建议:
- 默认使用 AbortPolicy:让任务提交失败及时暴露,便于调用方感知并做补偿处理,是生产环境最稳妥的选择。
- 对任务完整性要求高、可接受调用线程执行:使用 CallerRunsPolicy,通过调用方线程执行来降低任务丢弃率,但要注意调用线程可能被阻塞。
- 允许丢弃部分任务、追求吞吐量:使用 DiscardPolicy,静默丢弃不重要的任务。
- 消息时效性要求高、可接受丢弃旧任务:使用 DiscardOldestPolicy,优先保证最新任务被执行。
完整实战示例:
下面通过一个完整的实战示例,演示如何使用 ThreadPoolExecutor 手动创建线程池,并展示任务提交、执行结果获取和优雅关闭的完整流程:
java
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
public class ThreadPoolPractice {
public static void main(String[] args) throws InterruptedException, ExecutionException {
// 1. 手动创建线程池,显式配置各项参数
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, // 核心线程数:常驻线程
4, // 最大线程数:高峰期最多创建的线程数
30L, // 空闲线程存活时间
TimeUnit.SECONDS, // 时间单位
new ArrayBlockingQueue<>(3),// 有界任务队列:容量为 3
new ThreadPoolExecutor.AbortPolicy() // 拒绝策略:队列满且线程达上限时抛异常
);
// 2. 使用 AtomicInteger 统计成功执行的任务数
AtomicInteger successCount = new AtomicInteger(0);
// 3. 提交 6 个 Callable 任务,通过 Future 获取执行结果
// 核心线程 2 个 + 队列 3 个 = 前 5 个任务可被接受,第 6 个任务触发拒绝策略
Future<Integer>[] futures = new Future[6];
for (int i = 1; i <= 6; i++) {
final int taskId = i;
try {
futures[i - 1] = executor.submit(() -> {
System.out.println("任务 " + taskId + " 开始执行,线程:"
+ Thread.currentThread().getName());
Thread.sleep(500); // 模拟耗时任务
System.out.println("任务 " + taskId + " 执行完成");
return taskId * 10; // 返回计算结果
});
} catch (RejectedExecutionException e) {
System.out.println("任务 " + taskId + " 被拒绝:" + e.getMessage());
}
}
// 4. 获取并汇总各任务的执行结果
int total = 0;
for (Future<Integer> future : futures) {
if (future != null) {
try {
total += future.get(); // 阻塞等待任务完成并获取结果
successCount.incrementAndGet();
} catch (ExecutionException e) {
System.err.println("任务执行异常:" + e.getCause());
}
}
}
System.out.println("成功执行任务数:" + successCount.get() + ",结果总和:" + total);
// 5. 优雅关闭线程池
executor.shutdown(); // 不再接受新任务,等待已提交任务执行完毕
try {
// 最多等待 10 秒,超时则强制关闭
if (!executor.awaitTermination(10, TimeUnit.SECONDS)) {
System.out.println("任务未在限定时间内完成,强制关闭线程池");
executor.shutdownNow(); // 强制中断未执行的任务
}
} catch (InterruptedException e) {
executor.shutdownNow();
Thread.currentThread().interrupt();
}
System.out.println("线程池已优雅关闭,程序结束");
}
}
代码要点说明:
- 参数配置:核心线程数 2、最大线程数 4、有界队列容量 3,因此最多可同时容纳 5 个任务(2 个执行中 + 3 个排队),第 6 个任务会触发 AbortPolicy 拒绝策略。
- 任务提交 :使用
submit(Callable)提交带返回值的任务,返回Future对象用于获取执行结果。 - 结果获取 :
future.get()会阻塞当前线程直到任务完成,可捕获ExecutionException处理任务内部异常。 - 优雅关闭 :
shutdown()停止接收新任务但等待已提交任务完成,awaitTermination()设置最长等待时间,超时后用shutdownNow()强制中断,避免线程池无法正常退出。
7. 并发工具类
7.1 CountDownLatch
用于等待多个线程完成任务:
java
import java.util.concurrent.CountDownLatch;
public class CountDownLatchExample {
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(3);
for (int i = 0; i < 3; i++) {
new Thread(() -> {
System.out.println("子任务完成");
latch.countDown();
}).start();
}
latch.await();
System.out.println("所有子任务已完成,主线程继续执行");
}
}
7.2 CyclicBarrier
用于让一组线程互相等待:
java
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierExample {
public static void main(String[] args) {
CyclicBarrier barrier = new CyclicBarrier(3, () -> {
System.out.println("所有线程已到达屏障点");
});
for (int i = 0; i < 3; i++) {
new Thread(() -> {
try {
System.out.println("线程准备就绪");
barrier.await();
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
}
}
7.3 Semaphore
用于控制同时访问的线程数量:
java
import java.util.concurrent.Semaphore;
public class SemaphoreExample {
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(2);
for (int i = 0; i < 5; i++) {
new Thread(() -> {
try {
semaphore.acquire();
System.out.println("线程获取许可,执行任务");
Thread.sleep(1000);
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
}
8. 实战案例:多线程下载器
下面通过一个完整的实战案例,综合运用前面学到的知识:
java
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
public class MultiThreadDownloader {
private static final int THREAD_COUNT = 4;
private static final AtomicInteger downloadedSize = new AtomicInteger(0);
private static final int TOTAL_SIZE = 1000;
public static void main(String[] args) throws InterruptedException {
ExecutorService executor = Executors.newFixedThreadPool(THREAD_COUNT);
CountDownLatch latch = new CountDownLatch(THREAD_COUNT);
for (int i = 0; i < THREAD_COUNT; i++) {
final int threadId = i;
executor.execute(() -> {
try {
// 模拟下载任务
int chunkSize = TOTAL_SIZE / THREAD_COUNT;
for (int j = 0; j < chunkSize; j++) {
downloadedSize.incrementAndGet();
Thread.sleep(1);
}
System.out.println("线程 " + threadId + " 下载完成");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
latch.countDown();
}
});
}
latch.await();
executor.shutdown();
System.out.println("下载完成,总大小:" + downloadedSize.get());
}
}
9. 常见问题与最佳实践
9.1 死锁的避免
死锁是多线程编程中常见的问题,可以通过以下方式避免:
- 按固定顺序获取锁。
- 使用 tryLock 并设置超时时间。
- 尽量减少锁的持有时间。
下面通过一个典型的死锁示例,直观展示两个线程互相持有对方所需锁的场景:
java
public class DeadlockExample {
private static final Object lockA = new Object();
private static final Object lockB = new Object();
public static void main(String[] args) {
// 线程 1:先持有 lockA,再尝试获取 lockB
Thread thread1 = new Thread(() -> {
synchronized (lockA) {
System.out.println("线程1 持有 lockA,等待 lockB...");
try { Thread.sleep(100); } catch (InterruptedException e) { }
synchronized (lockB) { // 此时线程2 已持有 lockB,线程1 阻塞等待
System.out.println("线程1 获取到 lockB");
}
}
});
// 线程 2:先持有 lockB,再尝试获取 lockA
Thread thread2 = new Thread(() -> {
synchronized (lockB) {
System.out.println("线程2 持有 lockB,等待 lockA...");
try { Thread.sleep(100); } catch (InterruptedException e) { }
synchronized (lockA) { // 此时线程1 已持有 lockA,线程2 阻塞等待
System.out.println("线程2 获取到 lockA");
}
}
});
thread1.start();
thread2.start();
// 两个线程互相等待对方释放锁,程序将永远卡住,形成死锁
}
}
如何避免死锁?
方式一:按固定顺序获取锁
让所有线程都按照相同的顺序(如先 lockA 后 lockB)获取锁,即可打破循环等待:
java
public class FixedOrderExample {
private static final Object lockA = new Object();
private static final Object lockB = new Object();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
synchronized (lockA) {
synchronized (lockB) {
System.out.println("线程1 按固定顺序获取锁成功");
}
}
});
Thread thread2 = new Thread(() -> {
synchronized (lockA) { // 与线程1 保持相同的加锁顺序
synchronized (lockB) {
System.out.println("线程2 按固定顺序获取锁成功");
}
}
});
thread1.start();
thread2.start();
// 两个线程都先获取 lockA 再获取 lockB,永远不会出现互相等待
}
}
方式二:使用 tryLock 并设置超时时间
使用 Lock 接口的 tryLock 方法,在指定时间内获取不到锁就放弃,避免无限期等待:
java
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.TimeUnit;
public class TryLockExample {
private static final Lock lockA = new ReentrantLock();
private static final Lock lockB = new ReentrantLock();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
try {
// 尝试获取 lockA,最多等待 1 秒
if (lockA.tryLock(1, TimeUnit.SECONDS)) {
try {
System.out.println("线程1 获取 lockA");
Thread.sleep(100);
// 尝试获取 lockB,获取不到则放弃,避免死锁
if (lockB.tryLock(1, TimeUnit.SECONDS)) {
try {
System.out.println("线程1 获取 lockB,执行任务");
} finally {
lockB.unlock();
}
} else {
System.out.println("线程1 获取 lockB 超时,放弃执行");
}
} finally {
lockA.unlock();
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
Thread thread2 = new Thread(() -> {
try {
if (lockB.tryLock(1, TimeUnit.SECONDS)) {
try {
System.out.println("线程2 获取 lockB");
Thread.sleep(100);
if (lockA.tryLock(1, TimeUnit.SECONDS)) {
try {
System.out.println("线程2 获取 lockA,执行任务");
} finally {
lockA.unlock();
}
} else {
System.out.println("线程2 获取 lockA 超时,放弃执行");
}
} finally {
lockB.unlock();
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
thread1.start();
thread2.start();
// tryLock 超时后主动放弃,线程不会无限期阻塞,从而避免死锁
}
}
关键点总结:
- 死锁产生的四个必要条件:互斥、持有并等待、不可剥夺、循环等待,打破任意一个即可避免死锁。
- 固定锁顺序:让所有线程按相同顺序加锁,直接消除循环等待。
- tryLock 超时:获取不到锁时主动放弃并释放已持有的锁,避免无限期阻塞。
- 尽量减少锁的持有时间:只在临界区持有锁,不要在持锁时执行耗时操作。
9.3 原子类与锁的性能对比
在高并发自增场景下,AtomicInteger 与 synchronized 的性能差异非常明显。下面通过一个简单的基准测试来直观对比:
java
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.CountDownLatch;
public class AtomicVsSyncBenchmark {
private static final int THREAD_COUNT = 8;
private static final int INCREMENT_COUNT = 1_000_000;
public static void main(String[] args) throws InterruptedException {
// 方式一:使用 AtomicInteger(CAS 无锁)
AtomicInteger atomicCount = new AtomicInteger(0);
long atomicTime = runWithAtomic(atomicCount);
System.out.println("AtomicInteger 耗时:" + atomicTime + " ms,结果:" + atomicCount.get());
// 方式二:使用 synchronized(悲观锁)
SyncCounter syncCounter = new SyncCounter();
long syncTime = runWithSynchronized(syncCounter);
System.out.println("synchronized 耗时:" + syncTime + " ms,结果:" + syncCounter.getCount());
}
private static long runWithAtomic(AtomicInteger count) throws InterruptedException {
long start = System.currentTimeMillis();
CountDownLatch latch = new CountDownLatch(THREAD_COUNT);
for (int i = 0; i < THREAD_COUNT; i++) {
new Thread(() -> {
for (int j = 0; j < INCREMENT_COUNT; j++) {
count.incrementAndGet(); // CAS 自旋,无锁竞争
}
latch.countDown();
}).start();
}
latch.await();
return System.currentTimeMillis() - start;
}
private static long runWithSynchronized(SyncCounter counter) throws InterruptedException {
long start = System.currentTimeMillis();
CountDownLatch latch = new CountDownLatch(THREAD_COUNT);
for (int i = 0; i < THREAD_COUNT; i++) {
new Thread(() -> {
for (int j = 0; j < INCREMENT_COUNT; j++) {
counter.increment(); // 每次自增都需要获取锁
}
latch.countDown();
}).start();
}
latch.await();
return System.currentTimeMillis() - start;
}
static class SyncCounter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
}
性能差异分析:
- AtomicInteger:基于 CAS(Compare-And-Swap)无锁实现,自增操作在 CPU 层面完成,没有线程阻塞和上下文切换,高并发下性能通常优于 synchronized。
- synchronized:属于悲观锁,每次自增都需要获取和释放锁,存在锁竞争、线程阻塞和上下文切换的开销,并发越高开销越大。
选择建议:
- 简单计数器、累加器、统计类场景 :优先使用
AtomicInteger、AtomicLong等原子类,性能更好且代码简洁。 - 需要保护多个变量或复杂业务逻辑 :使用
synchronized或Lock,因为原子类只能保证单个变量的原子性,无法保证复合操作的原子性。 - 读多写少、对一致性要求极高的场景 :可结合
volatile保证可见性,配合原子类或锁保证原子性。
9.2 线程安全集合
优先使用 java.util.concurrent 包下的线程安全集合:
- ConcurrentHashMap
- CopyOnWriteArrayList
- BlockingQueue
9.3 最佳实践总结
- 优先使用线程池而非手动创建线程。
- 使用原子类替代简单的同步操作。
- 合理设置线程池参数,避免资源浪费。
- 使用 volatile 保证可见性,使用 synchronized 或 Lock 保证原子性。
- 避免在持有锁时执行耗时操作。
10. 总结与展望
10.1 核心知识点回顾
本文系统性地梳理了 Java 多线程编程的完整知识体系,核心知识点可归纳为以下五个层面:
- 线程基础:线程是 CPU 调度的基本单位,与进程共享地址空间;线程在其生命周期中经历 NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED 六种状态,理解状态转换是并发编程的基石。
- 创建方式 :继承
Thread、实现Runnable、使用Callable+Future三种方式,推荐优先使用接口实现以保留类的继承能力,并通过Future获取异步执行结果。 - 同步机制 :
synchronized提供内置互斥,Lock接口更灵活(支持tryLock、公平锁),volatile保证可见性但不保证原子性,原子类基于 CAS 实现无锁自增。 - 线程池 :通过复用线程降低创建销毁开销,推荐使用
ThreadPoolExecutor手动创建,显式配置核心线程数、最大线程数、队列容量和拒绝策略,避免Executors便捷方法的无界队列与线程数失控隐患。 - 并发工具类 :
CountDownLatch用于等待多个任务完成,CyclicBarrier让一组线程互相等待,Semaphore控制同时访问的线程数量,配合线程安全集合(ConcurrentHashMap、CopyOnWriteArrayList、BlockingQueue)应对各类并发场景。
10.2 进阶方向展望
掌握上述基础后,可以进一步探索以下多线程编程的进阶方向:
- CompletableFuture :基于
Future的增强,支持异步任务的链式编排、组合与回调,通过thenApply、thenCombine、allOf等方法优雅地处理复杂的异步流程,是函数式异步编程的核心工具。 - Fork/Join 框架:采用分治思想,将大任务递归拆分为小任务并行执行,再合并结果,适合计算密集型任务(如大数据排序、矩阵运算),配合工作窃取算法提升 CPU 利用率。
- 虚拟线程(Virtual Threads):JDK 21 正式引入的轻量级线程,由 JVM 调度而非操作系统,创建成本极低,可支撑百万级并发,大幅简化高并发 IO 密集型应用的编写,是未来 Java 并发编程的重要方向。
10.3 常见并发问题的排查建议
在实际开发中,遇到并发问题时可参考以下排查思路:
- 死锁(Deadlock) :表现为程序卡死、线程互相等待对方持有的锁。排查时先用
jstack导出线程转储,定位处于 BLOCKED 状态且互相等待的线程;修复时按固定顺序获取锁、使用tryLock设置超时,或尽量减少锁的持有时间,打破循环等待。 - 活锁(Livelock):线程并未阻塞,却因互相谦让而不断改变状态、始终无法推进。排查时观察线程是否反复执行相同的让步逻辑却无实际进展;修复时引入随机退避或优先级策略,避免线程无限循环地互相让步。
- 线程饥饿(Starvation) :低优先级线程长期得不到 CPU 时间或锁资源,导致任务迟迟无法执行。排查时结合线程转储观察是否有线程长期处于 RUNNABLE 却未获得调度;修复时使用公平锁(如
ReentrantLock(true))保证等待最久的线程优先获得锁,并合理设置线程优先级。
多线程编程是一个理论与实践并重的领域,建议读者在理解原理的基础上,多动手编写并发代码、善用 jstack 等诊断工具,逐步提升自己的并发编程与问题排查能力。