一、线程的状态

1.线程状态的枚举类型(Thread.State

Java 中线程的状态是一个枚举类型 Thread.State,通过代码可以直接打印所有状态:

执行后会输出 6 种状态NEWRUNNABLEBLOCKEDWAITINGTIMED_WAITINGTERMINATED

2、各线程状态的具体含义

1. NEW(初始状态)
  • 含义:线程已创建但未启动(即只执行了 new Thread(),还没调用 start() 方法)。
  • 示例:

2. RUNNABLE(可运行状态)
  • 含义:线程可被 CPU 调度执行,包含两种子状态:
    • 就绪(READY):线程已准备好,等待 CPU 分配时间片。
    • 运行中(RUNNING):线程正在 CPU 上执行代码。
  • 注意:这两个子状态在 Thread.State 中统一归为 RUNNABLE,因为 Java 层面无法区分 “就绪” 和 “运行中”(这是操作系统线程调度的细节)。
  • 触发场景:调用 start() 后线程进入 RUNNABLE;线程从 BLOCKEDWAITINGTIMED_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() 方法执行完毕,或因未捕获的异常导致线程终止。
  • 示例:

二、多线程带来的的⻛险-线程安全

观察线程不安全:

一、线程安全的概念

线程安全(是指多线程环境下,一段代码或一个数据结构在并发执行时,始终能表现出符合预期的、正确的行为,不会出现数据错乱、逻辑异常或不可预测的结果。)

简单来说:

  • 单线程环境下 “正确” 的代码,在多线程并发访问时,依然能保证结果正确、状态一致,就是线程安全的;
  • 反之,若多线程执行时出现数据错误(如计数不准)、逻辑混乱(如流程跳转异常),则是线程不安全的。
线程安全的核心目标:
  1. 原子性:关键操作(如 “读取 - 修改 - 写入”)不可分割,要么全部执行,要么全部不执行,不会被其他线程打断;
  2. 可见性:一个线程对共享变量的修改,能被其他线程及时感知到;
  3. 有序性:线程执行的指令不会因 CPU 指令重排序(优化行为)导致逻辑混乱。

Java内存模型(JMM):Java虚拟机规范中定义了Java内存模型.

  • ⽬的是屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到⼀致的并发效果.

  • 线程之间的共享变量存在主内存(MainMemory).
  • 每⼀个线程都有⾃⼰的"⼯作内存"(WorkingMemory)

二、线程不安全的原因

线程不安全的本质是:多线程并发访问共享资源(如共享变量、文件、数据库连接等)时,破坏了 “原子性、可见性、有序性” 中的一个或多个,导致资源状态被错误修改或读取。

核心原因可归纳为以下 4 点,结合实例理解更清晰:

1. 共享资源的并发修改(根本原因)

多线程同时操作共享可变资源(即多个线程都能访问且能修改的资源,如全局变量、静态变量、堆内存中的对象属性等),是线程不安全的前提。

  • 若资源是 “私有” 的(如线程局部变量 ThreadLocal)或 “不可变” 的(如 final 修饰的常量、字符串),则天然线程安全;
  • 若资源是 “共享且可变” 的,且无任何并发控制,必然导致冲突。
2. 非原子操作的拆分(直接原因)

很多看似 “单一” 的操作(如 count++a = b + 1),在底层会被拆分为多个 CPU 指令,这些指令之间可能被其他线程打断,导致操作不完整。

以 count++ 为例,底层拆解为 3 步(非原子):

  1. 读取:从内存中读取 count 的当前值(如 0);
  2. 修改:在 CPU 中对值进行 +1 运算(0→1);
  3. 写入:将修改后的值写回内存(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 天然支持)

    说明method1method2都被synchronized修饰(锁对象都是this)。线程调用method1时持有this锁,调用method2时可直接重入,不会阻塞,最终能正常执行并修改count

    • 死锁:多个进程 / 线程在竞争资源时,互相等待对方持有的资源,导致所有参与方都无法继续推进的永久阻塞状态
    • 可重入是单线程、同一把锁的重复加锁能力,是synchronized的设计优势,用于避免线程自己阻塞自己。
    维度 不可重入锁 可重入锁(以 synchronized 为例)
    核心问题 线程未释放已持有锁时,再次加锁会阻塞,因需等待自身释放锁,最终死锁(“把自己锁死”) 允许同一线程多次获取同一把锁,不会因重复加锁阻塞,避免自我死锁
    实现逻辑 锁未记录持有线程的重入次数,只要锁被占用,同一线程再次申请也会被视为 “竞争” 内部维护线程持有计数,同一线程每次加锁计数 + 1,解锁计数 - 1,计数为 0 时释放锁
    场景示例 如 “进 WC 锁门后失忆,在外抱怨里面的人不出来”,线程自我阻塞 线程调用synchronized修饰的方法 A,方法 A 中又调用同锁的方法 B,可正常执行(因是同一线程,锁可重入)
    实际应用 因易死锁,实际开发中极少使用 Java 中synchronizedReentrantLock均为可重入锁,是并发编程中保障线程安全的常用手段

    简单来说,可重入锁通过 “记录线程重入次数” 的机制,解决了不可重入锁 “自我锁死” 的问题,让同一线程能安全地多次获取同一把锁,是应对复杂并发场景的关键设计。

    1. 锁对象必须是「成员变量」(类里定义,不是方法里),加final修饰,避免被修改;
    2. 锁对象类型固定用new Object()干净无坑,不用想其他类型;
    3. 多把锁时,只改变量名(lockA、lockB、lockC...),每个变量对应一个独立的锁对象,保护不同临界区;
    4. 简单场景(比如整个方法都要保护):直接用synchronized修饰方法(锁this),不用手动创建锁对象,比如:

    • 锁对象是 “钥匙”,临界区是 “门”,同一把钥匙才能锁门互斥,钥匙要唯一(成员变量),钥匙类型选最干净的 Object
    • 锁的本质效果:锁住 “临界区代码的执行权”—— 确保同一时间只有一个拿到锁的线程能执行代码,防止多个线程同时操作共享资源(比如修改同一个变量)导致数据错乱。

    4.锁粒度细化:一个方法里有多个 “独立的临界区”(不是 “边界方法”),创建多个锁对象的目的,是让每个锁对应一个独立临界区,实现 “不同临界区互不干扰、各自互斥”

    代码示例:

    总结:

    1. 先判断方法里的临界区是否独立:如果多个临界区操作的是不同共享变量,且互不影响 → 用多把锁;
    2. 锁对象命名规范:共享变量名 + Lock(比如balanceLock保护balance),一眼就能对应上,不会混淆;
    3. 锁对象创建:private final Object 专属锁名 = new Object();,每个独立临界区一个,final 修饰避免被修改;
    4. 核心原则:锁只保护需要保护的最小范围,独立临界区用独立锁,不浪费并发效率

    一句话记住:多临界区→多锁,一一对应,互不干扰,又快又安全

    总结

    1. 多独立临界区 + 多把锁:抢同一把锁的线程互斥等待,抢不同锁的线程并行执行—— 不用等其他锁释放,效率更高;
    2. 锁的获取规则:同一把锁释放后,哪个线程被 CPU 调度到,哪个就先拿到锁(不是 “先等先得”,是调度优先);
    3. 线程执行逻辑:进入方法后,非临界区并发执行,遇到哪个临界区就抢对应锁,抢到就执行,没抢到就阻塞,执行完释放锁,继续往下走;
    4. 实操要点:独立临界区一定要用独立锁(变量名对应,比如balanceLock对应改余额),避免用一把锁导致所有临界区串行,浪费效率。

    一句话记住:多锁对应多临界区,同锁互斥,异锁并行,CPU 调度决定谁先拿锁

    非临界区并发跑,遇锁抢锁分两道:抢到直接执行,抢不到队列等;锁释放唤醒一个(cpu随机调度),继续往下执行不跑偏,不同锁互不干扰

    2. 监视器锁(monitor lock)

    • 基本定义:监视器锁是 JVM(Java 虚拟机)中用于描述锁机制的术语,与synchronized关键字的实现密切相关
    • 实际意义:当程序在使用锁的过程中抛出异常时,报错信息中可能会出现 “监视器锁” 的表述,它是 JVM 层面对锁竞争、锁状态等问题的底层描述方式,帮助开发者理解并发场景中锁的行为与异常根源
    • 简单说,“监视器锁” 在报错里,就是个 “提示词,帮你快速把错误归到 “锁相关” 类别,再通过具体描述判断是死锁、超时还是单纯阻塞,不用在业务逻辑里瞎找问题
    Logo

    有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

    更多推荐