《写多线程总踩坑?这篇让你从 “线程状态” 到 “锁机制”,把坑踩明白》
本文介绍了Java线程的六种状态(NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED)及其转换条件,重点分析了多线程环境下的线程安全问题。线程不安全的核心原因是共享资源的并发修改导致原子性、可见性和有序性被破坏。文章详细阐述了通过synchronized关键字实现加锁机制的解决方案,包括对象锁和类锁的区别、可重入锁特性以及锁粒度优化策略。最后
一、线程的状态
1.线程状态的枚举类型(Thread.State)
Java 中线程的状态是一个枚举类型 Thread.State,通过代码可以直接打印所有状态:

执行后会输出 6 种状态:NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED。
2、各线程状态的具体含义
1. NEW(初始状态)
- 含义:线程已创建但未启动(即只执行了
new Thread(),还没调用start()方法)。 - 示例:

2. RUNNABLE(可运行状态)
- 含义:线程可被 CPU 调度执行,包含两种子状态:
- 就绪(READY):线程已准备好,等待 CPU 分配时间片。
- 运行中(RUNNING):线程正在 CPU 上执行代码。
- 注意:这两个子状态在
Thread.State中统一归为RUNNABLE,因为 Java 层面无法区分 “就绪” 和 “运行中”(这是操作系统线程调度的细节)。 - 触发场景:调用
start()后线程进入RUNNABLE;线程从BLOCKED、WAITING、TIMED_WAITING状态恢复后,也会回到RUNNABLE。
3. BLOCKED(阻塞状态)
- 含义:线程因竞争
synchronized锁失败,被阻塞在锁的入口处,等待获取锁。 - 触发场景:多个线程竞争同一把
synchronized锁时,未抢到锁的线程会进入BLOCKED状态。 - 示例:

4. WAITING(无限等待状态)
- 含义:线程无超时地等待某个事件发生,直到被显式唤醒,否则会一直阻塞。
- 触发方法:
Object.wait()(无超时版)Thread.join()(无超时版)LockSupport.park()
- 唤醒方式:
Object.notify()/Object.notifyAll()Thread.interrupt()(中断)LockSupport.unpark(Thread)
5. TIMED_WAITING(超时等待状态)
- 含义:线程有超时时间地等待某个事件,超时后会自动唤醒,若在超时前被显式唤醒也会提前恢复。
- 触发方法:
Thread.sleep(long millis)Object.wait(long timeout)(带超时版)Thread.join(long millis)(带超时版)LockSupport.parkNanos(long nanos)/LockSupport.parkUntil(long deadline)
- 唤醒方式:
- 显式唤醒(同
WAITING的唤醒方式) - 超时自动唤醒
- 显式唤醒(同
6. TERMINATED(终止状态)
- 含义:线程的
run()方法执行完毕,或因未捕获的异常导致线程终止。 - 示例:

二、多线程带来的的⻛险-线程安全
观察线程不安全:

一、线程安全的概念
线程安全(是指多线程环境下,一段代码或一个数据结构在并发执行时,始终能表现出符合预期的、正确的行为,不会出现数据错乱、逻辑异常或不可预测的结果。)
简单来说:
- 单线程环境下 “正确” 的代码,在多线程并发访问时,依然能保证结果正确、状态一致,就是线程安全的;
- 反之,若多线程执行时出现数据错误(如计数不准)、逻辑混乱(如流程跳转异常),则是线程不安全的。
线程安全的核心目标:
- 原子性:关键操作(如 “读取 - 修改 - 写入”)不可分割,要么全部执行,要么全部不执行,不会被其他线程打断;
- 可见性:一个线程对共享变量的修改,能被其他线程及时感知到;
- 有序性:线程执行的指令不会因 CPU 指令重排序(优化行为)导致逻辑混乱。
Java内存模型(JMM):Java虚拟机规范中定义了Java内存模型.
- ⽬的是屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到⼀致的并发效果.

- 线程之间的共享变量存在主内存(MainMemory).
- 每⼀个线程都有⾃⼰的"⼯作内存"(WorkingMemory)
二、线程不安全的原因
线程不安全的本质是:多线程并发访问共享资源(如共享变量、文件、数据库连接等)时,破坏了 “原子性、可见性、有序性” 中的一个或多个,导致资源状态被错误修改或读取。
核心原因可归纳为以下 4 点,结合实例理解更清晰:
1. 共享资源的并发修改(根本原因)
多线程同时操作共享可变资源(即多个线程都能访问且能修改的资源,如全局变量、静态变量、堆内存中的对象属性等),是线程不安全的前提。
- 若资源是 “私有” 的(如线程局部变量
ThreadLocal)或 “不可变” 的(如final修饰的常量、字符串),则天然线程安全; - 若资源是 “共享且可变” 的,且无任何并发控制,必然导致冲突。
2. 非原子操作的拆分(直接原因)
很多看似 “单一” 的操作(如 count++、a = b + 1),在底层会被拆分为多个 CPU 指令,这些指令之间可能被其他线程打断,导致操作不完整。
以 count++ 为例,底层拆解为 3 步(非原子):
- 读取:从内存中读取
count的当前值(如 0); - 修改:在 CPU 中对值进行 +1 运算(0→1);
- 写入:将修改后的值写回内存(1 写入
count)。
冲突场景:
- 线程 1 执行完 “读取(0)→ 修改(1)” 后,还没来得及写入内存;
- 线程 2 此时读取
count,拿到的还是 0,执行 “修改(1)→ 写入(1)”; - 线程 1 再将自己的 1 写入内存,最终
count为 1(而非预期的 2)。
3. 内存可见性问题(隐藏原因)
CPU 为了提升效率,会将共享变量缓存到线程本地(寄存器、高速缓存),而非每次都直接访问主内存。这会导致:
- 线程 A 修改了共享变量,但只更新了本地缓存,未同步到主内存;
- 线程 B 读取该变量时,从主内存读取到旧值(未感知到 A 的修改),出现 “可见性问题”。
4. 指令重排序(隐藏原因)
CPU 和编译器为了优化性能,会在不影响单线程执行结果的前提下,对指令的执行顺序进行重新排序。但在多线程环境下,重排序可能破坏线程间的逻辑依赖,导致有序性问题。
实例:线程 1 初始化对象 obj,线程 2 读取 obj 并使用,但重排序可能导致 “obj 未初始化完成就被线程 2 访问”:
- 单线程下,
init()中 “指令 1→指令 2” 和 “指令 2→指令 1” 的结果一致(都是初始化完成); - 多线程下,若
init()被重排序为 “指令 2→指令 1”,线程 2 可能在obj未创建时就进入if分支,调用obj.toString()抛出空指针。
总结
- 线程安全的核心是保证多线程并发访问共享资源时的原子性、可见性、有序性;
- 线程不安全的根本原因是共享可变资源的并发修改,直接 / 间接原因是非原子操作、内存可见性缺失、指令重排序。
三、解决线程安全问题
1、加锁机制(synchronized关键字-监视器锁 monitor lock )
synchronized的特性
加锁的核心是利用互斥性,保证同一时间只有一个线程能执行临界区代码。
synchronized的使用方式:- 锁对象:
synchronized (锁对象) { 线程安全的代码 },进入代码块时加锁,离开时自动解锁。 - 修饰普通方法:相当于对
this对象加锁,即public synchronized void method() { ... }等价于synchronized (this) { ... }。 - 修饰静态方法:相当于对类对象加锁,即
public static synchronized void method() { ... }等价于synchronized (类名.class) { ... }。 - 普通同步方法:对象级锁 → 同一个对象抢同一个方法才排队,不同对象互不影响;
- 静态同步方法:类级锁 → 不管多少个对象,抢这个静态方法都要排队(锁的是整个类)。
- 锁对象:
1.1synchronized 修饰普通方法(锁当前对象 this)
想象:有一个 “厕所” 对象,里面有一个 “上厕所” 的同步方法。
同一个厕所对象:多个人(线程)要上,必须排队(同一时间只能一个人用)
不同厕所对象:各用各的,不用排队(互相不影响)

![]()
1.2synchronized 修饰静态方法(锁类对象.class)
想象:厕所的 “大门” 是同步静态方法(比如 “进入厕所区域”)。
- 不管你想上哪个厕所(不管创建多少个 Toilet 对象),都要先过大门,同一时间只能一个人进。

总结(一句话分清):
- 普通同步方法:对象级锁 → 同一个对象抢同一个方法才排队,不同对象互不影响;
- 静态同步方法:类级锁 → 不管多少个对象,抢这个静态方法都要排队(锁的是整个类)。
- 互斥的前提:多个线程必须针对同一个锁对象加锁,才会产生锁竞争(互斥)。若线程持有的是不同锁,则不会互斥。


2.理解"阻塞等待"
针对每⼀把锁,操作系统内部都维护了⼀个等待队列.当这个锁被某个线程占有的时候,其他线程尝试 进⾏加锁,就加不上了,就会阻塞等待,⼀直等到之前的线程解锁之后,由操作系统唤醒⼀个新的线程, 再来获取到这个锁.
注意:
- 上⼀个线程解锁之后,下⼀个线程并不是⽴即就能获取到锁.⽽是要靠操作系统来"唤醒".这也就 是操作系统线程调度的⼀部分⼯作.
- 假设有ABC三个线程,线程A先获取到锁,然后B尝试获取锁,然后C再尝试获取锁,此时B和C都在阻塞队列中排队等待.但是当A释放锁之后,虽然B⽐C先来的,但是B不⼀定就能获取到锁,⽽是和C重新竞争,并不遵守先来后到的规则.
3.不可重入锁的 “自我锁死” 与可重入锁的解决逻辑
死锁演示代码(模拟 “自我锁死”)

可重入锁演示代码(Java synchronized 天然支持)

说明:method1和method2都被synchronized修饰(锁对象都是this)。线程调用method1时持有this锁,调用method2时可直接重入,不会阻塞,最终能正常执行并修改count。
- 死锁:多个进程 / 线程在竞争资源时,互相等待对方持有的资源,导致所有参与方都无法继续推进的永久阻塞状态。
- 可重入是单线程、同一把锁的重复加锁能力,是
synchronized的设计优势,用于避免线程自己阻塞自己。
| 维度 | 不可重入锁 | 可重入锁(以 synchronized 为例) |
|---|---|---|
| 核心问题 | 线程未释放已持有锁时,再次加锁会阻塞,因需等待自身释放锁,最终死锁(“把自己锁死”) | 允许同一线程多次获取同一把锁,不会因重复加锁阻塞,避免自我死锁 |
| 实现逻辑 | 锁未记录持有线程的重入次数,只要锁被占用,同一线程再次申请也会被视为 “竞争” | 内部维护线程持有计数,同一线程每次加锁计数 + 1,解锁计数 - 1,计数为 0 时释放锁 |
| 场景示例 | 如 “进 WC 锁门后失忆,在外抱怨里面的人不出来”,线程自我阻塞 | 线程调用synchronized修饰的方法 A,方法 A 中又调用同锁的方法 B,可正常执行(因是同一线程,锁可重入) |
| 实际应用 | 因易死锁,实际开发中极少使用 | Java 中synchronized、ReentrantLock均为可重入锁,是并发编程中保障线程安全的常用手段 |
简单来说,可重入锁通过 “记录线程重入次数” 的机制,解决了不可重入锁 “自我锁死” 的问题,让同一线程能安全地多次获取同一把锁,是应对复杂并发场景的关键设计。
- 锁对象必须是「成员变量」(类里定义,不是方法里),加
final修饰,避免被修改; - 锁对象类型固定用
new Object(),干净无坑,不用想其他类型; - 多把锁时,只改变量名(lockA、lockB、lockC...),每个变量对应一个独立的锁对象,保护不同临界区;
- 简单场景(比如整个方法都要保护):直接用
synchronized修饰方法(锁this),不用手动创建锁对象,比如:

- 锁对象是 “钥匙”,临界区是 “门”,同一把钥匙才能锁门互斥,钥匙要唯一(成员变量),钥匙类型选最干净的 Object
- 锁的本质效果:锁住 “临界区代码的执行权”—— 确保同一时间只有一个拿到锁的线程能执行代码,防止多个线程同时操作共享资源(比如修改同一个变量)导致数据错乱。
4.锁粒度细化:一个方法里有多个 “独立的临界区”(不是 “边界方法”),创建多个锁对象的目的,是让每个锁对应一个独立临界区,实现 “不同临界区互不干扰、各自互斥”
代码示例:

总结:
- 先判断方法里的临界区是否独立:如果多个临界区操作的是不同共享变量,且互不影响 → 用多把锁;
- 锁对象命名规范:
共享变量名 + Lock(比如balanceLock保护balance),一眼就能对应上,不会混淆; - 锁对象创建:
private final Object 专属锁名 = new Object();,每个独立临界区一个,final 修饰避免被修改; - 核心原则:锁只保护需要保护的最小范围,独立临界区用独立锁,不浪费并发效率。
一句话记住:多临界区→多锁,一一对应,互不干扰,又快又安全
总结:
- 多独立临界区 + 多把锁:抢同一把锁的线程互斥等待,抢不同锁的线程并行执行—— 不用等其他锁释放,效率更高;
- 锁的获取规则:同一把锁释放后,哪个线程被 CPU 调度到,哪个就先拿到锁(不是 “先等先得”,是调度优先);
- 线程执行逻辑:进入方法后,非临界区并发执行,遇到哪个临界区就抢对应锁,抢到就执行,没抢到就阻塞,执行完释放锁,继续往下走;
- 实操要点:独立临界区一定要用独立锁(变量名对应,比如
balanceLock对应改余额),避免用一把锁导致所有临界区串行,浪费效率。
一句话记住:多锁对应多临界区,同锁互斥,异锁并行,CPU 调度决定谁先拿锁
非临界区并发跑,遇锁抢锁分两道:抢到直接执行,抢不到队列等;锁释放唤醒一个(cpu随机调度),继续往下执行不跑偏,不同锁互不干扰
2. 监视器锁(monitor lock)
- 基本定义:监视器锁是 JVM(Java 虚拟机)中用于描述锁机制的术语,与
synchronized关键字的实现密切相关。 - 实际意义:当程序在使用锁的过程中抛出异常时,报错信息中可能会出现 “监视器锁” 的表述,它是 JVM 层面对锁竞争、锁状态等问题的底层描述方式,帮助开发者理解并发场景中锁的行为与异常根源。
- 简单说,“监视器锁” 在报错里,就是个 “提示词,帮你快速把错误归到 “锁相关” 类别,再通过具体描述判断是死锁、超时还是单纯阻塞,不用在业务逻辑里瞎找问题
更多推荐


所有评论(0)