基本概念

Java 是支持多线程的开发语言,意思就是可以在多 CPU 核心的机子上同时处理不同的任务,优化资源的使用率,提升程序的效率。

并发编程三要素

  • 原子性:指一个操作或一系列操作要么全部执行完成,要么全部不执行(synchronized,CAS)
  • 有序性:指程序执行的顺序按照代码的逻辑顺序进行(volatile,锁机制)
  • 可见性:指当一个线程修改了共享变量的值后,其他线程能立即看到这个修改。

volatile

核心作用是通过 JVM 的底层机制解决多线程通信中的可见性和有序性问题。
volatile 是 Java 提供的一种轻量级同步机制,被 volatile 修饰的变量具有两个核心特性:

  • 可见性:一个线程对变量的修改,其他线程能立即看到
  • 有序性:禁止特定类型的指令重排序

Java 内存模型(JMM)是 JVM 定义的一套抽象规范,它规定了线程如何通过内存进行交互,目的是解决多线程通过共享内存通信时的三大问题:可见性、原子性、有序性。

JMM

主内存:所有线程共享的内存区域,存储共享变量(实例变量、静态变量等)。
工作内存:每个线程独有的内存区域,存储主内存中共享变量的副本()。
在线程操作变量的流程是:线程操作变量时,先从主内存加载到工作内存,修改后再刷新回主内存。在这个过程中,就会出现可见性问题和有序性问题。
可见性问题:如果线程 A 修改了变量但未及时刷新到主内存,线程 B 从主内存读取的还是旧值,就会出现 “线程 A 的修改对线程 B 不可见” 的问题。
有序性问题:为了优化性能,CPU 和编译器会对无依赖关系的指令进行重排序

解决方案:
可见性:

  1. 被 volatile 修饰的变量,JVM 会通过强制刷新内存的机制保证可见性,在进行写操作时,线程修改 volatile 变量后,会立即将工作内存中的新值强制刷新到主内存
  2. 线程读取 volatile 变量时,会强制从主内存重新加载,放弃从工作内存读取就副本,而是直接读取最新值
    有序性:
  3. volatile 通过 JVM 插入内存屏障,禁止特定指令重排序,内存屏障时一种 CPU 指令,会阻止屏障两侧的指令重排序,并保证内存操作的顺序性。内存屏障的类型 + LoadLoad:禁止读操作间重排序 + StoreStore:禁止写操作间重排序 + LoadStore:禁止读后写 + StoreLoad:禁止写后读
    在写操作前插入 StoreStore 屏障,确保写操作完成,在写操作后插入 StoreLoad 屏障,确保写操作刷新到主内存后,在执行后续操作;为了优化性能,CPU 和编译器会对无依赖关系的指令进行重排序
    *********volatile 只能保证单次读 / 写操作的原子性(如 flag = true),但无法保证复合操作的原子性

CAS

CAS 是一种无锁机制,它是依赖 CP 来保证原子性的。有 3 个核心变量,一个是内存地址存储共享变量的实际内存位置,一个是预期值及线程认为当前内存中的值,最后一个是新值及线程希望将变量更新为的值。当且仅当预期值内存地址的实际值等于预期值才会被更新为新值,它是实现乐观锁的核心。相比 synchronized 等悲观锁,避免了线程阻塞和上下文切换的开销,性能更优
但它存在两个问题:

  • ABA 问题:线程 1 读取到值 A,线程 2 将 A 改为 B 再改回 A,线程 1 的 CAS 会误认为 “值未变” 而成功更新,可能导致数据不一致,加入版本号,只有当版本号、预期值与内存中实际值相等的时候才会更新为新值
  • 不断重试:类似于自旋,CAS 失败的线程会不断循环重试,占用 CPU 资源。解决方案,使用 AQS 来限制重试次数或者在高冲突环境下使用悲观锁

AQS

AQS 是 Java 并发编程的核心基础框架,位于 java.util.concurrent.locks 包中。它为几乎所有 JUC 同步工具(如 ReentrantLock、CountDownLatch、Semaphore、CyclicBarrier 等)提供了统一的底层支持,解决了同步状态管理、线程排队、等待唤醒等核心问题。

AQS 的设计

AQS 是一个抽象类,它通过模版方法模式定义了获取锁、释放锁的流程,而将具体的如判断是否能获取锁的逻辑延迟到它的子类来实现。
同步状态 state:AQS 内部维护了一个 volatile int state 变量,用于表示同步状态,例如锁的持有次数、计数器值等,子类通过 getState()、setState()、compareAndSetState()来修改转态。
同步队列 CLH:当线程获取同步状态失败时,AQS 会将线程包装成一个 Node 节点,加入一个双向阻塞队列当中,并让线程阻塞等待;当同步状态释放时,在唤醒队列中的线程重新竞争。


同步状态和同步队列共同支撑了线程的同步与排队机制。
State:

  • ReentrantLock 中:state 表示锁的重入次数,如果 state 为 0,表示没有锁,不为 0 表示被线程持有,值为重入次数
    双向阻塞队列
    CLH 队列是 AQS 实现线程排队的核心,是一种双向链表,每个节点代表一个等待同步状态的线程。
    每个节点的关键变量:
1
2
3
4
5
6
7
8
9
10
11
12
static final class Node {
// 节点状态(如 SIGNAL:当前节点释放后需唤醒后继节点)
volatile int waitStatus;
// 前驱节点
volatile Node prev;
// 后继节点
volatile Node next;
// 当前节点对应的线程
volatile Thread thread;
// 条件队列的后继节点(用于Condition)
Node nextWaiter;
}

AQS 通过 head(头节点)和 tail(尾节点)指针管理队列,初始时 head = tail = null。
头节点是 “已获取同步状态的线程” 对应的节点(或哨兵节点),新节点总是加入队列尾部(通过 CAS 设置 tail)。

工作流程

  • 当线程调用 lock()时,会触发 AQS 的 acquire(1),尝试获取锁资源,如果此时 state 为 0,通过 CAS 将 state 从 0 改为 1,成功获取锁资源,记录当前线程未持有者。如果 state>0 且当前线程是锁的持有者,则 state+=1,该线程仍然持有锁。如果失败就将当前线程包装成 voladate 的 Node 节点,加入双向队列的尾部。
  • 新节点进入队列后,会进入自旋+阻塞的循环,检查当前节点的前驱是否为头结点,如果是头结点表示可能轮到自己竞争锁,再次尝试获取锁,若成功将当前节点设为新头结点,原来的头结点出队。若失败,根据前驱节点的状态决定是否阻塞当前线程。线程被唤醒后,重复上述步骤,直到获取到锁或被中断。
  • 当前程调用 unlock()时,触发 release(1),如果当前线程不是资源的持有者,抛出异常。如果是当前持有该资源的线程,那么 state-=1,若 state==0,则清空持有者线程,释放锁资源。释放锁资源后唤醒头结点的后继节点,被唤醒的线程会重新尝试获取锁资源,尝试获取锁。
1
2
3
4
5
6
7
8
9
public final void acquire(int arg) {
// 1. 尝试获取同步状态(tryAcquire由子类实现)
if (!tryAcquire(arg) &&
// 2. 失败则将当前线程包装成Node,加入同步队列
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {
// 3. 若需要,中断当前线程
selfInterrupt();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
public final boolean release(int arg) {
// 1. 尝试释放同步状态(tryRelease由子类实现)
if (tryRelease(arg)) {
// 2. 释放成功,唤醒队列中的后继节点
Node h = head;
if (h != null && h.waitStatus != 0) {
unparkSuccessor(h); // 唤醒后继节点
}
return true;
}
return false;
}

AQS 与条件等待队列

AQS 还通过 ConditionObject(实现 Condition 接口)支持条件等待,类似 Object 的 wait()/notify(),一个锁可关联多个条件队列。

  • 每个 conditon 对应一个条件队列(单向列表),线程调用 await(),会释放锁并加入条件队列,进入等待状态。调用 signal()时,会将条件队列的节点转移到 AQS 的同步队列中,竞争锁资源

Java 中的锁是并发编程的核心机制,用于解决多线程共享资源竞争问题。

锁的分类

乐观锁和悲观锁

  • 悲观锁:认为多线程一定会竞争资源,每次操作前必须先获取锁,阻止其他线程访问。(synchronized、ReentrantLoack)
  • 乐观锁:认为竞争很少发生,操作时先不加锁,只在提交修改时检查是否有冲突,通过 CAS+版本号来实现(AtomicInteger、StampedLock)

公平锁和非公平锁

  • 公平锁:线程获取锁的顺序严格按照请求顺序(FIFO),先请求的线程先获得锁,避免 “饥饿”;ReentrantLock(true)(构造函数传 true)
  • 非公平锁:线程获取锁时不按顺序,允许 “插队”(刚释放锁的线程可能立即再次获取锁),性能更高(减少线程切换开销);synchronized(默认非公平)、ReentrantLock(false)(默认)

可重入锁和不可重入锁

  • 可重入:同一线程可以多次获取同一把锁(避免死锁)synchronized(隐式重入)、ReentrantLock(显式重入)。
  • 不可重入:同一线程多次获取同一把锁时会被阻塞(可能导致死锁)。java 中几乎不使用

独占锁和共享锁

  • 排他锁:同一时间只有一个线程能持有锁,其他线程必须等待。典型:synchronized、ReentrantLock(默认模式)。
  • 共享锁: 同一时间允许多个线程持有锁;典型:ReadWriteLock 的读锁(多个线程可同时读)、CountDownLatch(多个线程等待同一事件)。

常用锁

synchronized JVM 内置锁(C++实现)

synchronized 是 Java 语言内置的隐式同步锁,无需手动释放,由 JVM 自动管理。它是解决线程安全问题最基础也最常用的机制

基本使用

synchronized 的核心作用是保证临界区代码的原子性

  1. 同步实例方法:锁是当前对象 this,多个线程调用同一对象的同步方法会互斥,但调用不同对象的同步方法不会互斥。
  2. 同步静态方法:锁是类对象 class,所有线程调用该类的任意同步静态方法都会互斥。
  3. 同步代码块:通过 synchronized 指定锁对象,灵活性高,锁可以是任意对象
核心实现

synchronized 的底层依赖 JVM 的监视器锁(monitor) 机制,而锁的状态信息存储在对象头的 Mark Word 中

  • 锁的载体:Java 对象在堆内存中的布局分为三部分:对象头、实例数据、对齐填充。其中,对象头是 synchronized 实现的核心,包含类型指针(指向对象所属类的元数据)和 Mark Word 来存储对象的运行时状态(如哈希码、GC 年龄、锁状态等),是实现锁的关键。
    Mark Word的结构随着锁结构状态而变化 - 无锁状态:初始状态,存储对象哈希码,GC,是否偏向锁 - 偏向锁:当只有一个线程访问时,存储偏向的线程 id - 轻量级锁:存储指向线程栈中锁记录的指针(轻微竞争时) - 重量级锁:存储指向 monitor 对象的指针(激烈竞争时)
  • monitor 机制;重量级锁的核心时 monitor,它是由 C++来来实现的,有 3 个核心变量 - _owner:当前持有锁的线程 - _WaitSet:等待唤醒的线程队列 - _EntryList:等待获取锁的线程队列 - _recursions: 重入次数
    重量锁的工作流程
  1. 当线程进入同步代码块时,尝试获取 monitor 的_owner 所有权,若_owner 为空,那么当前线程持有锁,_recursions 重入次数记为 1.若_owner 是当前线程,那么_recursions++;若是其他线程,当前线程进入_EntryList 队列,阻塞等待(上下文切换开销大)
  2. 当线程退出同步代码块时,_recursions—;若减到 0,释放锁资源,_EntryList 或_WaitSet 中唤醒一个线程竞争锁资源。
锁优化

重量所上下文切换开销大,性能较差。JDK6 之后,JVM 引入锁升级机制(无锁、偏向锁、轻量级锁、重量级锁),根据竞争程度动态调整锁状态来提升性能。

  • 无锁状态:对象创建后默认处于无锁状态,Mark Word 存储对象哈希码等信息。此时无线程竞争,无需同步。
  • 偏向锁:线程第一次进入同步块,检查 Mark Word 是否为偏向锁状态(是否偏向锁=1 且线程 ID=0)。通过 CAS 操作将 Mark Word 的线程 ID 设为当前线程 ID,后续该线程再次进入同步块时,只需对比 Mark Word 的线程 ID 是否为自身,无需 CAS;当其他线程尝试获取偏向锁时,JVM 会撤销偏向锁(升级为轻量级锁)。
  • 轻量级锁:当多个线程交替访问同步块(无同时竞争)时,偏向锁撤销后升级为轻量级锁,通过 CAS 操作避免重量级锁的阻塞;线程进入同步块时,JVM 在当前线程的栈帧中创建一个锁记录(Lock Record),存储对象 Mark Word 的副本(Displaced Mark Word)。通过 CAS 操作将对象 Mark Word 的指针指向栈中的锁记录,若 CAS 成功,线程获取轻量级锁(Mark Word 状态变为 “轻量级锁”)。若 CAS 失败(说明其他线程已持有轻量级锁),JVM 会尝试自旋等待(线程循环重试 CAS,避免立即阻塞)。线程退出同步块时,通过 CAS 将栈中锁记录的 Displaced Mark Word 写回对象 Mark Word,若 CAS 成功,释放轻量级锁。若 CAS 失败(说明有其他线程在自旋等待),轻量级锁升级为重量级锁。
  • 重量级锁:

ReentrantLock JUC 锁

ReentrantLock 是 java.util.concurrent.locks 包中的锁,需手动释放,功能更灵活(支持公平锁、中断、超时等),底层基于 AQS(AbstractQueuedSynchronizer,抽象队列同步器)。

  • 同一线程可以多次获取同一把锁
  • 可以实现公平锁和非公平锁,默认为非公平锁,因为非公平锁的效率高;公平锁线程按请求顺序获取锁(FIFO 先进先出队列)
  • 必须通过 lock()手动获取锁,通过 unlock()手动释放
  • 线程获取锁时若被中断,可放弃等待并抛出 InterruptedException,避免无限阻塞。
  • 可设置超时时间,若超时未获取到锁则返回 false,适合需要控制等待时间的场景。
  • 通过 newCondition()创建多个 Condition 对象,实现更精细的线程通信
基于 AQS 实现

ReentrantLock 的底层依赖 AQS 框架,通过 AQS 的 state 变量记录锁的重入次数,通过 CLH 队列管理等待线程。关于这些细节在 AQS 有详细说明。

常用方法
  • lock():获取锁
  • tryLock():尝试获取锁
  • unlock():释放锁
  • newCondition():创建 Condition 对象,用于线程通信(替代 wait()/notify())
  • isLocked():判断锁是否被持有
    示例 1:
1
2
3
4
5
6
7
8
9
10
ReentrantLock lock = new ReentrantLock(); // 非公平锁

public void doTask() {
lock.lock(); // 获取锁
try {
// 临界区代码(线程安全)
} finally {
lock.unlock(); // 释放锁(必须)
}
}

示例 2:

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
34
ReentrantLock lock = new ReentrantLock();
Condition notEmpty = lock.newCondition(); // 非空队列条件
Condition notFull = lock.newCondition(); // 非满队列条件
Queue<String> queue = new LinkedList<>();
int capacity = 10;

// 生产者
public void produce(String item) throws InterruptedException {
lock.lock();
try {
while (queue.size() == capacity) {
notFull.await(); // 队列满时等待
}
queue.add(item);
notEmpty.signal(); // 通知消费者
} finally {
lock.unlock();
}
}

// 消费者
public String consume() throws InterruptedException {
lock.lock();
try {
while (queue.isEmpty()) {
notEmpty.await(); // 队列空时等待
}
String item = queue.poll();
notFull.signal(); // 通知生产者
return item;
} finally {
lock.unlock();
}
}

ReadWriteLock

线程

线程是 Java 并发编程的基本单位,其运行机制深度依赖 JVM 的内存模型、调度策略和与操作系统的交互

基本概念

进程:操作系统分配资源的基本单位(如内存、文件句柄),进程间相互隔离。
线程:进程内的执行单元,共享进程的资源(内存、文件等),但有独立的执行栈和程序计数器。Java 线程是 “映射到操作系统线程” 的轻量级执行单元

JVM 通过调用操作系统的线程 API 创建和管理线程,

JVM 中线程的结构

JVM 为每个 Java 线程维护了一套专属的数据结构,用于支持线程的独立执行和状态管理

  • 线程栈:每个线程在创建时,JVM 会为其分配独立的线程栈(内存区域,大小可通过参数设置,默认值几 MB),用来存储
    • 局部变量表:方法中的局部变量
    • 操作数栈:方法执行时的临时数据
    • 方法返回地址:当前方法执行完后,返回上一层方法的位置
    • 动态链接:指向运行时常量池中该方法的引用
  • 程序计数器:JVM 为每个线程分配一个程序计数器(寄存器级别的内存空间),用于记录当前线程正在执行的字节码指令的地址
    • 线程切换后,能通过程序计数器恢复到切换前的执行位置
    • 若执行的是 native 方法,程序计数器值为 undefined(因为 native 方法由 OS 直接执行,不经过 JVM 字节码)。
  • Java 线程与 OS 线程的映射关系:HotSpot JVM 中,每个 java.lang.Thread 对象对应一个 OS 线程,JVM 通过线程控制块(Thread Control Block, TCB) 维护两者的关联:
    • 当调用 Thread.start()时,JVM 会调用 OS 的线程创建 API,生成一个 OS 线程。
    • 该 OS 线程的入口函数是 JVM 的 JavaThread::run(),最终调用 Java 线程的 run()方法。
    • 该 OS 线程的入口函数是 JVM 的 JavaThread::run(),最终调用 Java 线程的 run()方法。

JVM 中线程的生命周期和转台转换

状态

  1. new():线程已经创建但是没有启用,Thread 对象存在,OS 线程未创建,通过 new Thread()创建,对象实例调用.start()方法退出
  2. runnable():就绪状态,线程正在 JVM 中执行,或等待 OS 调度,start()调用后进入或者进入阻塞状态;被 OS 调度暂停或者进入阻塞转态或者执行完毕
  3. Blocked():阻塞状态,线程等待获取锁资源;在锁被其它线程占有时进入该转态;在其它线程释放锁资源,当前线程获取锁资源后退出
  4. waiting():等待状态,线程等待被唤醒。调用 Object.wait(),Thread.join,进入该状态;被其它线程调用 notify/notifyALl()唤醒或者被 interrupt()中断后退出;
  5. 超时等待:
  6. Terminated:线程终止,run()方法执行完毕或者异常终止

阻塞和等待的区别:阻塞仅针对锁的竞争,线程在锁池中等待,在自动获取到锁资源后自动唤醒;等待时线程主动调用等待方法,进入等待池,需要手动唤醒。
sleep()和 wait()的区别:Threa.sleep(),线程进入超市等待转态,如果持有锁资源,不会释放锁,JVM 通过计时器唤醒;Object.wait()线程进入等待,必须释放锁资源,需要其他资源来唤醒该线程

JVM 的线程调度机制

线程调度是指 OS 或 JVM 决定哪个线程获得 CPU 时间的过程。

  1. 依赖操作系统的抢占式调度
    JVM 本身不实现线程调度,而是委托给操作系统,操作系统采用抢占式调度,高优先级线程可抢占低优先级线程的 CPU 时间。JVM 为每个线程设置优先级(1-10,默认为 5),不同操作系统对优先级的处理不同。
  2. 线程切换的代价
    当 OS 切换线程时,会发生上下文切换,操作系统先保存当前进程的状态到 TCB,恢复目标线程的转态,加载到 CPU 寄存器。上下文切换会导致性能开销,JVM 的锁优化本质就是减少上下文切换。

JVM 中线程同步与安全

线程安全的核心是解决共享资源竞争,JVM 通过内存模型和锁机制保证同步

线程的创建和终止

线程创建的 3 种方式及 JVM 行为

  • 继承 Thread 类:重写 run()方法,调用 start()时 JVM 创建 OS 线程,执行 run()函数
  • 实现 Runnable 接口:Thread 接受 Runnable 对象,start()后线程执行 Runable.run()

每种创建方式都需要 Thread.start()来触发 JVM 的 start0(),再由 OS 来创建线程

线程的终止

  1. 中断机制:调用 thread.interrupt(),设置线程的 “中断状态”,线程可通过 isInterrupted()检测并主动退出。
1
2
3
4
5
// 线程内部响应中断
while (!Thread.currentThread().isInterrupted()) {
// 业务逻辑
if (需要退出) break;
}
  1. 标志位:volatile boolean 作为终止标志,线程循环检测标志位退出

ThreadLocal

ThreadLocal 允许线程拥有 “私有变量”(每个线程独立存储一份),每个 Thread 内部有 ThreadLoaclMap(类似于 HashMap),key 是 ThreadLoacl 实例,value 是线程的私有变量,当线程调用 threadLocal.get()时,JVM 通过当前的线程找到 ThreadLoaclMap 取出对应的 value,这个值仅在当前线程有效

线程池

在主流 JVM(如 HotSpot)中,线程采用 1:1 映射模型:即 JVM 中的一个线程对应操作系统的一个原生线程(OS Thread)。

  • 每个线程有独立的线程栈,由 JVM 在创建线程时分配。
  • 操作系统需为每个原生线程分配 PCB(进程控制块)、TCB(线程控制块)等数据结构,用于存储线程状态、优先级等信息,这些资源由内核管理,开销较大。
  • 线程切换时,CPU 需保存当前线程的寄存器状态(如程序计数器、栈指针),加载新线程的状态,此过程涉及内核态与用户态切换,耗时约 1~10 微秒(具体取决于 CPU),频繁切换会严重占用 CPU 资源。
    因此线程的创建和销毁会消耗大量 JVM 和系统资源

线程池的核心设计目标是减少线程创建 / 销毁的资源开销

  • 复用线程:减少线程栈与内核资源的重复分配;核心线程在创建后不会被销毁,其线程栈和对应的内核资源(PCB/TCB)长期存在,避免了重复分配 / 释放的开销。任务执行时,直接复用已有的线程栈,任务的 run()方法会被压入该线程的栈帧中执行,执行完毕后栈帧弹出,线程继续等待下一个任务,无需重建线程栈。
  • 控制线程总数:避免 JVM 内存溢出,线程池通过 maximumPoolSize 限制最大线程数,直接规避了 “无限制创建线程” 导致的内存溢出的风险。
  • 任务队列:平衡 JVM 堆内存与线程调度;线程池的任务队列本质是堆内存中的任务缓冲,任务队列存储 Runnable 对象,当任务提交速度超过线程处理速度时,队列临时缓存任务,避免因 “线程不足” 直接拒绝任务。队列分为无界队列(理论上可缓存无限任务,但若任务过多,会导致堆内存耗尽)和有界队列(提前限制队列容量,提前限制队列容量)

  1. 线程池状态与任务调度的同步:基于 JVM 锁机制;ThreadPoolExecutor 通过重入锁(ReentrantLock) 和条件变量(Condition) 实现线程安全的状态管理和任务调度
  2. 线程池的任务队列(BlockingQueue)本身是线程安全的,确保任务的 “入队” 和 “出队” 操作对所有线程可见
  3. 线程销毁与 GC:线程池中的线程销毁后,其对应的资源会被 JVM 回收