第一章:Java并发编程概述
在现代软件开发中,随着多核处理器的普及和应用复杂度的提升,Java并发编程已成为构建高性能、高响应性系统的核心技术之一。并发编程允许多个任务在同一时间段内交替执行,从而更高效地利用系统资源,特别是在I/O密集型和计算密集型场景中表现尤为突出。
并发与并行的区别
- 并发(Concurrency):多个任务交替执行,逻辑上看似同时运行,实际可能由CPU时间片切换实现。
- 并行(Parallelism):多个任务真正同时执行,通常依赖于多核或多处理器架构。
Java中的线程模型
Java通过
java.lang.Thread类和
java.util.concurrent包提供了强大的并发支持。每个Java程序默认启动一个主线程,开发者可通过继承
Thread类或实现
Runnable接口创建新线程。
// 创建并启动线程的示例
public class SimpleThread implements Runnable {
public void run() {
System.out.println("当前线程: " + Thread.currentThread().getName());
}
public static void main(String[] args) {
Thread thread = new Thread(new SimpleThread());
thread.start(); // 启动新线程
}
}
上述代码中,
run()方法定义了线程执行的逻辑,调用
start()方法后,JVM会调度该线程异步执行。
并发编程的核心挑战
| 挑战 | 描述 |
|---|
| 可见性 | 一个线程对共享变量的修改,其他线程可能无法立即看到。 |
| 原子性 | 多个操作在执行过程中不可中断,否则可能导致数据不一致。 |
| 有序性 | JVM和处理器可能对指令重排序,影响程序正确性。 |
graph TD
A[开始] --> B{是否多线程?}
B -->|是| C[同步访问共享资源]
B -->|否| D[顺序执行]
C --> E[使用锁或volatile]
E --> F[保证线程安全]
第二章:线程池核心机制与最佳实践
2.1 线程池的生命周期与状态控制
线程池在其生命周期中经历创建、运行、关闭和终止等关键阶段。通过状态控制机制,可以精确管理任务执行与资源释放。
核心状态流转
线程池通常维护 RUNNING、SHUTDOWN、STOP、TERMINATED 等状态,控制任务提交与线程中断行为。
// 示例:Go 中通过通道控制线程池关闭
func (p *Pool) Shutdown() {
close(p.jobQueue)
for _, worker := range p.workers {
worker.Stop()
}
}
该代码通过关闭任务队列并逐个停止工作协程,实现优雅关闭。jobQueue 为无缓冲通道,关闭后不再接收新任务。
状态转换规则
- RUNNING:接受新任务,处理队列中的任务
- SHUTDOWN:不接受新任务,继续处理队列任务
- STOP:不接受新任务,尝试中断正在执行的任务
- TERMINATED:所有任务结束,资源回收完成
2.2 ThreadPoolExecutor参数调优与实战配置
合理配置`ThreadPoolExecutor`的参数对系统性能至关重要。核心线程数(`corePoolSize`)应根据CPU密集型或IO密集型任务类型设定,通常IO密集型可设为CPU核心数的2倍。
关键参数配置示例
new ThreadPoolExecutor(
8, // corePoolSize
16, // maximumPoolSize
60L, // keepAliveTime (seconds)
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1024),
new ThreadPoolExecutor.CallerRunsPolicy()
);
上述配置适用于高并发Web服务:核心线程保持常驻,最大线程应对突发流量,队列缓存待处理任务,拒绝策略由调用线程直接执行。
参数调优建议
- 避免使用无界队列,防止资源耗尽
- keepAliveTime设置过短可能导致频繁创建/销毁线程
- 拒绝策略需结合业务场景选择,生产环境建议自定义日志记录
2.3 自定义线程池设计与拒绝策略应用
在高并发场景中,合理配置线程池能有效控制系统资源消耗。通过 `ThreadPoolExecutor` 可精细控制核心线程数、最大线程数、队列容量及拒绝策略。
自定义线程池示例
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, // 核心线程数
4, // 最大线程数
60L, // 空闲线程存活时间
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(10), // 任务队列
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
);
上述代码创建了一个可伸缩的线程池,当任务队列满且线程数达到上限时,由提交任务的线程直接执行任务,避免系统过载。
常见拒绝策略对比
| 策略 | 行为 |
|---|
| AbortPolicy | 抛出 RejectedExecutionException |
| CallerRunsPolicy | 调用者线程执行任务 |
| DiscardPolicy | 静默丢弃任务 |
2.4 ForkJoinPool与工作窃取算法实践
ForkJoinPool 是 Java 中用于并行执行任务的线程池,特别适用于可分解为小任务的计算密集型操作。其核心在于工作窃取(Work-Stealing)算法:每个工作线程维护一个双端队列,任务被拆分后推入队列尾部,线程从头部获取任务执行;当某线程队列为空时,会从其他线程队列尾部“窃取”任务,减少空闲时间。
核心组件与使用模式
通过继承
RecursiveTask 或
RecursiveAction 实现任务拆分与合并。
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;
}
@Override
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() 将任务放入当前线程队列,
join() 阻塞等待结果。ForkJoinPool 自动调度任务并利用工作窃取提升整体吞吐。
2.5 线程池性能监控与内存泄漏防范
监控核心指标采集
通过暴露线程池的运行时状态,可实时追踪活跃线程数、任务队列长度和已完成任务数。JDK 提供了
ThreadPoolExecutor 的访问接口:
ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(10);
System.out.println("Active Threads: " + executor.getActiveCount());
System.out.println("Task Queue Size: " + executor.getQueue().size());
System.out.println("Completed Tasks: " + executor.getCompletedTaskCount());
上述代码用于获取关键运行指标,便于集成至 Prometheus 或日志系统,实现可视化监控。
防止内存泄漏的最佳实践
未正确关闭线程池可能导致线程持续驻留,引发内存泄漏。应确保在应用关闭时调用优雅停机:
- 使用
shutdown() 启动有序关闭,不再接受新任务 - 配合
awaitTermination() 设置超时等待任务完成 - 避免使用无界队列(如
LinkedBlockingQueue)导致任务堆积
第三章:锁优化技术深度解析
3.1 synchronized底层原理与JVM优化机制
对象头与锁状态
synchronized的实现依赖于Java对象头中的Mark Word,它在运行时存储对象的哈希码、GC分代年龄和锁状态。根据竞争程度,JVM会逐步升级锁状态:无锁 → 偏向锁 → 轻量级锁 → 重量级锁。
Monitor机制
每个Java对象都关联一个Monitor(管程),当线程尝试获取synchronized锁时,需竞争进入Monitor的入口区。以下是Monitor关键字段的简化表示:
| 字段 | 说明 |
|---|
| _owner | 持有锁的线程引用 |
| _EntryList | 阻塞等待获取锁的线程队列 |
| _WaitSet | 调用wait()后进入的等待集合 |
代码执行示例
synchronized (obj) {
// 临界区
obj.notify();
}
上述代码在字节码层面会被编译为
monitorenter和
monitorexit指令。JVM通过CAS操作尝试将Mark Word指向当前线程栈帧,若失败则触发锁膨胀。
3.2 ReentrantLock与CAS操作的性能对比
数据同步机制
在高并发场景下,ReentrantLock 和基于 CAS(Compare-And-Swap)的原子操作是两种常见的线程安全实现方式。ReentrantLock 依赖于 AQS 框架,通过挂起线程实现等待,适合长时间持有锁的场景;而 CAS 是无锁算法,通过硬件指令保证原子性,适用于竞争较轻的环境。
性能对比测试
以下代码展示了使用
ReentrantLock 和
AtomicInteger 进行计数的对比:
// 使用 ReentrantLock
private final ReentrantLock lock = new ReentrantLock();
private int countWithLock = 0;
public void incrementWithLock() {
lock.lock();
try {
countWithLock++;
} finally {
lock.unlock();
}
}
// 使用 CAS
private final AtomicInteger countWithCAS = new AtomicInteger(0);
public void incrementWithCAS() {
countWithCAS.incrementAndGet();
}
上述代码中,
incrementWithLock 在竞争激烈时可能引发线程阻塞和上下文切换,开销较大;而
incrementWithCAS 利用 CPU 的原子指令,避免阻塞,但在高争用下可能因重试导致“自旋”开销。
适用场景总结
- CAS 更适合低争用、高频次的简单操作,如计数器。
- ReentrantLock 提供更灵活的控制(如可中断、超时),适合复杂临界区逻辑。
3.3 锁粗化、锁消除与偏向锁的应用场景
锁粗化:减少频繁加锁开销
当一系列同步操作集中在同一代码块时,JVM 可能将多个连续的
synchronized 块合并为一个更大范围的锁,称为锁粗化。这适用于高频短临界区场景,如循环中的字符串拼接。
for (int i = 0; i < 100; i++) {
sb.append("a"); // 多次调用同步方法
}
JVM 可能将 100 次
append 的锁合并为一次,降低上下文切换成本。
锁消除:基于逃逸分析的优化
若对象仅在局部作用域中使用且无逃逸,JVM 会消除其同步操作。例如:
public void localSync() {
Object obj = new Object();
synchronized(obj) { // JVM 可能消除此锁
System.out.println("No escape");
}
}
逃逸分析确认
obj 不会被外部线程访问,因此同步无意义。
偏向锁:减少单线程竞争开销
偏向锁允许线程在无竞争时无需执行原子指令。适用于大多数时间由单一线程访问的场景,如单线程处理配置对象。
- 锁粗化提升高频同步效率
- 锁消除依赖逃逸分析
- 偏向锁优化单线程主导场景
第四章:AQS框架源码剖析与扩展实现
4.1 AQS设计模式与CLH队列深入解读
AQS(AbstractQueuedSynchronizer)采用模板方法模式,将同步状态管理、线程阻塞与唤醒等核心逻辑封装,允许子类通过重写`tryAcquire`、`tryRelease`等方法实现定制化同步语义。
CLH队列的变体实现
AQS内部维护一个虚拟的双向队列,用于存储等待获取锁的线程节点。每个节点代表一个线程,包含前驱和后继指针,形成FIFO等待队列。
static final class Node {
static final Node SHARED = new Node();
static final Node EXCLUSIVE = null;
volatile int waitStatus;
volatile Node prev, next, thread;
}
上述Node结构是CLH队列的扩展版本,通过prev和next构成双向链表,支持高效的节点取消和中断处理。
同步机制的核心流程
当线程尝试获取锁失败时,AQS将其封装为Node并加入队列尾部,随后进入自旋+LockSupport.park阻塞状态,直到被前驱节点唤醒。
4.2 基于AQS实现自定义同步器的完整案例
在Java并发编程中,AbstractQueuedSynchronizer(AQS)为构建自定义同步组件提供了强大基础。通过继承AQS并重写其核心方法,可实现独占或共享模式的同步语义。
核心方法重写
需重点实现
tryAcquire 与
tryRelease 方法,控制状态获取与释放逻辑:
public class SimpleMutex extends AbstractQueuedSynchronizer {
@Override
protected boolean tryAcquire(int acquires) {
return compareAndSetState(0, 1); // CAS设置状态
}
@Override
protected boolean tryRelease(int releases) {
setState(0); // 释放锁,置状态为0
return true;
}
}
上述代码实现了一个简单的互斥锁。当 state 为 0 时,线程可成功获取锁并将状态设为 1;释放时重置状态,唤醒等待队列中的首个线程。
使用方式
封装 acquire 和 release 方法供外部调用:
acquire():调用 AQS 的模板方法,触发尝试获取与排队逻辑release():释放锁,通知后续节点
4.3 CountDownLatch与CyclicBarrier源码级对比
核心机制差异
CountDownLatch 基于一次性的计数器,等待方调用
await() 阻塞,计数归零后释放所有线程。CyclicBarrier 则是可重复使用的栅栏,当指定数量的线程到达时触发执行。
// CountDownLatch 示例
CountDownLatch latch = new CountDownLatch(2);
new Thread(() -> {
System.out.println("Task 1 done");
latch.countDown();
}).start();
latch.await(); // 等待两个 countDown
上述代码中,
countDown() 递减计数,
await() 阻塞直至计数为0。
// CyclicBarrier 示例
CyclicBarrier barrier = new CyclicBarrier(2, () -> System.out.println("Barrier tripped"));
new Thread(() -> {
System.out.println("Thread 1 reached");
barrier.await();
}).start();
此处
await() 表示线程到达屏障点,当达到2个时执行预设任务,且可重置使用。
关键特性对比
| 特性 | CountDownLatch | CyclicBarrier |
|---|
| 可重用性 | 否 | 是 |
| 计数方向 | 递减 | 递增(到达数) |
| 底层实现 | AQS共享模式 | ReentrantLock + Condition |
4.4 Semaphore信号量模型与限流实践
Semaphore(信号量)是一种用于控制并发访问资源数量的同步工具,常用于实现限流、资源池管理等场景。其核心思想是通过预设的许可数量,限制同时访问临界区的线程数。
基本工作原理
信号量维护一组许可,线程需调用
acquire() 获取许可才能执行,执行完毕后通过
release() 归还。若无可用许可,线程将阻塞直至其他线程释放。
Java中Semaphore的使用示例
Semaphore semaphore = new Semaphore(3); // 最多允许3个线程并发
semaphore.acquire(); // 获取许可
try {
// 执行受限操作
} finally {
semaphore.release(); // 释放许可
}
上述代码创建了一个最多允许3个线程并发执行的信号量。每次
acquire() 调用会减少一个许可,
release() 则增加一个。该机制有效防止资源过载。
典型应用场景
- 数据库连接池的连接数控制
- API接口的并发请求限流
- 微服务中的突发流量削峰
第五章:高频面试题总结与进阶学习路径
常见并发编程问题解析
在Go面试中,
sync.Mutex 与
channel 的选择是高频考点。例如,实现一个线程安全的计数器时,可使用互斥锁:
type SafeCounter struct {
mu sync.Mutex
count int
}
func (c *SafeCounter) Inc() {
c.mu.Lock()
defer c.mu.Unlock()
c.count++
}
而使用 channel 实现信号量控制更符合 Go 的“通过通信共享内存”哲学。
系统设计能力考察点
面试官常要求设计短链服务。核心步骤包括:
- 生成唯一短码(可采用 base62 + 哈希或分布式ID)
- 存储映射关系(Redis 缓存 + MySQL 持久化)
- 处理高并发读(CDN 缓存、热点 key 分片)
性能优化实战案例
某次线上服务 GC 频繁,pprof 分析显示大量小对象分配。优化方案:
- 使用
sync.Pool 复用临时对象 - 减少字符串拼接,改用
strings.Builder - 避免不必要的结构体拷贝,传递指针
进阶学习路线推荐
| 阶段 | 学习内容 | 推荐资源 |
|---|
| 中级 | Go 运行时源码分析 | The Go Programming Language Book |
| 高级 | 调度器与GC机制 | Go 官方博客、runtime/proc.go |
流程图示意短链跳转流程:
Client → DNS → CDN → API Gateway → Redis → DB Fallback