并发编程-锁相关知识点
状态名称含义NEW线程已创建(调用),但未调用start(),此时线程未启动。RUNNABLE线程调用start()后进入该状态(包含 “就绪” 和 “运行中”):- 就绪:线程已准备好,等待 CPU 调度;- 运行中:CPU 正在执行该线程。BLOCKED线程因竞争锁失败(被其他线程持有)而阻塞,直到获取锁。WAITING线程进入 “无限期等待” 状态,需被其他线程显式唤醒,否则一直等待。线程进
一、线程基础
1. 线程的四种创建方式及优缺点对比
Java 中创建线程有四种核心方式,底层均依赖 Thread 类的 run() 方法,只是实现形式不同,具体对比如下:
| 创建方式 | 实现方式 | 优点 | 缺点 |
|---|---|---|---|
| 继承 Thread 类 | 1. 自定义类继承 Thread;2. 重写 run() 方法;3. 创建实例并调用 start()。 |
1. 实现简单,代码直观;2. 可直接通过 this 访问线程对象。 |
1. Java 单继承限制,无法再继承其他类;2. 业务逻辑与线程耦合,不利于解耦;3. 无法获取返回值。 |
| 实现 Runnable 接口 | 1. 自定义类实现 Runnable;2. 实现 run() 方法;3. 将实例传入 Thread 构造器,调用 start()。 |
1. 规避单继承限制,可继承其他类;2. 业务逻辑与线程解耦,符合 “单一职责”;3. 可共享 Runnable 实例(多线程共享数据)。 | 1. 无法直接获取返回值;2. 需借助 Thread 类启动线程,稍繁琐。 |
| 实现 Callable 接口 | 1. 自定义类实现 Callable<V>;2. 实现 call() 方法(可抛异常、有返回值);3. 封装为 FutureTask,传入 Thread 启动;4. 通过 FutureTask.get() 获取返回值。 |
1. 有返回值(解决前两种无返回值问题);2. 可抛出受检异常;3. 同样规避单继承限制。 | 1. get() 方法会阻塞线程(直到获取返回值);2. 代码实现相对复杂。 |
| 线程池创建 | 1. 通过 Executors 或 ThreadPoolExecutor 创建线程池;2. 提交 Runnable/Callable 任务;3. 线程池管理线程生命周期。 |
1. 复用线程,避免频繁创建 / 销毁线程的开销;2. 可控制并发数,防止资源耗尽;3. 提供任务队列、拒绝策略等扩展能力;4. 支持批量提交任务、获取返回值。 | 1. 需手动管理线程池(如关闭),否则可能导致内存泄漏;2. 入门门槛稍高,需理解核心参数(核心线程数、最大线程数等)。 |
代码示例(核心方式):
// 1. 继承Thread
class MyThread extends Thread {
@Override
public void run() {
System.out.println("继承Thread创建线程");
}
}
// 2. 实现Runnable
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("实现Runnable创建线程");
}
}
// 3. 实现Callable
class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
return "Callable返回值";
}
}
// 4. 线程池
public class ThreadCreateDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 方式1
new MyThread().start();
// 方式2
new Thread(new MyRunnable()).start();
// 方式3
FutureTask<String> futureTask = new FutureTask<>(new MyCallable());
new Thread(futureTask).start();
System.out.println(futureTask.get()); // 获取返回值
// 方式4
ExecutorService executor = Executors.newFixedThreadPool(2);
executor.submit(new MyRunnable());
Future<String> future = executor.submit(new MyCallable());
System.out.println(future.get());
executor.shutdown(); // 关闭线程池
}
}
2. 线程六大状态及状态转换
JDK 1.5 后,Thread.State 枚举定义了线程的六大状态,状态转换由具体的线程操作触发,核心关系如下:
(1)六大状态定义
| 状态名称 | 含义 |
|---|---|
| NEW | 线程已创建(调用 new Thread()),但未调用 start(),此时线程未启动。 |
| RUNNABLE | 线程调用 start() 后进入该状态(包含 “就绪” 和 “运行中”):- 就绪:线程已准备好,等待 CPU 调度;- 运行中:CPU 正在执行该线程。 |
| BLOCKED | 线程因竞争 synchronized 锁失败(被其他线程持有)而阻塞,直到获取锁。 |
| WAITING | 线程进入 “无限期等待” 状态,需被其他线程显式唤醒,否则一直等待。 |
| TIMED_WAITING | 线程进入 “有限期等待” 状态,超时后会自动唤醒,无需其他线程干预。 |
| TERMINATED | 线程执行完成(run()/call() 执行结束)或异常终止,生命周期结束。 |
(2)状态转换条件及触发场景
graph TD
A[NEW] -->|调用start()| B[RUNNABLE]
B -->|竞争synchronized锁失败| C[BLOCKED]
C -->|获取synchronized锁| B
B -->|调用wait()| D[WAITING]
B -->|调用sleep(long)/wait(long)/join(long)| E[TIMED_WAITING]
D -->|其他线程调用notify()/notifyAll()| B
E -->|超时/其他线程调用notify()/notifyAll()| B
B -->|run()/call()执行完成/异常终止| F[TERMINATED]
D -->|其他线程调用interrupt()| B
E -->|其他线程调用interrupt()| B
关键转换场景详解:
- NEW → RUNNABLE:仅当调用
thread.start()时触发(注意:start()只能调用一次,重复调用会抛IllegalThreadStateException); - RUNNABLE → BLOCKED:线程进入
synchronized代码块 / 方法,且锁被其他线程持有; - BLOCKED → RUNNABLE:持有锁的线程释放锁(退出同步块),当前线程竞争到锁;
- RUNNABLE → WAITING:
- 调用
Object.wait()(无参):需先持有对象锁,调用后释放锁,进入等待队列; - 调用
Thread.join()(无参):等待目标线程执行完成; - 调用
LockSupport.park():无锁要求,直接进入等待。
- 调用
- RUNNABLE → TIMED_WAITING:
- 调用
Thread.sleep(long ms):不释放锁,超时自动唤醒; - 调用
Object.wait(long ms):释放锁,超时或被唤醒; - 调用
Thread.join(long ms):等待目标线程指定时间; - 调用
LockSupport.parkNanos()/parkUntil():限时等待。
- 调用
- WAITING/TIMED_WAITING → RUNNABLE:
- WAITING:其他线程调用
notify()/notifyAll()(需持有同一把锁),或调用interrupt()中断; - TIMED_WAITING:超时自动唤醒,或被
notify()/notifyAll()/interrupt()触发。
- WAITING:其他线程调用
- RUNNABLE → TERMINATED:
- 正常结束:
run()/call()执行完毕; - 异常结束:执行过程中抛出未捕获的异常(如
NullPointerException)。
- 正常结束:
3. 线程优先级、守护线程的特性与使用
(1)线程优先级(Thread Priority)
-
核心特性:
- 优先级范围:1(
Thread.MIN_PRIORITY)~ 10(Thread.MAX_PRIORITY),默认优先级为 5(Thread.NORM_PRIORITY); - 优先级是 “提示性” 的:JVM 或操作系统不一定严格按优先级调度,仅表示线程获取 CPU 时间片的概率高低(高优先级线程更可能被调度,但不绝对);
- 继承性:子线程的优先级默认与创建它的父线程一致。
- 优先级范围:1(
-
使用方式:
Thread thread = new Thread(new MyRunnable()); thread.setPriority(Thread.MAX_PRIORITY); // 设置最高优先级 thread.start(); System.out.println(thread.getPriority()); // 获取优先级 -
注意事项:
- 不要依赖优先级实现核心业务逻辑(如 “高优先级线程必须先执行”),不同操作系统的调度策略不同;
- 避免设置极端优先级(1 或 10),可能导致低优先级线程长期无法执行(饥饿问题)。
(2)守护线程(Daemon Thread)
-
核心特性:
- 定义:守护线程是 “后台线程”,为用户线程(非守护线程)提供服务(如 GC 线程、定时器线程);
- 生命周期依赖:当所有用户线程执行完毕,JVM 会自动退出,无论守护线程是否执行完成;
- 设置时机:必须在
start()前调用setDaemon(true),否则抛IllegalThreadStateException; - 继承性:默认情况下,子线程的守护属性与父线程一致(父线程是守护线程,子线程默认也是)。
-
使用方式:
Thread daemonThread = new Thread(() -> { while (true) { // 无限循环,但若所有用户线程结束,该线程会被终止 System.out.println("守护线程运行中"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }); daemonThread.setDaemon(true); // 设置为守护线程 daemonThread.start(); // 用户线程:执行3秒后结束 Thread userThread = new Thread(() -> { for (int i = 0; i < 3; i++) { System.out.println("用户线程运行中"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }); userThread.start();执行结果:用户线程执行 3 次后结束,JVM 退出,守护线程的无限循环被终止。
-
注意事项:
- 守护线程中不要执行关键业务(如文件写入、数据保存),可能因 JVM 退出导致操作中断;
- 守护线程的优先级默认较低,且无法通过
join()等待其执行完成(因为用户线程结束后它会被强制终止)。
二、总结
- 线程四种创建方式中,线程池 是生产环境首选(高效、可控),
Callable适合需要返回值的场景,Runnable解耦性优于Thread继承方式; - 线程六大状态的核心转换触发点:
start()启动线程、synchronized锁竞争触发 BLOCKED、wait()/sleep()/join()触发等待状态、执行完成 / 异常触发 TERMINATED; - 线程优先级是 “提示性” 调度策略,不可依赖;守护线程为用户线程服务,生命周期随用户线程结束而终止,适合做后台辅助任务。
二.synchronized核心知识
1. synchronized的底层实现:对象监视器(Monitor)
(1)Java 对象头与锁标记
每个 Java 对象在内存中包含三部分:对象头、实例数据、对齐填充。其中 对象头 是实现锁的关键,它由两部分组成(以 64 位 JVM 为例):
- Mark Word(标记字段):占 64bit,存储对象的哈希值、GC 分代年龄、锁状态标记(无锁 / 偏向锁 / 轻量级锁 / 重量级锁)、持有锁的线程 ID 等核心信息;
- Klass Pointer(类型指针):占 64bit,指向对象所属类的元数据。
(2)Monitor 对象(监视器)
Monitor 是一个底层的 C++ 对象(ObjectMonitor),每个 Java 对象都可以关联一个 Monitor(当对象被用作锁时,JVM 会为其创建 / 关联 Monitor)。ObjectMonitor 核心结构包括:
_owner:指向持有当前 Monitor 的线程;_EntryList:等待获取锁的线程队列(阻塞状态);_WaitSet:调用wait()后等待被唤醒的线程队列;_count:锁的重入次数。
(3)底层执行逻辑
- 当线程进入
synchronized代码块时,JVM 会通过monitorenter指令尝试获取对象的 Monitor:- 如果
_owner为空,当前线程会占用_owner,并将_count置为 1,成功获取锁; - 如果当前线程已持有该 Monitor(重入),则
_count加 1; - 如果 Monitor 被其他线程持有,当前线程会进入
_EntryList阻塞,直到锁被释放。
- 如果
- 当线程退出
synchronized代码块时,执行monitorexit指令:_count减 1,当_count为 0 时,释放 Monitor(_owner置空),并唤醒_EntryList中的线程竞争锁。
补充:
synchronized修饰方法时,底层不依赖monitorenter/monitorexit指令,而是通过方法的access_flags标记(ACC_SYNCHRONIZED)实现,JVM 调用方法时会检查该标记,进而触发 Monitor 的获取 / 释放逻辑。
2. synchronized锁升级流程(无锁 → 偏向锁 → 轻量级锁 → 重量级锁)
JDK 1.6 为了优化 synchronized 的性能,引入了 锁升级 机制(不可逆,只能升级不能降级),核心思想是 “按需升级”,避免一开始就使用性能开销大的重量级锁:
(1)无锁状态
- 对象刚创建时,Mark Word 中锁标记为 “无锁”,存储对象哈希值、GC 年龄等信息,此时没有任何线程竞争锁。
(2)偏向锁(Biased Locking)
- 适用场景:单线程重复获取同一把锁(无多线程竞争)。
- 核心逻辑:
- 当第一个线程获取锁时,JVM 会将 Mark Word 中的锁标记改为 “偏向锁”,并记录该线程的 ID(无需 CAS 操作,仅修改标记);
- 后续该线程再次进入 / 退出同步块时,无需竞争锁,只需检查 Mark Word 中的线程 ID 是否为自己,直接进入,大幅降低锁获取开销。
- 撤销条件:当有第二个线程尝试获取锁时,偏向锁会被撤销(全局安全点 Safepoint 执行),根据当前线程状态升级为轻量级锁或重量级锁。
(3)轻量级锁(Lightweight Locking)
- 适用场景:多线程交替获取锁(无持续竞争,即 “自旋” 可解决)。
- 核心逻辑:
- 线程获取锁时,JVM 会在该线程的栈帧中创建一块 “锁记录(Lock Record)” 区域,存储对象 Mark Word 的拷贝;
- 通过 CAS 操作将对象 Mark Word 改为指向该锁记录的指针,并将锁标记改为 “轻量级锁”,成功则获取锁;
- 如果 CAS 失败(说明有其他线程竞争),当前线程会通过 自旋(Spin) (循环重试 CAS)尝试获取锁(默认自旋 10 次),自旋成功仍可获取轻量级锁。
- 升级条件:自旋达到阈值仍未获取锁,或自旋期间有更多线程竞争,轻量级锁升级为重量级锁。
(4)重量级锁(Heavyweight Locking)
- 适用场景:多线程持续竞争锁(自旋无法解决)。
- 核心逻辑:
- 锁升级为重量级锁后,对象 Mark Word 会指向 Monitor 对象的地址,锁标记改为 “重量级锁”;
- 竞争失败的线程不再自旋,而是进入 Monitor 的
_EntryList阻塞(用户态→内核态切换,开销大); - 锁释放时唤醒阻塞线程,重新竞争锁。
锁升级流程总结:无锁 → (单线程获取)偏向锁 → (多线程交替竞争)轻量级锁(自旋) → (多线程持续竞争)重量级锁。
3. JVM 对 synchronized 的优化:锁消除、锁粗化
JDK 1.6 除了锁升级,还提供了 锁消除 和 锁粗化 两种编译期 / 运行期优化,进一步降低 synchronized 的开销:
(1)锁消除(Lock Elimination)
- 定义:JVM 编译器在编译阶段,检测到某些锁对象不存在多线程竞争的可能,直接消除该锁的获取 / 释放逻辑。
- 核心原理:基于 “逃逸分析”(Escape Analysis)—— 判断对象是否仅在当前线程内使用(未逃逸到其他线程),若未逃逸,则该对象的锁无竞争意义。
- 示例:
JVM 经逃逸分析发现public String concat(String a, String b) { // StringBuffer 本身是线程安全的,内部方法有 synchronized 修饰 StringBuffer sb = new StringBuffer(); sb.append(a).append(b); return sb.toString(); }sb仅在concat方法内创建和使用,未逃逸到外部,会消除sb.append()中的synchronized锁,提升性能。
(2)锁粗化(Lock Coarsening)
- 定义:JVM 将多个连续的、对同一把锁的获取 / 释放操作,合并为一次获取 / 释放,减少锁的频繁切换开销。
- 适用场景:循环中频繁加锁 / 解锁(如 for 循环内的 synchronized 块)。
- 示例:
锁粗化避免了 1000 次锁的获取 / 释放操作,大幅降低开销。// 优化前:循环内每次迭代都加锁、解锁 for (int i = 0; i < 1000; i++) { synchronized (lock) { list.add(i); } } // JVM 锁粗化后:仅在循环外加一次锁、解锁 synchronized (lock) { for (int i = 0; i < 1000; i++) { list.add(i); } }
二、总结
synchronized底层依赖 对象监视器(Monitor) 实现,核心关联 Java 对象头(Mark Word)和monitorenter/monitorexit指令(方法级同步依赖ACC_SYNCHRONIZED标记);- 锁升级是 JDK 1.6 的核心优化,遵循 “无锁→偏向锁→轻量级锁→重量级锁” 的不可逆流程,按需升级以降低锁开销;
- JVM 还通过 锁消除(基于逃逸分析消除无竞争锁)和 锁粗化(合并连续锁操作)进一步优化
synchronized的性能。
二.volatile核心原理
1. volatile 的核心特性及底层原理(内存可见性、禁止指令重排)
volatile 是 Java 提供的轻量级同步机制,核心作用是保证变量的内存可见性和禁止指令重排,其底层完全依赖 CPU 的 内存屏障(Memory Barrier) 实现。
(1)内存可见性原理
-
问题背景:多线程环境下,每个线程有自己的工作内存(CPU 缓存),线程读取变量时会先从主内存加载到工作内存,修改后仅更新工作内存,不会立即同步到主内存;其他线程无法感知该变量的修改,导致 “内存不可见”。
-
volatile 解决逻辑:
- 当线程修改
volatile变量时,JVM 会触发 写屏障(Store Barrier):强制将工作内存中的变量值刷新到主内存,而非仅停留在 CPU 缓存; - 当线程读取
volatile变量时,JVM 会触发 读屏障(Load Barrier):强制清空工作内存中该变量的缓存,从主内存重新加载最新值; - 底层通过 CPU 的
sfence(写屏障)、lfence(读屏障)或mfence(全屏障)指令实现,确保变量的读写直接操作主内存。
- 当线程修改
-
示例理解:
// 线程A volatile boolean flag = false; public void setFlag() { flag = true; // 写屏障:立即刷到主内存 } // 线程B public void loop() { while (!flag) { // 读屏障:每次都从主内存读最新值 // 无操作 } System.out.println("flag已更新"); }若
flag不加volatile,线程 B 可能一直读取工作内存中的旧值(false),陷入死循环;加volatile后,线程 B 能立即感知线程 A 的修改。
(2)禁止指令重排原理
-
问题背景:JVM 编译器和 CPU 为了优化执行效率,会对无依赖关系的指令进行 “重排序”(如:
int a=1; int b=2;可能被重排为int b=2; int a=1;),但多线程下重排可能导致逻辑错误(如双重检查锁的单例问题)。 -
volatile 解决逻辑:JVM 为
volatile变量的读写操作插入 内存屏障,限制指令重排的范围:- 在
volatile变量写操作前:插入 StoreStore 屏障,禁止写操作之前的普通指令重排到写操作之后; - 在
volatile变量写操作后:插入 StoreLoad 屏障,禁止写操作之后的指令重排到写操作之前; - 在
volatile变量读操作前:插入 LoadLoad 屏障,禁止读操作之前的普通指令重排到读操作之后; - 在
volatile变量读操作后:插入 LoadStore 屏障,禁止读操作之后的指令重排到读操作之前。
- 在
-
经典场景:双重检查锁(DCL)的单例模式
// 正确的DCL单例(instance必须加volatile) public class Singleton { private static volatile Singleton instance; // 禁止指令重排 private Singleton() {} public static Singleton getInstance() { if (instance == null) { // 第一次检查 synchronized (Singleton.class) { if (instance == null) { // 第二次检查 instance = new Singleton(); // 不加volatile会重排 } } } return instance; } }解释:
instance = new Singleton()实际分 3 步:① 分配内存 ② 初始化对象 ③ 指向内存地址。不加volatile时,JVM 可能重排为 ①→③→②,导致其他线程拿到 “未初始化完成” 的实例;加volatile后,禁止该重排,确保实例完全初始化后才被其他线程可见。
2. volatile 与 happens-before 原则的关联
happens-before(先行发生)是 Java 内存模型(JMM)的核心原则,定义了多线程操作的可见性规则,volatile 直接关联其中两条关键规则:
(1)volatile 变量规则
- 规则内容:对一个
volatile变量的写操作,happens-before 于后续对该变量的读操作。 - 通俗理解:线程 A 修改了
volatile变量,线程 B 后续读取该变量时,一定能看到线程 A 的修改结果(这也是内存可见性的底层规则支撑)。
(2)传递性规则
- 规则内容:如果 A happens-before B,B happens-before C,那么 A happens-before C。
- 与 volatile 结合示例:
根据规则:// 线程A执行: int a = 1; // 操作1 volatile int b = 2; // 操作2(volatile写) // 线程B执行: int c = b; // 操作3(volatile读) int d = a; // 操作4- 操作 1 happens-before 操作 2(程序次序规则);
- 操作 2 happens-before 操作 3(volatile 变量规则);
- 操作 3 happens-before 操作 4(程序次序规则);
- 因此操作 1 happens-before 操作 4(传递性),线程 B 读取
a时能看到线程 A 设置的a=1。
3. volatile 无法保证原子性的原因及解决办法
(1)无法保证原子性的核心原因
- 原子性定义:一个操作(或多个操作)要么全部执行且执行过程不被中断,要么全部不执行。
- volatile 缺陷:仅保证可见性和禁止重排,但不保证复合操作的原子性。
- 典型示例:
volatile int count = 0;执行count++:count++实际分 3 步:① 读取count值 ② 加 1 ③ 写回新值。即使count加了volatile,多线程下仍会出现 “指令交错”:plaintext
线程1:读count=0 → 加1=1(未写回) 线程2:读count=0 → 加1=1(未写回) 线程1:写回count=1 线程2:写回count=1 最终count=1(预期应为2)
(2)解决办法
针对 volatile 原子性不足的问题,有 3 种常用解决方案:
方案 1:使用 synchronized 关键字
通过同步锁将复合操作变为原子操作,适合低并发场景:
volatile int count = 0;
public void increment() {
synchronized (this) { // 加锁保证count++原子性
count++;
}
}
方案 2:使用 JUC 原子类(推荐)
Java 并发包(java.util.concurrent.atomic)提供了原子操作类(如 AtomicInteger、AtomicLong),底层基于 CAS(Compare-And-Swap)实现无锁原子操作,性能优于 synchronized:
AtomicInteger count = new AtomicInteger(0); // 无需加volatile
public void increment() {
count.incrementAndGet(); // 原子自增,底层CAS操作
}
解释:CAS 包含 3 个参数(内存地址、预期值、新值),仅当内存中的值等于预期值时,才将其更新为新值,全程无锁,避免线程阻塞。
方案 3:使用 Lock 锁
通过 ReentrantLock 显式锁实现原子操作,灵活性更高(支持公平锁、可中断等):
volatile int count = 0;
Lock lock = new ReentrantLock();
public void increment() {
lock.lock(); // 加锁
try {
count++;
} finally {
lock.unlock(); // 必须在finally中释放锁
}
}
二、总结
volatile的核心能力是内存可见性(通过读写屏障同步主内存与工作内存)和禁止指令重排(通过内存屏障限制重排范围),底层依赖 CPU 内存屏障指令实现;volatile是happens-before原则的重要体现,其写操作先行发生于后续读操作,且支持规则传递性;volatile无法保证复合操作的原子性,解决办法优先选择Atomic原子类(无锁高效),其次是synchronized或Lock锁。
更多推荐



所有评论(0)