笔记:JUC

一、TODO


二、脑图

  1. Xmind

  2. Edraw

  3. Hexo 地址
    👉 http://blog.wangjia.ink/2025/11/23/笔记:JUC/

  4. 代码洪荒(面试宝典)
    👉 https://www.notion.so/JUC-2b2974fbda5e8012975ef0197d6e8237


三、JUC 前置基础

1. 线程的状态

1.1. 操作系统层

操作系统层的线程状态,是操作系统内核对线程的实际调度反映


1.2. Java 应用层

Java 应用层的线程状态,是 JVM 对线程生命周期的抽象和管理,主要反映线程在 Java 内存模型中的行为

  1. NEW
    1. Java 应用层,Thread 实例已创建,但尚未调用 Thread#start,处于未启动状态
    2. 需要注意的是,我们只是创建了线程实例,但是该实例尚未与操作系统层面的线程建立关联
  2. RUNNABLE
    1. 可运行状态
      1. 当我们调用 Thread#start 后,JVM 会在操作系统层面请求创建一个本地线程
      2. 一旦操作系统创建了这个本地线程,并将其与 Thread 实例 “绑定”,Thread 实例就会进入可运行状态
      3. 可运行状态表示 Thread 实例已经准备好运行了,然而,能否真正运行还要取决于操作系统的调度
    2. 运行状态
      1. 本地线程被分配到 CPU 时间片后,开始执行 Java 代码
    3. 阻塞状态
      1. 阻塞状态其实就是本地线程进入 IO 阻塞
      2. 需要注意的是:
        1. Java 应用层的 IO 阻塞、BLOCKEDWAITINGTIMED_WAITING,在操作系统层都属于阻塞状态,CPU 不再调用这个线程
        2. IO 阻塞严格来说是操作系统层的阻塞,但是我们没必要划分的那么清晰,知道它是什么东西即可
  3. TERMINATED
    1. 当线程执行完任务后,线程实例就会进入TERMINATED 状态
    2. Java 层面上,线程是一个线程对象,当这个线程对象不再被引用时,会在下一次 GC 时被垃圾回收。

2. 线程相关分类

Java 应用层,线程分为:

  1. 用户线程
  2. 守护线程

我们平时创建的 Thread 实例默认都是用户线程,main 方法的主线程也是一个用户线程。很多人误以为:只要主线程结束后,JVM 进程就会结束。事实上,这种理解是错误的,只要仍有用户线程在运行,JVM 进程就会继续存活。

但是守护线程的行为则非如此,当所有用户线程都执行完毕后,JVM 进程将自动退出,而不管是否还有守护线程在运行。此时守护线程会被强制终止,其 finally 块中的代码也不保证一定执行。因此守护线程常用于后台服务,例如垃圾回收、心跳监控、日志清理等任务


3. 线程阻塞相关分类

  1. IO 阻塞
    1. Java 程序在与操作系统交互时,由操作系统层代我们执行的阻塞,让我们的本地线程进入阻塞状态
    2. 虽然 IO 阻塞在操作系统层面也属于阻塞状态,但对 Java 应用层来说,它并不能直接感知本地线程是否正处于 IO 阻塞,因此 Thread 实例的状态仍为 RUNNABLE。简单来说就是:因为感知不到,所以认为还在运行
  2. BLOCKED 阻塞、WAITING 阻塞、TIMED_WAITING 阻塞
    1. Java 应用层还提供了多种 API 让本地线程主动进入阻塞状态,Thread 实例进入 BWTW 状态
    2. 因为调用了 Java 应用层提供的 API,所以 Java 应用层能知道线程进入了阻塞状态,并且还知道线程进入阻塞状态的 “原因”,所以 Thread 实例能进入对应的 BLOCKEDWAITINGTIMED_WAITING 状态
    3. Java 应用层提供的相关 API 主要包括:
      1. Thread.sleep
        1. 本地线程进入阻塞状态,Thread 实例进入 TIMED_WAITING 状态,等待被中断(Thread#interrupt)、阻塞超时
      2. 基于 Monitor 的阻塞
        1. 竞争 Monitor 锁失败
          1. synchronized
            1. 本地线程进入阻塞状态,Thread 实例进入 BLOCKED 状态,并被投递到 MonitorEntryList 队列(竞争队列),等待被唤醒(Monitor 锁被释放时,由 JVM 唤醒)
        2. Object#wait
          1. Object#wait()
            1. 本地线程进入阻塞状态,Thread 实例进入 WAITING 状态,并被投递到 MonitorWaitSet 队列(等待队列),等待被唤醒(Object#notify)、被中断
          2. Object#wait(long timeoutMillis)
            1. 本地线程进入阻塞状态,Thread 实例进入 TIMED_WAITING 状态,并被投递到 MonitorWaitSet 队列(等待队列),等待被唤醒(Object#notify)、被中断、阻塞超时
        3. Thread#join
          1. Thread#join()
            1. 没什么好说的,因为其本质是基于 Object#wait 实现的阻塞
          2. Thread#join(final long millis)
          3. Thread#join(long millis, int nanos)
        4. 需要注意的是:
          1. Object#waitThread#join 都会使 Thread 实例进入 Monitor 的等待队列。那么为什么要进入等待队列,而不是进入竞争队列呢?
          2. 进入等待队列是因为 Thread 实例在竞争到 Monitor 锁后,发现某个条件尚未满足,为了不影响其他线程继续获取 Monitor 锁,所以会主动释放锁,并进入等待队列中等待条件被满足
          3. 所以 Object#waitThread#join 的前置条件是:竞争到 Monitor 锁。只有先竞争到 Monitor 锁,才知道某个条件尚未满足
          4. 我的意思是:Object#waitThread#joinsynchronized 是绑定的,它们必须出现在 synchronized 代码块中,否则会抛出异常。而 Thread.sleep、基于 Park 的阻塞没有这种限制,可以在任何地方使用
          5. 需要注意的是:如果竞争到 Monitor 锁,但是使用了 Thread.sleep、基于 Park 的阻塞的情况下,Monitor 锁是不会被释放的
      3. 基于 Park 的阻塞
        1. LockSupport.park
          1. LockSupport.park()
            1. 本地线程进入阻塞状态,Thread 实例进入 WAITING 状态,等待被唤醒(LockSupport.unpark)、被中断(Thread#interrupt
          2. LockSupport.parkNanos(long nanos)
            1. 本地线程进入阻塞状态,Thread 实例进入 TIMED_WAITING 状态,等待被唤醒(LockSupport.unpark)、被中断(Thread#interrupt)、阻塞超时
          3. LockSupport.parkUntil(long deadline)
            1. 本地线程进入阻塞状态,Thread 实例进入 TIMED_WAITING 状态,等待被唤醒(LockSupport.unpark)、被中断(Thread#interrupt)、阻塞超时
          4. 需要注意的是,具体的过程比上述描述稍微复杂一点,详见源码:LockSupportobsidian 内部链接:源码:java.util.concurrent.locks.LockSupport源码解析Hexo 链接: http://blog.wangjia.ink/2025/11/13/源码:java.util.concurrent.locks.LockSupport源码解析/

[!NOTE] 注意事项

  1. IO 阻塞、Threa.sleep 都依赖于操作系统层的队列
  2. 基于 Monitor 的阻塞依赖于 Java 应用层的 Monitor 中的 EntryListWaitSet 队列
  3. 而基于 Park 的阻塞,原则上是不依赖于任何数据结构,不过在实际中,几乎所有基于 Park 阻塞的产品,都会维护一个数据结构(可能是队列,也可能是其他数据结构)。因为如果没有这个数据结构,就无法记录哪些线程被阻塞、以及应当唤醒哪些线程

4. 线程之间的执行关系

1. 串行
串行是指任务按顺序一个接一个地执行,只有当前一个任务执行完成,后一个任务才会开始执行,严格遵循任务提交的先后顺序。即便系统拥有多个 CPU 核心,在串行模式下,任意时刻也只会有一个线程在运行。

2. 并发
并发是指多个线程看起来像是在同时运行,其是通过时间片轮转机制实现,通过快速切换线程,让每个线程都获得运行机会,因此并发特性在单核环境中体现得尤为明显。

而在多核 CPU 上,线程有可能被分配到不同的核心上并行执行,但当线程数量多于核心数量时,CPU 仍需通过时间片轮转进行调度,以确保所有线程都能获得执行机会。

3. 并行
并行是指多个线程在真正意义上同时运行,分别占用不同的 CPU 核心,在同一时刻执行各自的任务,体现出真正的同时处理能力。


5. 线程的活跃性

5.1. 死锁

5.1.1. 死锁概述

死锁是指:两个或两个以上的线程在执行过程中,因互相持有对方所需的资源而造成的一种互相等待的现象。如果没有外力干涉,这些线程都将无法推进下去,永远处于阻塞状态

例如线程 $T_1$ 已经持有了锁 A,现在想去竞争锁 B。线程 $T_2$ 已经持有了锁 B,现在想去竞争锁 A,这便形成了死锁

需要注意的是:在 JUC 中,所谓的 “资源” 是指:锁


5.1.2. 死锁的必要条件

  1. 互斥条件
    1. 资源是独占的,同一时刻只能被一个线程持有
  2. 不剥夺条件
    1. 线程已持有的资源,在末使用完之前,不能被其他线程强行剥夺,只能由线程自己主动释放
  3. 循环等待条件
    1. 若干线程之间形成一种头尾相接的循环等待资源的关系(例如 ABBA
  4. 请求与保持条件
    1. 线程在等待新资源时,对已经持有的资源保持不放(吃着碗里的,看着锅里的)

[!NOTE] 注意事项

  1. 解决死锁的核心思路,就是破坏上述四个条件的任意一个。由于 “互斥条件” 是锁的基本特性,通常我们无法破坏,因此主要针对后三个条件进行破坏

5.1.3. 死锁的解决方案

5.1.3.1. 破坏不剥夺条件

破坏不剥夺条件是指:线程等待资源超过一段时间,就放弃等待,并释放已持有的资源


5.1.3.2. 破坏循环等待条件(最常用)

破坏循环等待条件是指:规定所有线程必须按照相同的顺序获取资源。例如规定所有线程必须先获取锁 A,再获取锁 B,再获取锁 C


5.1.3.3. 破坏请求与保持条件

破坏请求与保持条件是指:采用 “预先申请” 的方式,让所有线程必须一次性申请它所需要的所有资源。如果能申请到所有资源,线程继续向下执行。如果不能申请到所有资源,则释放已持有的资源

[!NOTE] 注意事项

  1. 破坏请求与保持条件,通常和破坏不剥夺条件结合使用

5.2. 活锁

5.2.1. 活锁概述

活锁是指:两个或两个以上的线程在执行过程种,因为不断互相改变对方的终止条件,导致所有线程都无法继续向下执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class TestLiveLock {

static volatile int count = 10;

private static void sleep(double seconds) {
try {
Thread.sleep((long) (seconds * 1000));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}

public static void main(String[] args) {
new Thread(() -> {
// 期望减到 0 退出循环
while (count > 0) {
sleep(0.2);
count--;
log.debug("count: {}", count);
}
}, "t1").start();

new Thread(() -> {
// 期望加到 20 退出循环
while (count < 20) {
sleep(0.2);
count++;
log.debug("count: {}", count);
}
}, "t2").start();
}

}

[!NOTE] 注意事项

  1. 活锁本质上是 ”重试“ 导致的冲突

5.2.2. 活锁的解决方案

5.2.2.1. 加锁

活锁本质上并不是 “锁” 本身,但解决活锁时,却是可以使用 ”加锁“ 来解决


5.2.2.2. 随机避让(最常用)

随即避让是指:当多个 ”请求“ 同时争抢资源失败时,不要立即重试,也不要使用固定的休眠时间。更合理的做法是采用 ”指数退避 + 随机抖动“ 的策略,降低竞争冲突

这里同样能使用随即避让能避免这个问题,不过场景不再是 “争抢资源”,而是 “重试” 导致的冲突。我们可以在重试时加入随机避让,减少重试之间的同步碰撞


5.2.2.3. 限制重试次数

5.3. 饥饿

5.3.1. 饥饿概述

饥饿是指:某个线程虽然长期处于可运行状态,但是却一直无法获取所需要的资源(例如 CPU 时间片、锁、资源池资源等),导致线程无法继续执行


5.3.2. 饥饿的解决方案

无论是线程饥饿、流量控制,还是 IO 阻塞,这些问题的本质都是相同的:因无法获取所需要的资源,导致无法继续执行。

针对这种 ”资源匮乏“ 的问题,通常有五种解决方案:

  1. 强制排队
  2. 资源预留
  3. 及时止损
    1. 异步处理
    2. 降级处理
    3. 启动告警
    4. 异步编程回调
  4. 随机避让
  5. 调整优先级

[!NOTE] 注意事项

  1. 流量控制主要包括并发控制和速率限制
5.3.2.1. 强制排队

强制排队是指:放弃 “自由竞争”,严格遵循 “先来后到” 原则,保证每个 ”请求“ 最终都能得到处理

以锁资源为例,我们可以使用公平锁,而不是非公平锁


5.3.2.2. 资源预留

资源预留是指:提前划分好资源配额,实行资源隔离,做到 ”专款专用“,防止某单一 ”请求“ 耗尽所有资源

以线程资源为例,我们可以为每个业务分配一个独立的线程池,而不是共用一个大的线程池


5.3.2.3. 及时止损

及时止损是指:当通过限时阻塞(超时未获取)或非阻塞(尝试一次未获取)确定拿不到资源的时候,不再继续等待,而是采取以下策略:

  1. 异步处理
    1. 利用 ”削峰填谷“ 的思想,将请求写入 MQ
  2. 降级处理
  3. 启动告警
  4. 异步编程回调

[!NOTE] 注意事项

  1. 可能是限时阻塞,也可能是非阻塞,还可能是直接使用异步编程回调(连非阻塞也不用,直接使用异步编程回调)

5.3.2.4. 随机避让

随即避让是指:当多个 ”请求“ 同时争抢资源失败时,不要立即重试,也不要使用固定的休眠时间。更合理的做法是采用 ”指数退避 + 随机抖动“ 的策略,降低竞争冲突


5.3.2.5. 调整优先级

调整优先级是指:调整竞争权重,让核心的 ”请求“ 拥有更高的优先级

CPU 时间片资源为例,我们动态调整线程的优先级

[!NOTE] 注意事项

  1. 虽然 Java 提供了 Thread#setPriority,但这通常不是一个靠谱的解决方案,更多时候我们要反其道而行之:尽量不要修改线程的优先级,让所有线程的优先级保持同等水平

8. 线程相关常用方法

线程相关的常用方法主要包括:

  1. Thread 中的一些的方法
  2. TimeUnit 中的一些方法
    1. TimeUnit#sleep
    2. TimeUnit#timedJoin
    3. TimeUnit#timedWait
  3. Object 中的一些方法
    1. Object#wait
    2. Object#notify
  4. LockSupport 中的一些方法

[!NOTE] 注意事项

  1. 详见源码:Thread
    1. obsidian 内部链接:
      1. 源码:java.lang.Thread源码解析
    2. Hexo 链接:
      1. http://blog.wangjia.ink/2025/11/12/源码:java.lang.Thread源码解析/
  2. 详见源码:Object
    1. obsidian 内部链接:
      1. 源码:java.lang.Object源码解析
    2. Hexo 链接:
      1. http://blog.wangjia.ink/2025/09/24/源码:java.lang.Object源码解析/
  3. 详见源码:LockSupport
    1. obsidian 内部链接:
      1. 源码:java.util.concurrent.locks.LockSupport源码解析
    2. Hexo 链接:
      1. http://blog.wangjia.ink/2025/11/13/源码:java.util.concurrent.locks.LockSupport源码解析/

四、ThreadLocal

ThreadLocal 是指:Thread->threadLocals,用于记录该 Thread 实例独属的一些信息。而 Thread->threadLocals 又是 ThreadLocal.ThreadLocalMap 类型,详见源码:ThreadLocal<T>.ThreadLocalMapobsidian 内部连接:源码:java.lang.ThreadLocal<T>.ThreadLocalMap源码解析Hexo 链接: http://blog.wangjia.ink/2025/11/16/源码:java.lang.ThreadLocal<T>.ThreadLocalMap源码解析/


五、锁

1. 锁基础体系

  1. 详见源码:Serializable
    1. obsidian 内部链接:
      1. 源码:java.io.Serializable源码解析
    2. Hexo 链接:
      1. http://blog.wangjia.ink/2025/10/28/源码:java.io.Serializable源码解析/
  2. 详见源码:Lock
    1. obsidian 内部链接:
      1. 源码:java.util.concurrent.locks.Lock源码解析
    2. Hexo 链接:
      1. http://blog.wangjia.ink/2025/11/07/源码:java.util.concurrent.locks.Lock源码解析/
  3. 详见源码:ReadWriteLock
    1. obsidian 内部链接:
      1. 源码:java.util.concurrent.locks.ReadWriteLock源码解析
    2. Hexo 链接:
      1. http://blog.wangjia.ink/2025/11/07/源码:java.util.concurrent.locks.ReadWriteLock源码解析/
  4. 详见源码:Condition
    1. obsidian 内部链接:
      1. 源码:java.util.concurrent.locks.Condition源码解析
    2. Hexo 链接:
      1. http://blog.wangjia.ink/2025/08/31/源码:java.util.concurrent.locks.Condition源码解析/
  5. 详见源码:AbstractOwnableSynchronizer
    1. obsidian 内部链接:
      1. 源码:java.util.concurrent.locks.AbstractOwnableSynchronizer源码解析
    2. Hexo 链接:
      1. http://blog.wangjia.ink/2025/09/02/源码:java.util.concurrent.locks.AbstractOwnableSynchronizer源码解析/
  6. 详见源码:AbstractQueuedSynchronizer
    1. obsidian 内部链接:
      1. 源码:java.util.concurrent.locks.AbstractQueuedSynchronizer源码解析
    2. Hexo 链接:
      1. http://blog.wangjia.ink/2025/09/01/源码:java.util.concurrent.locks.AbstractQueuedSynchronizer源码解析/
  7. 详见源码:AbstractQueuedLongSynchronizer
    1. obsidian 内部链接:
      1. 源码:java.util.concurrent.locks.AbstractQueuedLongSynchronizer源码解析
    2. Hexo 链接:
      1. http://blog.wangjia.ink/2025/11/19/源码:java.util.concurrent.locks.AbstractQueuedLongSynchronizer源码解析/

2. synchronized

2.1. synchronized 前置基础

2.1.1. Java 对象头

通常我们的一个 Java 实例,他在堆内存中由三部分组成:

  1. Java 对象头(Object Header
  2. 实例数据(Instance Data
  3. 对齐填充(Padding

32 位虚拟机为例:

[!NOTE] 注意事项

  1. 这里的 Klass Word 应该叫做 Klass Pointer

2.2. synchronized 概述

synchronized 是一种可重入、悲观、非公平、互斥的锁


2.3. synchronized 使用方式

以如下代码为例,如果 Room#increment 竞争到了锁,那么 Room#decrementRoom#getCounter 都无法再竞争到锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Room {

private int counter = 0;

public synchronized void increment() {
counter++;
}

public synchronized void decrement() {
counter--;
}

public synchronized int getCounter() {
return counter;
}

}

[!NOTE] 注意事项

  1. 当我们为静态具体方法加上 synchronized 修饰符,锁的是类对象(即 Class 对象)
1
2
3
4
5
6
7
8
9
10
11
12
// 原写法
public static synchronized void increment() {
counter++;
}


// 等价写法
public void increment() {
synchronized (Room.Class) {
counter++;
}
}

[!NOTE] 注意事项
2. 当我们为实例普通方法加上 synchronized 修饰符,锁的是仅是当前对象(即 this

1
2
3
4
5
6
7
8
9
10
11
12
// 原写法
public synchronized void increment() {
counter++;
}


// 等价写法
public void increment() {
synchronized (this) {
counter++;
}
}

[!NOTE] 注意事项
3. 普通实例方法并不必然只能锁当前对象 (this),静态方法也并非只能锁类对象(Class 对象)

1
2
3
4
5
6
7
8
9
10
11
12
public static void increment() {
synchronized (this) {
counter++;
}
}


public void increment() {
synchronized (Room.class) {
counter++;
}
}

[!NOTE] 注意事项
4. 以如下代码为例,虽然 Room#sleepRoom#study 本身没有逻辑上的交集,我们可以通过引入多把锁来进行优化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 原代码
public class Room {

public synchronized void sleep() {
System.out.println("sleeping 2 小时");
}

public synchronized void study() {
System.out.println("study 1 小时");
}

}


// 引入多把锁进行优化
public class Room {

private final Object studyRoom = new Object();

private final Object bedRoom = new Object();

public void sleep(){
synchronized (bedRoom) {
System.out.println("sleeping 2 小时");
}
}

public void study() {
synchronized (studyRoom) {
System.out.println("study 1 小时");
}
}
}

[!NOTE] 注意事项
5. 即便一个方法中,只涉及到了共享变量的读操作,我们仍然要为其加锁。这主要是为了其可见性、原子性。可见性可以通过多种方式来保证,因此并不一定需要加锁。但原子性必须依赖锁来确保,避免 B 线程在进行读操作时,A 线程进行写操作,从根本上杜绝脏读、不可重复读、幻读等问题
6. 对于 synchronized 的本质,我们可以简单的理解为:通过为某对象或某类对象的加锁,从而控制多线程对共享资源的读写操作


2.4. synchrnoized 锁升级

锁升级的流程为:偏向锁 ➔ 轻量级锁 ➔ 重量级锁

2.4.1. 偏向锁

偏向锁的设计初衷,是在无竞争场景下进一步降低竞争锁的开销,从而提升锁操作的性能

当某线程(例如 $T_1$) 要去执行临界区代码,JVM 会通过 CAS 尝试将 $T_1$ 的 Thread ID 写入被加锁对象或被加锁类对象的对象头中的 Mark Word 中。

如果写入成功, $T_1$ 顺利执行临界区代码。如果 $T_1$ 后续再次执行临界区代码,只需检查 Mark Word 中的 Thread ID 是否与自身一致(由于偏向锁在释放时并不会主动重置对象头,因此在无竞争场景下,Mark Word 中依旧保留着 $T_1$ 的 Thread ID,所以能直接执行)

如果写入失败,则是其他线程(例如 $T_2$)正在持有偏向锁(无论 $T_2$ 是否在执行临界区代码),而这就发生了竞争,偏向锁模式就会宣告失败。此时 JVM 会将偏向锁升级为轻量级锁

我们可以简单的理解为:一旦某个线程持有了偏向锁,它就 “偏向” 这个线程,从头到尾都只认它,无论该线程是否在执行临界区代码。但是一旦出现第二个线程尝试竞争锁,偏向锁就会被撤销并升级为轻量级锁

然而,随着硬件性能提升和虚拟机其他优化手段的发展,偏向锁带来的性能收益已逐渐减弱,同时其实现的复杂性也成为阻碍 JVM 进一步优化的负担。除此之外,在高并发场景中,偏向锁撤销的开销往往大于其带来的收益。因此,OracleJava15 之后将偏向锁标记为废弃,并在 Java17 中将其彻底移除。

[!NOTE] 注意事项

  1. 如果使用的是 Java8,偏向锁默认是开启的,我们可以通过 JVM 启动参数来关闭偏向锁
1
-XX:-UseBiasedLocking

[!NOTE] 注意事项
2. 由于 Mark Word 的空间是有限的,存储了 HashCode 就没地方存 Thread ID 了。因此,如果在无锁状态计算了 HashCode,就无法再使用偏向锁,而是直接使用轻量级锁。如果在持有偏向锁时计算了 HashCode,偏向锁会被立即撤销,并直接升级为重量级锁(因为重量级锁的 Monitor 中可以存储 HashCode


2.4.2. 轻量级锁

轻量级锁的设计初衷,是在轻量竞争场景下进一步降低竞争锁的开销,从而提升锁操作的性能

当某线程(例如 $T_1$)要去执行临界区代码,JVM 会在 $T_1$ 的虚拟机栈的栈帧中创建一个锁记录(Lock Record

然后 JVMLock RecordObject reference 指向被加锁对象或被加锁类对象,并且通过 CAS 尝试将 Lock Recordlock record 地址 00 与被加锁对象或被加锁类对象的 MarkWord(即 Hashcode Age Bias 01)进行交换

如果交换成功, $T_1$ 顺利执行临界区代码。如果在 $T_1$ 执行临界区代码时发生了重入,同样会添加新的 Lock Record。$T_1$ 的虚拟机栈的栈帧中有多少个 Lock Record,说明发生了多少次重入

当 $T_1$ 执行完临界区的代码,JVM 会将两者再交换回来

如果交换失败,则是其他线程(例如 $T_2$)持有轻量级锁,那么 $T_1$ 会进行多次自旋。如果多次自旋仍热未成功,JVM 会将轻量级锁升级为重量级锁


2.4.3. 重量级锁

监视器(Monitor、管程)是 JVM 内部专门用于实现重量级锁的结构。当升级为重量级锁时,JVM 会为被加锁对象或被加锁类对象创建一个 Monitor 对象

当 $T_1$ 执行完临界区代码后,会根据线程中保存的指向被加锁对象或被加锁类对象的地址,找到该对象或类对象,然后再根据该对象的对象头的 Mark Word 找到 Monitor,将其 Owner 设置为 null,并唤醒 MonitorEntryList 中所有等待的线程,这些线程随后开始竞争重量级锁( MonitorOwner


3. ReentrantLock

详见源码:ReentrantLock

  1. obsidian 内部链接:
    1. 源码:java.util.concurrent.locks.ReentrantLock源码解析
  2. Hexo 链接:
    1. http://blog.wangjia.ink/2025/08/31/源码:java.util.concurrent.locks.ReentrantLock源码解析/

4. ReentrantReadWriteLock

详见源码:ReentrantReadWriteLock

  1. obsidian 内部链接:
    1. 源码:java.util.concurrent.locks.ReentrantReadWriteLock源码解析
  2. Hexo 链接:

5. CAS

5.1. CAS 概述

CAS 是一种思想,其核心思想是:当前线程在进行写操作时,当 V = E 时,修改为 N。当 V ≠ E 时,不进行任何操作。其中 VEN 是指:

  1. VCurrent Value
    1. 当前线程在进行写操作时,共享变量在主内存中的实际值
  2. EExpected Value
    1. 当前线程在进行写操作时,期望共享变量在主内存中的实际值
    2. 简单来说就是:$t_1$ 时从主内存拿到的共享变量,我们期望 $t_2$ 时主内存中的共享变量仍然是这个值
  3. NNew Value
    1. 当前线程在进行写操作时,要把共享变量修改为该值

为了实现 CAS 思想,不同的 CPU 厂商提供了不同的原子性的机器码指令(例如 x86CMPXCHGARMLDXR/STXR 组合),我们一般把这些机器码指令称之为 “CAS 指令”

在日常交流中,我们常把把 CASCAS 指令混为一谈。在 Java 中,说到 “使用 CAS”,其实就是通过 Unsafe 调用了 CAS 指令

[!NOTE] 注意事项

  1. CAS 本质上并不是一种锁,而是一种更新机制
  2. 虽然 V 说是 ”当前线程在进行写操作时,共享变量在主内存中的实际值“,但是由于 JMM 的存在,我们可能仍从工作内存读 V,所以有可见性问题。除此之外,还有有序性问题。因此即便使用 CAS,通常结合 volatile 修饰符使用,CAS 保证原子性,volatile 保证可见性、顺序性
  3. 当我们使用 CAS 时,目标肯定是希望写操作成功。所以我们通常会循环执行 CAS,直到写操作成功

5.2. CAS 问题爆破

5.2.1. ABA 问题

5.2.1.1. ABA 问题概述

ABA 问题是指:当 V = E 时,尽管 CAS 操作成功了,但实际上该位置的数据可能曾经发生过变化。例如我们桌子上有一杯水(A),你离开了一会儿。期间别人把你杯子里的水喝了(变成 B),然后又重新倒满水(变回 A)。等你回来时,看到水还是满的,你以为没人动过,但实际上里面的水已经变了

很多时候 ABA 问题可能无伤大雅。但是在一些业务逻辑时,这个问题可能是致命的


5.2.1.2. ABA 问题解决方案
5.2.1.2.1. 版本号

版本号是指:不仅比较值(即 V = E),还要比较版本号。即 $CAS(E, N)➔CAS(E + Version_E, B + Version_B)$,当线程在进行写操作时,不仅要 V = E,还要 $Version_V=Version_E$

AtomicStampedReference 就采用了这种方式解决了 ABA 问题。

除此之外,AtomicMarkableReference 可以看作是 AtomicStampedReference 的简化版,但是它并没有严格使用版本号。AtomicMarkableReference 是为了解决 “很多时候我们并不需要知道它被修改了多少次,而是只需要知道它是否被修改过” 的问题


5.3. Atomic* 原子类

5.3.1. Atomic* 原子类概述

Atomic* 原子类是使用 CAS 实现多线程并发安全的一些类


5.3.2. Atomic* 原子类相关分类

  1. AtomicBoolean
  2. AtomicInteger
  3. AtomicIntegerArray
  4. AtomicIntegerFieldUpdater
  5. AtomicIntegerFieldUpdaterImpl
  6. AtomicLong
  7. AtomicLongArray
  8. AtomicLongFieldUpdater
  9. AtomicReference
  10. AtomicReferenceArray
  11. AtomicReferenceFieldUpdater
  12. AtomicReferenceFieldUpdaterImpl
  13. AtomicMarkableReference
  14. AtomicStampedReference
  15. LongAdder
  16. DoubleAdder

六、并发工具

1. CountDownLatch

详见源码:CountDownLatch

  1. obsidian 内部链接:
    1. 源码:java.util.concurrent.CountDownLatch源码解析
  2. Hexo 链接:
    1. http://blog.wangjia.ink/2025/11/16/源码:java.util.concurrent.CountDownLatch源码解析/

2. CyclicBarrier

详见源码:CyclicBarrier

  1. obsidian 内部链接:
    1. 源码:java.util.concurrent.CyclicBarrier源码解析
  2. Hexo 链接:
    1. http://blog.wangjia.ink/2025/11/19/源码:java.util.concurrent.CyclicBarrier源码解析/

3. Semaphore

详见源码:Semaphore

  1. obsidian 内部链接:
    1. 源码:java.util.concurrent.Semaphore源码解析
  2. Hexo 链接:
    1. http://blog.wangjia.ink/2025/11/19/源码:java.util.concurrent.Semaphore源码解析/

七、线程池

1. 线程池基础体系

  1. 详见源码:Executor
    1. obsidian 内部链接:
      1. 源码:java.util.concurrent.Executor源码解析
    2. Hexo 链接:
      1. http://blog.wangjia.ink/2025/08/31/源码:java.util.concurrent.Executor源码解析/
  2. 详见源码:ExecutorService
    1. obsidian 内部链接:
      1. 源码:java.util.concurrent.ExecutorService源码解析
    2. Hexo 链接:
      1. http://blog.wangjia.ink/2025/09/03/源码:java.util.concurrent.ExecutorService源码解析/
  3. 详见源码:AbstractExecutorService
    1. obsidian 内部链接:
      1. 源码:java.util.concurrent.AbstractExecutorService源码解析
    2. Hexo 链接:
      1. http://blog.wangjia.ink/2025/11/03/源码:java.util.concurrent.AbstractExecutorService源码解析/
  4. 详见源码:ScheduledExecutorService
    1. obsidian 内部链接:
      1. 源码:java.util.concurrent.ScheduledExecutorService源码解析
    2. Hexo 链接:
      1. http://blog.wangjia.ink/2025/11/03/源码:java.util.concurrent.ScheduledExecutorService源码解析/
  5. 详见源码:ThreadPoolExecutor
    1. obsidian 内部链接:
      1. 源码:java.util.concurrent.ThreadPoolExecutor源码解析
    2. Hexo 链接:
      1. http://blog.wangjia.ink/2025/09/03/源码:java.util.concurrent.ThreadPoolExecutor源码解析/
  6. 详见源码:ScheduledThreadPoolExecutor
    1. obsidian 内部链接:
      1. 源码:java.util.concurrent.ScheduledThreadPoolExecutor源码解析
    2. Hexo 链接:
      1. http://blog.wangjia.ink/2025/11/04/源码:java.util.concurrent.ScheduledThreadPoolExecutor源码解析/
  7. 详见源码:ForkJoinPool
    1. obsidian 内部链接:
      1. 源码:java.util.concurrent.ForkJoinPool源码解析
    2. Hexo 链接:
      1. http://blog.wangjia.ink/2025/11/14/源码:java.util.concurrent.ForkJoinPool源码解析/

[!NOTE] 注意事项

  1. 线程池的三架马车:ThreadPoolExecutorScheduledThreadPoolExecutorForkJoinPool

八、异步编程

1. 异步编程基础体系

  1. 详见源码:Serializable
    1. obsidian 内部链接:
      1. 源码:java.io.Serializable源码解析
    2. Hexo 链接:
      1. http://blog.wangjia.ink/2025/10/28/源码:java.io.Serializable源码解析/
  2. 详见源码:Comparable<T>
    1. obsidian 内部链接:
      1. 源码:java.lang.Comparable<T>源码解析
    2. Hexo 链接:
      1. http://blog.wangjia.ink/2025/11/06/源码:java.lang.Comparable<T>源码解析/
  3. 详见源码:Callable<V>
    1. obsidian 内部链接:
      1. 源码:java.util.concurrent.Callable<V>源码解析
    2. Hexo 链接:
      1. http://blog.wangjia.ink/2025/09/02/源码:java.util.concurrent.Callable<V>源码解析/
  4. 详见源码:Runnable
    1. obsidian 内部链接:
      1. 源码:java.util.concurrent.Runnable源码解析
    2. Hexo 链接:
      1. http://blog.wangjia.ink/2025/09/02/源码:java.util.concurrent.Runnable源码解析/
  5. 详见源码:Delayed
    1. obsidian 内部链接:
      1. 源码:java.util.concurrent.Delayed源码解析
    2. Hexo 链接:
      1. http://blog.wangjia.ink/2025/11/06/源码:java.util.concurrent.Delayed源码解析/
  6. 详见源码:Future<V>
    1. obsidian 内部链接:
      1. 源码:java.util.concurren.Future<V>源码解析
    2. Hexo 链接:
      1. http://blog.wangjia.ink/2025/09/02/源码:java.util.concurren.Future<V>源码解析/
  7. 详见源码:RunnableFuture<V>
    1. obsidian 内部链接:
      1. 源码:java.util.concurrent.RunnableFuture<V>源码解析
    2. Hexo 链接:
      1. http://blog.wangjia.ink/2025/11/03/源码:java.util.concurrent.RunnableFuture<V>源码解析/
  8. 详见源码:ScheduledFuture<V>
    1. obsidian 内部链接:
      1. 源码:java.util.concurrent.ScheduledFuture<V>源码解析
    2. Hexo 链接:
      1. http://blog.wangjia.ink/2025/11/04/源码:java.util.concurrent.ScheduledFuture<V>源码解析/
  9. 详见源码:ForkJoinTask<V>
    1. obsidian 内部链接:
      1. 源码:java.util.concurrent.ForkJoinTask<V>源码解析
    2. Hexo 链接:
      1. http://blog.wangjia.ink/2025/11/15/源码:java.util.concurrent.ForkJoinTask<V>源码解析/
  10. 详见源码:FutureTask<V>
    1. obsidian 内部链接:
      1. 源码:java.util.concurrent.FutureTask<V>源码解析
    2. Hexo 链接:
      1. http://blog.wangjia.ink/2025/11/03/源码:java.util.concurrent.FutureTask<V>源码解析/
  11. 详见源码:RunnableScheduledFuture<V>
    1. obsidian 内部链接:
      1. 源码:java.util.concurrent.RunnableScheduledFuture<V>源码解析
    2. Hexo 链接:
      1. http://blog.wangjia.ink/2025/05/05/源码:java.util.concurrent.RunnableScheduledFuture<V>源码解析/
  12. 详见源码:ScheduledFutureTask<V>
    1. obsidian 内部链接:
      1. 源码:java.util.concurrent.ScheduledThreadPoolExecutor.ScheduledFutureTask<V>源码解析
    2. Hexo 链接:
      1. http://blog.wangjia.ink/2025/11/15/源码:java.util.concurrent.ScheduledThreadPoolExecutor.ScheduledFutureTask<V>源码解析/
  13. 详见源码:RecursiveAction
    1. obsidian 内部链接:
      1. 源码:java.util.concurrent.RecursiveAction源码解析
    2. Hexo 链接:
      1. http://blog.wangjia.ink/2025/11/15/源码:java.util.concurrent.RecursiveAction源码解析/
  14. 详见源码:RecursiveTask<V>
    1. obsidian 内部链接:
      1. 源码:java.util.concurrent.RecursiveTask<V>源码解析
    2. Hexo 链接:
      1. http://blog.wangjia.ink/2025/11/15/源码:java.util.concurrent.RecursiveTask<V>源码解析/
  15. 详见源码:CountedCompleter<T>
    1. obsidian 内部链接:
      1. 源码:java.util.concurrent.CountedCompleter<T>源码解析
    2. Hexo 链接:
      1. http://blog.wangjia.ink/2025/11/15/源码:java.util.concurrent.CountedCompleter<T>源码解析/

2. 异步编程实现方式

2.1. 伪异步

伪异步是指:当前线程将我们执行的 Java 代码中,一些耗时的操作交给其他线程(通常使用专门的线程池)去执行,当前线程可以直接继续向下执行


2.2. 伪异步 + 异步编程等待

伪异步 + 异步编程等待是指:虽然当前线程将我们执行的 Java 代码中,一些耗时的操作交给其他线程(通常使用专门的线程池)去执行,但当前线程仍然需要该任务的返回结果,所以当前线程必须在后续某个时刻获取该任务的返回结果

通常的做法是:提交任务后返回一个凭证(即 Future,常见具体实现类主要包括 FutureTaskScheduledFutureTaskForkJoinTask),然后根据这个凭证在需要结果时获取结果,然后再继续向下执行

[!NOTE] 注意事项

  1. 异步编程等待既可能是阻塞的也可能是限时阻塞的

2.3. 伪异步 + 异步编程回调

这部分内容涉及到了 CompletableFuture,详见笔记:Java 函数式编程(obsidian 内部链接:笔记:Java函数式编程Hexo 链接:)


伪异步 + 异步流式回调(响应式)



笔记:JUC
https://wangjia5289.github.io/2025/11/23/笔记:JUC/
Author
霸天
Posted on
November 23, 2025
Licensed under