第一章:Java并发编程核心概念
在多核处理器普及的今天,Java并发编程已成为提升应用性能的关键技术。理解并发的核心概念是构建高效、线程安全程序的基础。线程与进程的区别
- 进程是操作系统资源分配的基本单位,拥有独立的内存空间
- 线程是CPU调度的基本单位,同一进程内的线程共享内存资源
- 线程创建和上下文切换的开销远小于进程
Java中的线程创建方式
Java提供两种主流方式创建线程:- 继承Thread类并重写run()方法
- 实现Runnable接口,将其作为参数传递给Thread构造函数
public class MyThread extends Thread {
@Override
public void run() {
System.out.println("线程执行中...");
}
}
// 启动线程
new MyThread().start(); // 正确:启动新线程
// new MyThread().run(); // 错误:仅在当前线程调用方法
并发安全性问题
当多个线程访问共享数据时,可能出现以下问题:| 问题类型 | 描述 |
|---|---|
| 竞态条件(Race Condition) | 计算结果依赖线程执行顺序 |
| 可见性问题 | 一个线程修改变量后,其他线程无法立即看到最新值 |
| 原子性问题 | 复合操作被多个线程交错执行 |
graph TD
A[主线程] --> B[创建子线程]
B --> C[子线程运行]
C --> D[子线程结束]
D --> E[主线程继续执行]
第二章:线程创建与生命周期管理
2.1 线程的四种创建方式对比与最佳实践
在Java中,线程的创建主要有四种方式:继承Thread类、实现Runnable接口、实现Callable接口配合FutureTask、使用线程池。它们在灵活性、扩展性和资源管理上各有优劣。常见创建方式对比
- 继承Thread类:简单直接,但不支持多继承,扩展性差;
- 实现Runnable接口:解耦任务与线程,推荐用于任务抽象;
- Callable + FutureTask:支持返回值和异常抛出,适用于需获取执行结果的场景;
- 线程池(ExecutorService):复用线程资源,提升性能,是生产环境首选。
代码示例:Callable实现
Callable<Integer> task = () -> {
Thread.sleep(1000);
return 42;
};
FutureTask<Integer> futureTask = new FutureTask<>(task);
new Thread(futureTask).start();
Integer result = futureTask.get(); // 获取返回值
该方式通过Callable定义有返回值的任务,FutureTask封装异步计算结果,调用get()阻塞直至结果可用。
最佳实践建议
| 方式 | 是否推荐 | 适用场景 |
|---|---|---|
| Thread子类 | 否 | 简单演示 |
| Runnable | 是 | 通用任务定义 |
| Callable + Future | 是 | 需要返回值 |
| 线程池 | 强烈推荐 | 高并发生产环境 |
2.2 线程状态转换机制与源码剖析
线程在其生命周期中会经历多种状态转换,包括新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)、等待(Waiting)和终止(Terminated)。JVM 通过底层操作系统调度机制实现状态迁移。核心状态转换流程
新建 → 就绪 → 运行 ⇄ 阻塞
↓
终止
↓
终止
Java 线程状态枚举定义
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
该枚举由 Thread.State 提供,反映 JVM 层面对线程状态的抽象。例如,调用 thread.start() 后线程从 NEW 转为 RUNNABLE,而进入 synchronized 块竞争锁失败则转为 BLOCKED。
状态转换触发方式
wait()→ 进入 WAITING 状态sleep(long)→ 进入 TIMED_WAITING- 锁竞争失败 → BLOCKED
- 执行完毕 → TERMINATED
2.3 线程中断机制的理解与正确使用
线程中断是协作式中断机制,用于安全地请求线程停止当前操作。Java 提供了 `interrupt()`、`isInterrupted()` 和静态方法 `Thread.interrupted()` 来支持中断控制。中断状态与响应行为
调用 `thread.interrupt()` 会设置线程的中断标志位。若线程处于阻塞状态(如 `sleep()`、`wait()`),将抛出 `InterruptedException` 并清除中断状态。Thread worker = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
try {
// 模拟任务执行
Thread.sleep(1000);
} catch (InterruptedException e) {
// 中断异常处理,重新设置中断状态
Thread.currentThread().interrupt();
break;
}
}
});
worker.start();
worker.interrupt(); // 触发中断
上述代码中,捕获 `InterruptedException` 后调用 `interrupt()` 重置中断状态,确保外部能感知到中断请求。这是标准的中断响应模式。
常见误用与规范
- 忽略 InterruptedException:不应吞没异常,需恢复中断状态或显式处理
- 强制终止线程:不推荐使用已废弃的 `stop()` 方法
- 轮询中断状态:在长时间运行任务中应定期检查中断标志
2.4 ThreadLocal原理及其在并发场景中的应用
ThreadLocal 核心机制
ThreadLocal 为每个线程提供独立的变量副本,避免共享变量的线程安全问题。其内部通过ThreadLocalMap 存储线程本地数据,键为当前 ThreadLocal 实例的弱引用,值为用户设定的对象。
典型应用场景
在 Web 服务中,常用于保存用户会话上下文或数据库事务信息,确保跨方法调用时上下文一致性。public class ContextHolder {
private static final ThreadLocal<String> userContext = new ThreadLocal<>();
public static void setUser(String userId) {
userContext.set(userId);
}
public static String getUser() {
return userContext.get();
}
public static void clear() {
userContext.remove();
}
}
上述代码实现了一个线程安全的上下文持有者。每个线程调用 setUser() 时,仅影响自身副本;get() 获取本线程绑定的用户 ID,避免交叉污染。
内存泄漏防范
由于ThreadLocalMap 使用弱引用作为键,但仍需显式调用 remove() 防止值对象长期驻留,尤其在线程池环境下。
2.5 守护线程与用户线程的区别及使用场景
在Java中,线程分为用户线程和守护线程。JVM会在所有用户线程结束后终止程序,而不管是否有守护线程仍在运行。核心区别
- 生命周期控制:JVM依赖用户线程决定程序运行时长,守护线程随最后一个用户线程结束而自动销毁。
- 用途差异:用户线程执行核心业务逻辑,守护线程常用于垃圾回收、监控等后台任务。
代码示例
Thread daemonThread = new Thread(() -> {
while (true) {
System.out.println("守护线程运行中...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
break;
}
}
});
daemonThread.setDaemon(true); // 设置为守护线程
daemonThread.start();
上述代码创建了一个无限循环的线程,并通过setDaemon(true)将其设置为守护线程。当主线程(用户线程)结束时,该线程将被强制终止,无需手动关闭。
典型使用场景
| 线程类型 | 应用场景 |
|---|---|
| 用户线程 | 处理请求、数据库操作、文件读写等关键任务 |
| 守护线程 | 心跳检测、日志清理、性能监控等辅助性工作 |
第三章:synchronized与volatile关键字深度解析
3.1 synchronized的底层实现原理(Monitor、对象头)
对象头与锁状态
Java对象在内存中包含对象头,其中Mark Word存储了对象的哈希码、GC分代年龄和锁状态。当使用synchronized时,JVM通过修改Mark Word中的锁标志位来实现加锁。| 锁状态 | Mark Word含义 |
|---|---|
| 无锁 | 哈希码 + 分代年龄 |
| 偏向锁 | 线程ID + 偏向时间戳 |
| 轻量级锁 | 指向栈中锁记录的指针 |
| 重量级锁 | 指向Monitor对象的指针 |
Monitor机制
每个Java对象都关联一个Monitor(监视器)。当进入synchronized代码块时,线程需竞争获取Monitor的所有权。Monitor内部维护着EntryList和WaitSet,用于管理等待锁的线程。
// 示例:synchronized方法
public synchronized void increment() {
count++;
}
上述代码在编译后,会在方法调用前后插入monitorenter和monitorexit指令,由JVM确保同一时刻仅一个线程能执行该方法。
3.2 volatile的内存语义与禁止指令重排机制
内存可见性保障
当一个变量被声明为volatile,JVM 会确保该变量的修改对所有线程立即可见。每次读取 volatile 变量时,都会从主内存中获取最新值;每次写入时,会立即刷新到主内存。
禁止指令重排序
JVM 和处理器可能会对指令进行重排序以优化性能,但 volatile 通过插入内存屏障(Memory Barrier)防止这种行为。写操作前插入 StoreStore 屏障,读操作后插入 LoadLoad 屏障。
volatile boolean flag = false;
int data = 0;
// 线程1
data = 42; // 1
flag = true; // 2 - 写屏障确保 1 在 2 前执行
// 线程2
if (flag) { // 3 - 读屏障确保 4 在 3 后执行
System.out.println(data); // 4
}
上述代码中,volatile 确保了 data = 42 不会发生在 flag = true 之后,避免了数据读取的竞态条件。
3.3 synchronized与volatile的性能对比与选型建议
数据同步机制
Java 中synchronized 和 volatile 都用于线程安全控制,但实现机制不同。synchronized 保证原子性、可见性和有序性,而 volatile 仅保证可见性和禁止指令重排。
性能对比
- synchronized:基于监视器锁,存在上下文切换开销,适用于复杂临界区操作。
- volatile:轻量级,无锁机制,适合状态标志等简单变量读写场景。
| 特性 | synchronized | volatile |
|---|---|---|
| 原子性 | ✔️ | ❌ |
| 可见性 | ✔️ | ✔️ |
| 性能开销 | 高 | 低 |
volatile boolean running = true;
public void run() {
while (running) { // 线程能及时看到 running 的变化
// 执行任务
}
}
该代码使用 volatile 确保标志位的即时可见性,避免了加锁开销,适用于无需原子操作的场景。
第四章:Java内存模型与并发安全基础
4.1 JMM(Java内存模型)三大特性详解:原子性、可见性、有序性
原子性
原子性指一个操作不可中断,要么全部执行成功,要么全部失败。在多线程环境下,基本的读取或赋值操作可能不具备原子性。例如,对long 和 double 类型的非 volatile 变量进行写操作可能被拆分为两个步骤。
可见性
可见性指当一个线程修改了共享变量的值,其他线程能立即得知这个修改。volatile 关键字通过强制变量在修改后立即写回主内存,并使其他线程缓存失效来保证可见性。volatile boolean flag = false;
// 线程1
while (!flag) {
// 等待
}
// 线程2
flag = true;
上述代码中,若无 volatile,线程1可能永远看不到 flag 的更新。
有序性
有序性指程序执行顺序按照代码先后顺序进行。JVM 会进行指令重排优化,但会在单线程内保证最终结果符合顺序一致性。使用volatile 和 synchronized 可限制重排,确保多线程下的逻辑正确。
4.2 happens-before原则在实际编码中的应用
理解happens-before的可见性保障
在多线程编程中,happens-before原则确保一个操作的结果对另一个操作可见。例如,volatile变量的写操作先行发生于后续对该变量的读操作。
volatile boolean flag = false;
int data = 0;
// 线程1
data = 42; // 步骤1
flag = true; // 步骤2:写volatile变量
// 线程2
if (flag) { // 步骤3:读volatile变量
System.out.println(data); // 步骤4:必然看到data=42
}
上述代码中,由于volatile的happens-before规则,步骤1对data的赋值对步骤4一定可见。
锁与happens-before的结合
synchronized块同样建立happens-before关系。同一锁的释放与获取之间形成顺序一致性。- 线程释放锁前的所有写操作,对下一个获取该锁的线程可见
- 可避免数据竞争,保证复合操作的原子性与可见性
4.3 内存屏障的作用与JVM层面的实现机制
内存屏障(Memory Barrier)是确保多线程环境下内存操作顺序性的关键机制。它通过限制CPU和编译器的指令重排序行为,保障特定内存读写操作的可见性和有序性。内存屏障的类型
JVM中常见的内存屏障包括:- LoadLoad:保证后续加载操作不会被重排到当前加载之前
- StoreStore:确保所有之前的存储操作在当前存储完成前提交
- LoadStore:防止加载操作与后续存储操作重排序
- StoreLoad:最严格的屏障,确保存储完成后才进行后续加载
JVM中的实现示例
// volatile变量写操作插入StoreLoad屏障
volatile int flag = false;
public void writer() {
data = 42; // 普通写
flag = true; // volatile写,插入StoreStore + StoreLoad屏障
}
上述代码中,flag = true 触发JVM在底层插入相应的内存屏障,确保data = 42 对其他线程可见时,flag 的更新也已完成。
硬件与JIT协同实现
现代JVM通过JIT编译器将高级同步语义翻译为底层CPU指令,如x86的
mfence或ARM的dmb指令,实现跨平台一致性模型。
4.4 典型并发问题案例分析:单例模式双重检查锁定失效
在多线程环境下,双重检查锁定(Double-Checked Locking)常用于实现延迟加载的单例模式,但若未正确使用 volatile 关键字,可能导致实例化不安全。问题代码示例
public class Singleton {
private static Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton(); // 可能发生指令重排序
}
}
}
return instance;
}
}
上述代码在 instance 字段未声明为 volatile 时,JVM 可能对对象创建过程进行指令重排序,导致其他线程获取到未完全初始化的实例。
修复方案与原理
- 将 instance 声明为
volatile,禁止指令重排序; - 利用类加载机制或静态内部类实现更安全的单例。
第五章:常见面试陷阱与高频错误总结
忽视边界条件处理
许多候选人能在标准用例中写出正确代码,却在边界场景下失败。例如,在实现二分查找时未考虑数组为空或单元素的情况:// 错误示例:未处理空数组
func binarySearch(nums []int, target int) int {
left, right := 0, len(nums)-1
for left <= right {
mid := (left + right) / 2
if nums[mid] == target {
return mid
} else if nums[mid] < target {
left = mid + 1
} else {
right = mid - 1
}
}
return -1
}
// 正确做法:增加 len(nums) == 0 判断
沟通不足导致误解题意
面试官常故意模糊描述问题,观察候选人是否主动澄清。例如“设计一个缓存”可能指LRU、TTL或分布式缓存。应主动提问:- 数据规模是百万级还是亿级?
- 读写比例如何?是否需要线程安全?
- 是否允许丢失数据?
时间复杂度分析不准确
常见误区是将嵌套循环一律视为 O(n²),而忽略内层操作实际复杂度。例如以下代码:| 代码结构 | 实际复杂度 | 常见误判 |
|---|---|---|
| for i in range(n): hash_map[key] = value | O(n) | O(n²) |
| for i in range(n): list.insert(0, x) | O(n²) | O(n) |
51万+

被折叠的 条评论
为什么被折叠?



