Appearance
线程详解
了解JVM中的线程机制,需从线程实现模型、调度策略、状态转换、同步机制、底层结构及高级特性(如虚拟线程)等维度展开。以下内容基于《深入理解Java虚拟机》(第3版)及JDK 21+的最新进展,面向高级开发人员设计,注重原理深度与实践关联。
一、JVM线程的实现模型
线程的实现主要有三种模型:用户级线程(ULT, User-Level Thread)、内核级线程(KLT, Kernel-Level Thread)、混合模型(M:N模型)。Java采用1:1的内核级线程模型(即每个Java线程对应一个操作系统内核线程),这是理解JVM线程行为的核心前提。
1. 三种线程模型对比
模型 | 描述 | 优点 | 缺点 |
---|---|---|---|
用户级线程(ULT) | 线程由用户态库(如POSIX threads的用户态实现)管理,内核无法感知。 | 上下文切换快(用户态完成)、线程数可多。 | 无法利用多CPU(内核看不到线程)、IO阻塞会导致整个进程阻塞。 |
内核级线程(KLT) | 线程由内核直接管理(如Linux的pthread、Windows的Thread),每个线程对应内核线程。 | 支持多CPU并行、IO阻塞不影响其他线程。 | 上下文切换开销大(需切换内核态)、线程数受内核限制。 |
混合模型(M:N) | 多个ULT映射到少数KLT(如Solaris的LWP),内核管理KLT,用户态管理ULT。 | 兼顾ULT的轻量与KLT的并行性。 | 实现复杂,需协调用户态与内核态的调度。 |
2. Java选择1:1模型的原因
Java最初(JDK 1.2前)曾尝试过ULT模型,但因无法利用多CPU(如早期JVM在单CPU上运行,ULT足够,但多CPU时代劣势明显)和IO阻塞问题(如线程调用read()
会阻塞整个进程),最终切换到1:1的KLT模型。
关键结论:
- Java线程的生命周期(创建、启动、终止)完全依赖操作系统内核;
- Java线程的调度权由操作系统掌握(JVM仅负责线程的创建与状态转换通知);
- 线程的上下文切换(如从运行到阻塞)需由内核完成,开销远大于用户级线程(约为微秒级 vs 纳秒级)。
3. 1:1模型的局限性与优化
1:1模型的最大问题是线程数过多导致的资源耗尽(每个内核线程需占用栈空间、内核数据结构等资源)。例如,若每个线程栈大小为1MB,1000个线程就需1GB内存。
优化方案:
- 通过
-Xss
参数减小线程栈大小(如-Xss256k
),但需避免StackOverflowError
(栈溢出); - 使用线程池复用线程(减少线程创建/销毁开销);
- JDK 21引入虚拟线程(Virtual Threads)(M:N模型),彻底解决1:1模型的高并发瓶颈(详见后文)。
二、JVM线程的调度策略
线程调度是指操作系统内核决定哪个线程获得CPU时间片的过程。Java采用抢占式调度(Preemptive Scheduling),而非协同式调度(Cooperative Scheduling)。
1. 抢占式调度的核心特性
- 优先级驱动:Java线程有10个优先级(
Thread.MIN_PRIORITY=1
到Thread.MAX_PRIORITY=10
),默认优先级为5(Thread.NORM_PRIORITY
)。操作系统会将Java优先级映射到内核优先级(如Windows的7级、Linux的动态优先级),但映射关系不保证一致(例如,Linux会根据线程的CPU使用情况动态调整优先级,导致Java高优先级线程可能无法获得更多时间片)。 - 时间片轮转:每个线程获得的CPU时间片由操作系统决定(如Linux的CFS调度器默认时间片为10-20ms)。当时间片用完,线程会被挂起,放入就绪队列,等待下一次调度。
- 中断机制:操作系统可强制中断当前运行的线程(如更高优先级线程就绪),切换到其他线程执行。
2. Java优先级的“不可靠性”
Java优先级的设计初衷是提示操作系统调度器“该线程更重要”,但无法保证高优先级线程一定先执行。原因包括:
- 操作系统映射差异:例如,Windows将Java的10个优先级映射到7个内核优先级,导致多个Java优先级对应同一个内核优先级;
- 调度器算法优化:Linux的CFS(完全公平调度器)采用“权重分配时间”的策略,高优先级线程的权重更高,获得的时间片更多,但并非“绝对优先”;
- 线程状态影响:若高优先级线程处于
WAITING
或BLOCKED
状态,低优先级线程仍可执行。
3. 调度算法对Java的影响
- CPU密集型任务:应设置线程数等于CPU核心数(避免上下文切换开销);
- IO密集型任务:应设置线程数大于CPU核心数(利用IO等待时间让其他线程运行);
- 实时任务:Java的优先级机制不足以满足硬实时需求(需使用
RealtimeThread
或操作系统级实时调度策略,如Linux的SCHED_FIFO
)。
三、JVM线程的状态转换
Java线程的状态定义在Thread.State
枚举中,共6种状态。状态转换是理解线程行为的关键,需重点区分RUNNABLE
、BLOCKED
、WAITING
的差异。
1. 状态定义与转换图
状态 | 描述 |
---|---|
NEW | 线程已创建但未调用start() 方法(未启动)。 |
RUNNABLE | 线程正在运行或等待操作系统调度(JVM层面无“运行中”状态,统一归为RUNNABLE )。 |
BLOCKED | 线程等待监视器锁(如进入synchronized 块但未获得锁)。 |
WAITING | 线程无限期等待其他线程唤醒(如wait() 、join() 、park() )。 |
TIMED_WAITING | 线程有限期等待时间到或其他线程唤醒(如sleep(1000) 、wait(1000) )。 |
TERMINATED | 线程执行完毕(run() 方法返回)或异常终止。 |
2. 关键状态转换路径
(1)NEW → RUNNABLE
调用Thread.start()
方法后,JVM会向操作系统申请创建内核线程,成功后线程进入RUNNABLE
状态(等待调度)。
注意:start()
方法只能调用一次,多次调用会抛出IllegalThreadStateException
。
(2)RUNNABLE → BLOCKED
当线程尝试进入synchronized
块或方法时,若监视器锁已被其他线程持有,则当前线程会被放入监视器的EntryList(等待队列),状态变为BLOCKED
。
示例:
java
synchronized (lock) {
// 若lock已被其他线程持有,当前线程进入BLOCKED状态
}
(3)RUNNABLE → WAITING
线程调用无超时的等待方法时,会释放持有的监视器锁,进入监视器的WaitSet(等待队列),状态变为WAITING
。需其他线程调用notify()
或notifyAll()
唤醒。
触发方法:
Object.wait()
(需在synchronized
块中调用);Thread.join()
(等待目标线程终止);LockSupport.park()
(无超时的线程挂起)。
示例:
java
synchronized (lock) {
while (condition not met) {
lock.wait(); // 释放lock,进入WAITING状态
}
}
(4)RUNNABLE → TIMED_WAITING
线程调用有超时的等待方法时,状态变为TIMED_WAITING
。超时后自动唤醒,或被其他线程提前唤醒。
触发方法:
Thread.sleep(long)
(不释放监视器锁);Object.wait(long)
(释放监视器锁);Thread.join(long)
(等待目标线程终止,超时返回);LockSupport.parkNanos(long)
/parkUntil(long)
(有超时的线程挂起)。
注意:sleep()
不会释放监视器锁,而wait()
会释放——这是两者的核心区别。
(5)BLOCKED/WAITING/TIMED_WAITING → RUNNABLE
- BLOCKED → RUNNABLE:当持有监视器锁的线程释放锁(如
synchronized
块执行完毕),EntryList中的线程会竞争锁,成功的线程进入RUNNABLE
状态; - WAITING → RUNNABLE:其他线程调用
notify()
或notifyAll()
,将WaitSet中的线程移至EntryList,竞争锁成功后进入RUNNABLE
; - TIMED_WAITING → RUNNABLE:超时时间到,或被其他线程唤醒,进入EntryList竞争锁。
(6)RUNNABLE → TERMINATED
线程的run()
方法执行完毕,或因未捕获的异常终止,状态变为TERMINATED
。此时线程无法再回到其他状态。
3. 常见误区澄清
- 误区1:
RUNNABLE
状态等于“正在运行”。
错。RUNNABLE
包含正在运行(获得CPU时间片)和等待调度(处于就绪队列)两种情况。JVM层面不区分这两种状态,因为调度权在操作系统。 - 误区2:
BLOCKED
和WAITING
都是“等待”,无区别。
错。BLOCKED
是等待监视器锁(被动等待,需其他线程释放锁);WAITING
是等待其他线程唤醒(主动等待,需其他线程调用notify()
)。 - 误区3:
sleep()
会释放监视器锁。
错。sleep()
仅让线程让出CPU时间片,不释放任何锁(包括synchronized
锁)。
四、JVM线程的底层结构
每个Java线程对应一个操作系统内核线程,同时JVM为每个线程维护了私有数据结构,用于支撑字节码执行和本地方法调用。
1. 线程私有区域
根据《Java虚拟机规范》,JVM的内存区域分为线程私有和线程共享两部分。线程私有区域包括:
- 程序计数器(PC Register):
记录当前线程执行的字节码指令地址(若执行native
方法,则PC为undefined
)。
作用:线程切换时恢复执行位置(上下文切换的关键数据)。
特点:唯一不会抛出OutOfMemoryError
的区域。 - 虚拟机栈(VM Stack):
每个方法调用对应一个栈帧(Stack Frame),栈帧包含局部变量表(存储方法参数和局部变量)、操作数栈(字节码指令的运算空间)、动态链接(指向方法区的方法引用)、返回地址(方法执行完毕后回到的位置)。
作用:支撑方法的调用与返回。
特点:栈深度由-Xss
参数控制(默认1MB),栈溢出会抛出StackOverflowError
(如递归过深);若栈扩展时无法申请到内存,会抛出OutOfMemoryError
(如线程数过多)。 - 本地方法栈(Native Method Stack):
类似虚拟机栈,但用于执行native
方法(如Object.hashCode()
、Thread.start()
)。
特点:由操作系统实现(如Linux的pthread
栈),同样会抛出StackOverflowError
或OutOfMemoryError
。
2. 线程共享区域
- 堆(Heap):存储对象实例和数组,是GC的主要区域(线程共享);
- 方法区(Method Area):存储类元信息(如类名、方法表、字段表)、常量、静态变量等(线程共享);
- 运行时常量池(Runtime Constant Pool):方法区的一部分,存储编译期生成的常量和运行期动态生成的常量(如
String.intern()
的结果)。
3. 线程与内存的关系
线程的私有区域(虚拟机栈、本地方法栈、程序计数器)随线程创建而分配,随线程终止而释放;共享区域(堆、方法区)则由JVM统一管理,生命周期与JVM一致。
关键结论:
- 线程数越多,私有区域的总内存占用越大(如1000个线程,每个栈1MB,需1GB内存);
- 堆内存的分配与线程无关(对象实例由所有线程共享),但对象的访问需通过线程的栈帧(如局部变量表中的对象引用)。
五、JVM线程的同步机制
线程同步是解决并发安全(原子性、可见性、有序性)的核心手段。JVM提供了多种同步机制,其中**synchronized
和volatile
**是最基础的,JUC包(java.util.concurrent
)则提供了更灵活的同步工具(如ReentrantLock
、Condition
、Atomic
类)。
1. synchronized
:监视器锁(Monitor)的实现
synchronized
是Java中最常用的同步关键字,其底层依赖监视器锁(Monitor)——每个对象(包括数组、类对象)都有一个与之关联的Monitor。
(1)Monitor的结构
Monitor由操作系统内核实现(如Linux的futex
、Windows的Critical Section
),主要包含以下组件:
- Owner:当前持有Monitor的线程(只能有一个);
- EntryList:等待获取Monitor的线程队列(
BLOCKED
状态); - WaitSet:等待唤醒的线程队列(
WAITING
或TIMED_WAITING
状态); - 计数器:记录线程重入的次数(
synchronized
是可重入锁)。
(2)synchronized
的执行流程
当线程进入synchronized
块或方法时,会执行以下步骤:
- 尝试获取Monitor:
- 若Monitor的
Owner
为null
(未被持有),则当前线程成为Owner
,计数器设为1; - 若Monitor的
Owner
为当前线程(重入),计数器加1; - 若Monitor的
Owner
为其他线程,则当前线程进入EntryList
,状态变为BLOCKED
。
- 若Monitor的
- 执行同步代码:
线程持有Monitor期间,可执行同步代码(此时其他线程无法进入该synchronized
块)。 - 释放Monitor:
当线程退出synchronized
块或方法时,计数器减1;若计数器变为0,则释放Monitor(Owner
设为null
),并唤醒EntryList
中的线程(竞争锁)。
(3)synchronized
的优化
JDK 6及以后,synchronized
的性能得到了极大优化,引入了偏向锁、轻量级锁、重量级锁的升级机制(锁膨胀):
- 偏向锁:针对单线程访问的场景,将对象的
Mark Word
设置为偏向线程ID,避免每次获取锁都进行CAS操作(开销最小); - 轻量级锁:针对多线程交替访问的场景,通过CAS将对象的
Mark Word
设置为线程栈帧中的锁记录(Lock Record
),避免进入内核态(开销中等); - 重量级锁:针对多线程竞争的场景,将对象的
Mark Word
设置为指向Monitor的指针,此时线程需进入内核态等待(开销最大)。
优化效果:synchronized
的性能已接近ReentrantLock
(JUC包中的可重入锁),甚至在某些场景下更优(如锁膨胀的自动管理)。
2. volatile
:可见性与有序性的保证
volatile
是轻量级的同步关键字,用于修饰变量,无法修饰方法或代码块。其核心作用是保证变量的可见性和禁止指令重排序,但不保证原子性。
(1)可见性的实现
当线程修改volatile
变量的值时,JVM会执行以下操作:
- 将变量的值从线程的工作内存(CPU缓存)刷新到主内存;
- invalidate(失效)其他线程的工作内存中的该变量副本(迫使其他线程从主内存读取最新值)。
示例:
java
private volatile boolean flag = false;
// 线程A
flag = true; // 刷新到主内存,失效其他线程的flag副本
// 线程B
while (!flag) { // 从主内存读取最新的flag值
// 执行任务
}
(2)禁止指令重排序的实现
volatile
变量的读写操作会插入内存屏障(Memory Barrier),阻止编译器和处理器对指令进行重排序:
- 写屏障(Store Barrier):在
volatile
变量的写操作后插入,保证写操作之前的所有指令都已执行完毕,且结果已刷新到主内存; - 读屏障(Load Barrier):在
volatile
变量的读操作前插入,保证读操作之后的所有指令都不会提前执行,且读操作从主内存读取最新值。
经典场景:双重检查锁定(DCL)的单例模式:
java
public class Singleton {
private static volatile Singleton instance; // 必须用volatile修饰
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { // 第一次检查(无锁)
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查(有锁)
instance = new Singleton(); // 禁止重排序(避免半初始化)
}
}
}
return instance;
}
}
解释:instance = new Singleton()
分为三步:① 分配内存;② 初始化对象;③ 将instance
指向内存地址。若未用volatile
,编译器可能重排序为①→③→②,导致线程B在第一次检查时看到instance
非null
(但对象未初始化),从而返回半初始化的对象。
(3)volatile
的局限性
volatile
无法保证原子性,例如:
java
private volatile int count = 0;
// 多个线程同时执行
count++; // 非原子操作(读取→加1→写入)
count++
由三个步骤组成,volatile
只能保证每个步骤的可见性,但无法保证三个步骤的原子性(如线程A读取count=0
,线程B读取count=0
,都加1,最终count=1
而非2)。此时需用AtomicInteger
(原子类)或synchronized
保证原子性。
3. JUC包中的同步工具
java.util.concurrent
包提供了更灵活的同步机制,弥补了synchronized
和volatile
的不足:
ReentrantLock
:可重入锁,支持公平锁(按等待顺序获取锁)和非公平锁(默认),提供tryLock()
(尝试获取锁,超时返回)、lockInterruptibly()
(可中断的锁获取)等功能;Condition
:基于ReentrantLock
实现的条件变量,支持多个等待队列(synchronized
的WaitSet
只有一个),例如生产者-消费者模型中,可分别定义“空队列”和“满队列”的条件;Atomic
类:如AtomicInteger
、AtomicLong
,基于CAS(比较并交换)实现原子操作,避免使用锁(性能更高);CountDownLatch
:倒计时门闩,用于等待多个线程完成任务(如主线程等待子线程全部执行完毕);CyclicBarrier
:循环屏障,用于多个线程同步到某个点(如多个线程都到达屏障后再继续执行);Semaphore
:信号量,用于控制并发线程数(如限制数据库连接数)。
六、JVM线程的高级特性:虚拟线程(Virtual Threads)
JDK 21引入的虚拟线程(Virtual Threads)是Java并发模型的重大突破,彻底解决了1:1模型的高并发瓶颈。虚拟线程是用户级线程(ULT),由JVM管理,而非操作系统内核线程,采用M:N模型(多个虚拟线程映射到一个操作系统内核线程,称为载体线程(Carrier Thread))。
1. 虚拟线程的核心优势
- 轻量级:虚拟线程的栈空间由JVM动态分配(初始仅几KB),且在IO阻塞时会释放载体线程(让其他虚拟线程运行),因此可创建百万级虚拟线程(而1:1模型的线程数通常限制在几千);
- 低开销:虚拟线程的上下文切换在用户态完成(无需切换内核态),开销远小于内核线程(约为纳秒级 vs 微秒级);
- 兼容性:虚拟线程的API与普通线程完全一致(如
Thread.start()
、Runnable
),现有代码无需修改即可迁移(只需将线程池改为Executors.newVirtualThreadPerTaskExecutor()
)。
2. 虚拟线程的实现原理
虚拟线程的核心机制是协作式调度(由JVM的ForkJoinPool管理载体线程)和栈拆分(Stack Splitting):
- 协作式调度:当虚拟线程执行IO操作(如
Socket.read()
)时,JVM会将虚拟线程的栈保存到堆中,并释放载体线程(让其他虚拟线程运行);当IO操作完成后,JVM会将虚拟线程的栈恢复到载体线程,继续执行; - 栈拆分:虚拟线程的栈由多个**栈片段(Stack Chunk)**组成,每个栈片段对应方法调用的一部分(如调用
methodA()
时分配一个栈片段,调用methodB()
时分配另一个栈片段)。当栈片段满时,JVM会将其保存到堆中,并分配新的栈片段(避免栈溢出)。
3. 虚拟线程的使用场景
虚拟线程适合IO密集型任务(如Web服务、数据库访问、消息队列消费),因为这些任务的大部分时间都在等待IO(如网络响应、磁盘IO),虚拟线程可利用等待时间让其他虚拟线程运行,提高载体线程的利用率。
不适合的场景:CPU密集型任务(如大规模计算),因为虚拟线程的上下文切换开销虽小,但CPU密集型任务需要持续占用CPU,虚拟线程无法提高性能(此时应使用普通线程,线程数等于CPU核心数)。
4. 虚拟线程的示例
java
// 创建虚拟线程并启动
Thread virtualThread = Thread.startVirtualThread(() -> {
// 执行IO密集型任务(如HTTP请求)
try (var client = HttpClient.newHttpClient()) {
var request = HttpRequest.newBuilder()
.uri(URI.create("https://example.com"))
.build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
});
// 使用虚拟线程池(推荐)
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 100000; i++) {
executor.submit(() -> {
// 执行IO密集型任务
System.out.println(Thread.currentThread().getName());
});
}
} // 自动关闭线程池,等待所有任务完成
七、JVM线程的最佳实践
1. 避免过多线程创建
- 使用线程池(如
ThreadPoolExecutor
、Executors.newVirtualThreadPerTaskExecutor()
)复用线程,减少线程创建/销毁开销; - 线程池的核心参数需根据任务类型调整(CPU密集型:核心线程数=CPU核心数;IO密集型:核心线程数=CPU核心数×(1+IO等待时间/CPU处理时间))。
2. 避免死锁
死锁的四个必要条件:互斥条件、请求与保持条件、不可剥夺条件、循环等待条件。避免死锁的方法:
- 顺序获取锁:所有线程按相同的顺序获取锁(如先获取锁A,再获取锁B);
- 设置超时时间:使用
ReentrantLock.tryLock(long, TimeUnit)
尝试获取锁,超时则放弃; - 避免嵌套锁:尽量减少锁的嵌套(如
synchronized
块中调用其他synchronized
方法)。
3. 优先使用volatile
而非synchronized
若只需保证可见性和有序性(无需原子性),优先使用volatile
(开销更小)。例如,状态标记(如flag
)、单例模式的instance
变量。
4. 优先使用并发集合而非同步集合
- 并发集合(如
ConcurrentHashMap
、CopyOnWriteArrayList
)采用分段锁、写时复制等机制,性能远高于同步集合(如Hashtable
、Vector
); ConcurrentHashMap
的put()
、get()
方法是线程安全的,且无需额外同步(Hashtable
的put()
方法用synchronized
修饰,性能低)。
5. 避免使用过时的线程方法
Thread.stop()
:强制终止线程,会导致线程持有的锁未释放(数据不一致);Thread.suspend()
:挂起线程,会导致线程持有的锁未释放(死锁风险);Thread.resume()
:恢复线程,无法保证线程的状态(如线程已被终止)。
替代方案:使用interrupt()
方法中断线程(协作式中断),线程需自行处理中断(如检查isInterrupted()
状态,退出循环)。
6. 使用虚拟线程优化IO密集型任务
对于Web服务、数据库访问等IO密集型任务,将普通线程池替换为虚拟线程池(Executors.newVirtualThreadPerTaskExecutor()
),可显著提高并发量(从几千到百万),降低资源占用。
八、总结
JVM线程的机制是Java并发编程的基础,深入理解以下内容是成为高级开发人员的关键:
- 线程实现模型:1:1的内核级线程模型(JDK 21前)与M:N的虚拟线程模型(JDK 21后);
- 线程调度:抢占式调度、优先级的不可靠性、调度算法对性能的影响;
- 状态转换:
RUNNABLE
、BLOCKED
、WAITING
的差异及转换路径; - 同步机制:
synchronized
的Monitor实现、volatile
的可见性与有序性、JUC包的高级同步工具; - 高级特性:虚拟线程的原理与使用场景。
通过掌握这些知识,可编写高效、安全、可扩展的并发程序,应对高并发场景的挑战。