第一章:Java多线程编程基础概念与核心原理
在现代软件开发中,多线程编程是提升程序性能和响应能力的关键技术之一。Java 从语言层面原生支持多线程,通过 `java.lang.Thread` 类和 `java.util.concurrent` 包提供了丰富的并发工具。
线程的创建与启动
Java 中创建线程主要有两种方式:继承 `Thread` 类或实现 `Runnable` 接口。推荐使用 `Runnable`,因为它符合单一职责原则并支持函数式编程风格。
- 定义任务逻辑,实现 `Runnable` 接口的 `run()` 方法
- 创建 `Thread` 实例,并将 `Runnable` 实例作为参数传入构造函数
- 调用 `start()` 方法启动新线程
public class MyTask implements Runnable {
public void run() {
// 线程执行的任务
System.out.println("当前线程: " + Thread.currentThread().getName());
}
}
// 启动线程
Thread thread = new Thread(new MyTask(), "MyThread");
thread.start(); // 调用 start() 才会开启新线程
线程生命周期状态
Java 线程在其生命周期中经历多种状态,由 `Thread.State` 枚举定义:
| 状态 | 说明 |
|---|
| NEW | 线程刚被创建,尚未调用 start() |
| RUNNABLE | 正在JVM中运行,可能等待操作系统CPU资源 |
| BLOCKED | 等待监视器锁以进入同步块/方法 |
| WAITING | 无限期等待其他线程执行特定操作 |
| TIMED_WAITING | 在指定时间内等待 |
| TERMINATED | 线程执行完毕或异常退出 |
线程调度与优先级
Java 线程调度依赖于操作系统,但可通过 `setPriority(int)` 设置优先级(1-10),影响调度概率。高优先级线程更可能获得CPU时间片,但不保证执行顺序。
第二章:线程创建与管理的五种经典方式
2.1 继承Thread类实现多线程:理论与实例剖析
在Java中,通过继承`Thread`类是实现多线程的最直接方式之一。开发者需重写其`run()`方法,定义线程执行体,并通过调用`start()`方法启动线程。
基本实现步骤
- 创建类继承`Thread`
- 重写`run()`方法
- 实例化该类并调用`start()`
代码示例
class MyThread extends Thread {
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + ": " + i);
try {
Thread.sleep(100); // 暂停100毫秒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class ThreadDemo {
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
t1.start(); // 启动第一个线程
t2.start(); // 启动第二个线程
}
}
上述代码中,`run()`方法封装了线程的执行逻辑,`start()`方法由JVM调用,开启新线程并自动执行`run()`。`sleep(100)`模拟耗时操作,避免线程过于频繁输出。两个线程独立运行,体现并发特性。
2.2 实现Runnable接口构建线程:解耦与资源共享实践
通过实现 `Runnable` 接口而非继承 `Thread` 类,可实现线程逻辑与线程对象的解耦,提升代码灵活性和可复用性。
核心优势
- 避免Java单继承限制,类仍可继承其他父类
- 多个线程可共享同一 `Runnable` 实例,便于资源协同访问
- 更符合“任务”与“执行者”分离的设计理念
代码示例
public class CounterTask implements Runnable {
private int count = 0;
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + ": " + count++);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
}
// 启动线程
CounterTask task = new CounterTask();
new Thread(task, "T1").start();
new Thread(task, "T2").start();
上述代码中,两个线程共享同一个 `CounterTask` 实例,`count` 变量被共同操作,体现了资源共享场景。由于未使用 `synchronized`,可能出现竞态条件,为后续引入同步机制提供实践基础。
2.3 使用Callable与Future获取线程执行结果:异步任务实战
在Java并发编程中,
Runnable接口无法返回执行结果,而
Callable则弥补了这一缺陷。通过实现
Callable<T>接口,任务可以返回指定类型的计算结果,并配合
Future对象进行异步获取。
核心接口说明
- Callable<T>:定义带返回值的异步任务,通过
call()方法返回结果; - Future<T>:用于获取异步任务的执行状态和结果,支持取消任务、判断是否完成及获取结果。
代码示例:异步计算质数
ExecutorService executor = Executors.newSingleThreadExecutor();
Callable<Integer> task = () -> {
// 模拟耗时计算
Thread.sleep(1000);
return 97; // 返回质数
};
Future<Integer> future = executor.submit(task);
// 获取结果(阻塞直到完成)
Integer result = future.get(); // result = 97
上述代码提交一个可调用任务到线程池,
submit()方法返回
Future对象,调用其
get()方法可获得异步执行结果,若任务未完成则会阻塞当前线程。
2.4 线程池ThreadPoolExecutor的应用场景与性能对比
典型应用场景
ThreadPoolExecutor适用于高并发任务处理,如Web服务器请求调度、批量数据处理和异步任务执行。通过复用线程,减少线程创建销毁开销。
核心参数配置示例
ThreadPoolExecutor executor = new ThreadPoolExecutor(
4, // 核心线程数
10, // 最大线程数
60L, // 空闲线程存活时间
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(100), // 任务队列
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
);
该配置适用于负载波动较大的服务场景,核心线程保持常驻,超出任务进入队列或启用临时线程。
性能对比分析
| 线程池类型 | 吞吐量 | 响应延迟 | 资源消耗 |
|---|
| FixedThreadPool | 高 | 低 | 中 |
| CacheThreadPool | 中 | 高 | 高 |
| 自定义ThreadPoolExecutor | 最高 | 最低 | 可控 |
合理配置的ThreadPoolExecutor在吞吐量和资源控制上优于默认线程池。
2.5 ForkJoinPool工作窃取模型在分治算法中的应用
ForkJoinPool 通过“工作窃取”机制高效执行分治任务。每个线程维护一个双端队列,任务被分解后推入队尾,执行时从队头取出。当某线程空闲时,会从其他线程的队尾“窃取”任务,实现负载均衡。
核心优势
- 减少线程竞争,提升并行效率
- 适用于可递归拆分的大任务
典型代码示例
public class SumTask extends RecursiveTask<Long> {
private final long[] data;
private final int start, end;
public SumTask(long[] data, int start, int end) {
this.data = data;
this.start = start;
this.end = end;
}
protected Long compute() {
if (end - start <= 1000) {
return Arrays.stream(data, start, end).sum();
}
int mid = (start + end) / 2;
SumTask left = new SumTask(data, start, mid);
SumTask right = new SumTask(data, mid, end);
left.fork(); // 异步提交
return right.compute() + left.join(); // 合并结果
}
}
上述代码将数组求和任务递归拆分。当子任务足够小时直接计算,否则拆分为左右两个子任务,一个异步执行(fork),另一个立即执行(compute),最后合并结果(join)。该模式充分利用多核资源,显著提升处理效率。
第三章:线程同步与通信关键技术
3.1 synchronized关键字的底层原理与优化案例
数据同步机制
Java中的synchronized关键字通过JVM内置的监视器(Monitor)实现线程互斥。每个对象都关联一个监视器锁,当线程进入synchronized代码块时,必须先获取锁,执行完毕后释放。
字节码层面的实现
public void synchronizedMethod() {
synchronized (this) {
// 临界区操作
System.out.println("Thread-safe operation");
}
}
上述代码编译后会生成
monitorenter和
monitorexit指令,分别对应锁的获取与释放。JVM通过对象头中的Mark Word记录锁状态。
锁优化策略
- 偏向锁:减少无竞争场景下的同步开销
- 轻量级锁:避免重量级锁的系统调用成本
- 自旋锁:在等待时间短时提升性能
3.2 volatile关键字与内存可见性实战分析
内存可见性问题的根源
在多线程环境下,每个线程可能将共享变量缓存在本地内存(如CPU缓存),导致一个线程对变量的修改无法立即被其他线程感知。这种现象称为内存可见性问题。
volatile的解决方案
Java中的
volatile关键字确保变量的修改对所有线程立即可见。每当读取volatile变量时,JVM会强制从主内存中重新加载;写入时则立即刷新回主内存。
public class VolatileExample {
private volatile boolean running = true;
public void stop() {
running = false; // 其他线程能立即看到该变化
}
public void run() {
while (running) {
// 执行任务
}
}
}
上述代码中,
running被声明为volatile,保证了线程间对该标志位的可见性,避免无限循环。
volatile与指令重排序
JVM和处理器可能会进行指令重排序优化,但volatile变量的读写操作具有内存屏障语义,禁止特定类型的重排序,从而保障程序执行顺序的正确性。
3.3 wait/notify机制实现生产者消费者模式
在Java多线程编程中,`wait()` 和 `notify()` 方法是实现线程间协作的关键手段。通过共享对象的监视器锁,生产者线程在缓冲区满时调用 `wait()` 进入等待状态,消费者线程消费后调用 `notify()` 唤醒生产者;反之亦然。
核心代码实现
public synchronized void produce() {
while (buffer.isFull()) {
wait(); // 释放锁并等待
}
buffer.add(new Object());
notifyAll(); // 通知所有等待线程
}
上述方法中,`synchronized` 确保互斥访问,`while` 检查条件防止虚假唤醒,`wait()` 使当前线程阻塞,`notifyAll()` 唤醒其他等待线程。
协作流程
- 生产者获取锁,检查缓冲区是否已满
- 若满则调用 wait() 释放锁并挂起
- 消费者获取锁,消费数据后调用 notify()
- JVM从等待队列中唤醒一个线程重新竞争锁
第四章:并发工具类与高级并发控制
4.1 CountDownLatch在并发测试中的协调作用
同步多个线程的启动与结束
在并发测试中,常需确保多个线程同时开始执行或等待所有任务完成后再进行断言。CountDownLatch 通过计数器机制实现线程间的协调。
CountDownLatch startSignal = new CountDownLatch(1);
CountDownLatch doneSignal = new CountDownLatch(5);
for (int i = 0; i < 5; i++) {
new Thread(() -> {
try {
startSignal.await(); // 等待开始信号
System.out.println("Task executed");
} finally {
doneSignal.countDown(); // 任务完成,计数减一
}
}).start();
}
startSignal.countDown(); // 释放所有等待线程
doneSignal.await(); // 主线程等待所有子线程完成
上述代码中,
startSignal 确保所有线程同时启动,避免竞争不均;
doneSignal 则用于主线程等待全部任务结束。这种双重同步机制广泛应用于性能压测和并发正确性验证场景。
4.2 CyclicBarrier在并行计算中的同步控制
同步机制原理
CyclicBarrier 是 Java 并发包中用于线程同步的工具,适用于多线程并行计算场景。它允许一组线程相互等待,直到所有线程都到达某个公共屏障点,再继续执行后续逻辑,特别适合分阶段并行任务。
核心代码示例
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();
}
}).start();
}
上述代码创建了一个可重用的屏障,等待 3 个线程全部调用
await() 后触发汇总操作。参数 3 表示参与线程数,第二个参数为屏障动作(Runnable),在所有线程到达后执行。
- 支持重复使用,执行完一次可自动重置
- 适用于地图归约、并行算法分段计算等场景
4.3 Semaphore信号量控制资源访问数量的典型应用
在并发编程中,Semaphore(信号量)用于限制同时访问某一资源的线程数量,适用于数据库连接池、API调用限流等场景。
信号量基本原理
Semaphore通过维护许可数量来控制并发访问。线程需获取许可才能执行,执行完成后释放许可。
Go语言实现示例
package main
import (
"fmt"
"sync"
"time"
)
var sem = make(chan struct{}, 3) // 最多3个并发
var wg sync.WaitGroup
func accessResource(id int) {
defer wg.Done()
sem <- struct{}{} // 获取许可
fmt.Printf("协程 %d 开始访问资源\n", id)
time.Sleep(2 * time.Second)
fmt.Printf("协程 %d 结束访问\n", id)
<-sem // 释放许可
}
func main() {
for i := 1; i <= 5; i++ {
wg.Add(1)
go accessResource(i)
}
wg.Wait()
}
上述代码通过带缓冲的channel模拟信号量,限制最多3个goroutine同时访问资源。缓冲大小即为最大并发数,确保系统资源不被过度占用。
4.4 ReentrantLock与Condition实现精确线程通信
Condition接口的协作机制
ReentrantLock结合Condition接口可实现线程间的精准等待与唤醒。相较于Object的wait/notify,Condition支持多个等待队列,提供更细粒度的控制。
代码示例:生产者-消费者模型
Lock lock = new ReentrantLock();
Condition notFull = lock.newCondition();
Condition notEmpty = lock.newCondition();
// 生产者
lock.lock();
try {
while (queue.size() == CAPACITY) {
notFull.await(); // 队列满时等待
}
queue.add(item);
notEmpty.signal(); // 通知消费者
} finally {
lock.unlock();
}
上述代码中,
notFull和
notEmpty两个条件变量分别管理不同状态下的线程阻塞与唤醒,避免了虚假唤醒和锁竞争。
- Condition通过
await()使线程进入等待状态 signal()唤醒一个等待线程,signalAll()唤醒全部- 必须在lock块内调用,否则抛出IllegalMonitorStateException
第五章:高并发场景下的设计模式与最佳实践总结
服务降级与熔断机制
在流量高峰期间,保障核心链路的稳定性至关重要。采用熔断器模式可有效防止雪崩效应。以下为使用 Go 实现的简单熔断逻辑:
type CircuitBreaker struct {
failureCount int
threshold int
state string // "closed", "open", "half-open"
}
func (cb *CircuitBreaker) Call(serviceCall func() error) error {
if cb.state == "open" {
return errors.New("service is currently unavailable")
}
err := serviceCall()
if err != nil {
cb.failureCount++
if cb.failureCount >= cb.threshold {
cb.state = "open"
}
return err
}
cb.failureCount = 0
return nil
}
缓存策略优化
合理利用多级缓存架构(本地缓存 + 分布式缓存)可显著降低数据库压力。常见实践包括:
- 使用 Redis 作为一级缓存,设置合理的过期时间与淘汰策略
- 在应用层引入 sync.Map 或 Ristretto 实现本地热点数据缓存
- 采用缓存预热机制,在高峰期前加载高频访问数据
异步处理与消息队列
对于非实时操作,如订单日志记录、通知推送等,应通过消息中间件进行解耦。典型架构如下:
| 组件 | 作用 |
|---|
| Kafka | 高吞吐消息分发,支持削峰填谷 |
| 消费者组 | 并行消费,提升处理效率 |
[用户请求] → [API网关] → [写入Kafka] → [异步处理器]