并发编程是Java进阶的"分水岭",掌握它,你就拿到了大厂面试的入场券。
"线程安全"、"可见性"、"死锁"——这些词汇在Java面试中出现频率极高,但很多朋友对它们的理解停留在"背答案"层面:
synchronized 和 ReentrantLock 有什么区别? → "一个是关键字,一个是类"volatile 能保证原子性吗? → "不能"但如果面试官追问一句:"synchronized 在 JVM 层面是怎么实现的?""volatile 的内存屏障到底是如何禁止指令重排序的?""写一个死锁,怎么用 jstack 定位?" ——很多人的回答就开始含糊了。
本文会从用法 → 原理 → 实战排查三个维度,把 Java 并发多线程的核心知识点彻底讲透,每一部分都配有代码示例和 JVM 层面的原理解析。
方式 | 实现 | 特点 |
|---|---|---|
继承 Thread | extends Thread,重写 run() | 单一继承局限,不推荐 |
实现 Runnable | implements Runnable,实现 run() | 更灵活,任务与线程分离 |
实现 Callable | implements Callable<T>,有返回值,可抛异常 | 配合 FutureTask 获取结果 |
代码示例:
// 方式一:继承 Thread(不推荐)
class MyThread extends Thread {
@Override
public void run() {
System.out.println("Thread running");
}
}
// 方式二:实现 Runnable(推荐)
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("Runnable running");
}
}
// 方式三:实现 Callable + FutureTask(获取返回值)
class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
return "Callable result";
}
}
// 使用示例
public static void main(String[] args) throws Exception {
// Runnable 配合线程池使用更佳
new Thread(new MyRunnable()).start();
// Callable 获取返回值
FutureTask<String> futureTask = new FutureTask<>(new MyCallable());
new Thread(futureTask).start();
String result = futureTask.get(); // 阻塞等待结果
System.out.println(result);
}关键理解:
RUNNABLE 包含了"就绪"和"运行"两个状态,由操作系统调度BLOCKED 是 synchronized 专属,Lock 接口的等待是 WAITINGwait() 必须配合 synchronized 使用,会释放锁;sleep() 不释放锁现代 CPU 为了性能,引入了多级缓存和指令重排序。JMM 定义了 Java 程序在并发环境下的可见性、有序性、原子性规范。
JMM 抽象结构:
┌─────────────┐ ┌─────────────┐
│ 线程 A │──────────│ 线程 B │
└──────┬──────┘ └──────┬──────┘
│ 写入 │ 读取
▼ ▼
┌─────────────┐ ┌─────────────┐
│ 工作内存 │ │ 工作内存 │
│ (CPU 缓存) │ │ (CPU 缓存) │
└──────┬──────┘ └──────┬──────┘
│ │
└──────────┬─────────────┘
▼
┌─────────────┐
│ 主内存 │
│ (堆内存) │
└─────────────┘核心规则:线程对共享变量的所有操作都必须在工作内存中进行,不能直接读写主内存;不同线程之间不能直接访问对方的工作内存,变量值的传递必须通过主内存完成。
特性 | 含义 | 实现手段 |
|---|---|---|
原子性 | 操作不可中断 | synchronized、Lock、Atomic 类 |
可见性 | 一个线程修改,其他线程立即感知 | volatile、synchronized、final |
有序性 | 代码按顺序执行(不重排序) | volatile 禁止重排序、happens-before 规则 |
volatile 修饰的变量有两个语义:
JVM 对 volatile 的字节码处理:
// Java 代码
volatile boolean flag = true;
// JVM 字节码层面
// 在 volatile 写操作前后插入内存屏障
// StoreStoreBarrier → volatile 写 → StoreLoadBarrier
// LoadLoadBarrier → volatile 读 → LoadStoreBarrierHotSpot 源码中的实现(orderAccess.hpp):
// x86 架构下,volatile 写会加上 lock 前缀指令
// lock addl $0x0,(%rsp) —— 这是一个全屏障
inline void OrderAccess::fence() {
if (os::is_MP()) {
__asm__ volatile ("lock; addl $0,0(%%rsp)" : : : "cc", "memory");
}
}面试重点:volatile 不保证原子性。count++ 这种操作分为"读-改-写"三步,volatile 只能保证读和写各自是原子的,但三步合起来不是原子的。需要用 AtomicInteger 或 synchronized。
如果两个操作满足 happens-before 关系,那么前一个操作的结果对后一个操作可见:
// 1. 实例方法锁 —— 锁的是当前实例对象 this
public synchronized void method1() { ... }
// 2. 静态方法锁 —— 锁的是 Class 对象
public static synchronized void method2() { ... }
// 3. 代码块锁 —— 最灵活,可指定锁对象
public void method3() {
synchronized (this) { ... }
synchronized (lockObject) { ... }
}// 一段简单的同步代码块
public void add() {
synchronized (this) {
count++;
}
}
// 对应的字节码
public void add();
Code:
0: aload_0 // 将 this 推入栈顶
1: dup
2: astore_1 // 存储到局部变量
3: monitorenter // ★ 进入同步块,获取锁
4: aload_0
5: dup
6: getfield #2 // 获取 count 字段
9: iconst_1
10: iadd
11: putfield #2 // 写回 count
14: aload_1
15: monitorexit // ★ 退出同步块,释放锁
16: goto 24
19: astore_2
20: aload_1
21: monitorexit // ★ 异常处理中也要释放锁
22: aload_2
23: athrow
24: return每个 Java 对象都关联一个 Monitor 对象,monitorenter 尝试获取 Monitor 的所有权,获取成功后计数器 +1;monitorexit 计数器 -1,减到 0 时释放锁。
JDK 1.6 引入了偏向锁 → 轻量级锁 → 重量级锁的升级过程,避免直接使用重量级的互斥量(Mutex)。
锁状态 | 适用场景 | 实现原理 |
|---|---|---|
无锁 | 无竞争 | - |
偏向锁 | 同一线程多次重入 | Mark Word 存储线程 ID,CAS 替换 |
轻量级锁 | 少量线程竞争 | CAS 自旋尝试获取,不阻塞 |
重量级锁 | 高并发竞争 | 操作系统的 Mutex,线程阻塞,上下文切换 |
锁升级流程:
条件 | 说明 |
|---|---|
互斥 | 资源同一时间只能被一个线程占用 |
占有且等待 | 线程已占有一个资源,同时等待另一个资源 |
不可抢占 | 已占有的资源不能被强制剥夺 |
循环等待 | 线程之间形成资源等待环路 |
public class DeadlockDemo {
private static final Object RESOURCE_A = new Object();
private static final Object RESOURCE_B = new Object();
public static void main(String[] args) {
// 线程1:先拿 A,再拿 B
Thread t1 = new Thread(() -> {
synchronized (RESOURCE_A) {
System.out.println("Thread1: 持有 A,等待 B...");
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (RESOURCE_B) {
System.out.println("Thread1: 同时持有 A 和 B");
}
}
});
// 线程2:先拿 B,再拿 A —— 形成死锁
Thread t2 = new Thread(() -> {
synchronized (RESOURCE_B) {
System.out.println("Thread2: 持有 B,等待 A...");
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (RESOURCE_A) {
System.out.println("Thread2: 同时持有 A 和 B");
}
}
});
t1.start();
t2.start();
}
}运行后程序卡住不动,没有输出"同时持有"的日志。
步骤一:用 jps 查看 Java 进程 PID
$ jps
12345 DeadlockDemo
12346 Jps步骤二:用 jstack 导出线程快照
jstack 12345输出关键信息:
Found one Java-level deadlock:
=============================
"Thread1":
waiting to lock monitor 0x00007f... (object 0x... <-- RESOURCE_B)
which is held by "Thread2"
"Thread2":
waiting to lock monitor 0x00007f... (object 0x... <-- RESOURCE_A)
which is held by "Thread1"
Java stack information for the threads listed above:
===================================================
Thread1:
at DeadlockDemo.lambda$main$0(DeadlockDemo.java:15)
- waiting to lock <0x...> (a java.lang.Object)
- locked <0x...> (a java.lang.Object)
Thread2:
at DeadlockDemo.lambda$main$1(DeadlockDemo.java:26)
- waiting to lock <0x...> (a java.lang.Object)
- locked <0x...> (a java.lang.Object)jstack 直接告诉你死锁的线程、锁对象和代码行号,定位非常高效。
方案 | 说明 | 示例 |
|---|---|---|
破坏占有且等待 | 一次性申请所有资源 | 用 synchronized 嵌套时,统一锁顺序 |
破坏循环等待 | 所有线程按相同顺序加锁 | 都先拿 A 再拿 B |
使用超时机制 | 尝试获取锁,超时放弃 | ReentrantLock.tryLock(timeout, unit) |
使用 Lock 接口 | 更灵活的锁控制 | ReentrantLock + tryLock |
推荐方案:统一加锁顺序 + ReentrantLock.tryLock 超时机制
// 使用 tryLock 避免死锁
public boolean transfer(ReentrantLock lockA, ReentrantLock lockB) {
long timeout = 1000;
while (true) {
if (lockA.tryLock(timeout, TimeUnit.MILLISECONDS)) {
try {
if (lockB.tryLock(timeout, TimeUnit.MILLISECONDS)) {
try {
// 业务操作
return true;
} finally {
lockB.unlock();
}
}
} finally {
lockA.unlock();
}
}
// 超时重试
if (--retryCount == 0) return false;
}
}工具类 | 作用 | 底层原理 |
|---|---|---|
AtomicInteger | 原子操作 int | CAS(Unsafe.compareAndSwapInt) |
ReentrantLock | 可重入锁 | AQS(AbstractQueuedSynchronizer) |
CountDownLatch | 等待多个线程完成 | AQS 共享模式 |
CyclicBarrier | 多线程互相等待到齐 | ReentrantLock + Condition |
Semaphore | 控制并发线程数 | AQS 共享模式 |
ConcurrentHashMap | 线程安全 Map | CAS + synchronized(JDK 8) |
BlockingQueue | 阻塞队列 | ReentrantLock + Condition |
AbstractQueuedSynchronizer 是 JUC 包的基石,核心是一个 volatile int state 变量 + CLH 双向队列:
// AQS 核心结构
public abstract class AbstractQueuedSynchronizer {
private volatile int state; // 同步状态
private transient volatile Node head; // CLH 队列头
private transient volatile Node tail; // CLH 队列尾
// 尝试获取锁(由子类实现)
protected boolean tryAcquire(int arg) { ... }
// 尝试释放锁(由子类实现)
protected boolean tryRelease(int arg) { ... }
}ReentrantLock 的 state 表示锁的重入次数Semaphore 的 state 表示剩余许可证数量CountDownLatch 的 state 表示剩余计数对比维度 | sleep() | wait() |
|---|---|---|
所属类 | Thread 静态方法 | Object 实例方法 |
是否释放锁 | ❌ 不释放 | ✅ 释放(必须在 synchronized 块中) |
唤醒方式 | 时间到 / interrupt() | notify() / notifyAll() / 时间到 |
使用场景 | 线程暂停 | 线程间通信 |
答:为了确保调用 wait() 前已经持有锁(Monitor)。否则会抛出 IllegalMonitorStateException。这是防止虚假唤醒和保证 wait/notify 的可见性的基础。
答:不能。volatile 只保证可见性和有序性,不保证原子性。对于 count++ 这种复合操作,必须用 synchronized 或 AtomicXXX。
最后的话:并发编程的学习没有捷径,但有一条高效的路径——写代码验证 → 读源码理解 → 用工具排查。建议你把文中的死锁 Demo 跑一遍,用 jstack 实际定位一次,这比背十道面试题都管用。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。