在Java中,对线程,产生足够多层次
足够多信息量的积累,才可以去理解线程。
我们需要对线程,产生那些领域,深刻全面的认识。才算理解线程呢。
1.线程的原理
2.线程的基本状态
3.在代码层面,使用线程,应该掌握的信息量
1.线程的原理
是**"线程为什么能工作"**------这正好触及了计算机科学最核心的抽象之一。
我来分三层解释:硬件基础 → 操作系统机制 → JVM封装,最后解释为什么它符合你"想象中的效果"。
一、硬件基础:CPU如何"同时"做多件事
1. 物理并行(真正的同时)
多核CPU的每个核心有独立的:
- ALU(算术逻辑单元)
- 寄存器组(通用寄存器、PC程序计数器、栈指针)
- L1/L2缓存
所以双核CPU真正能同时执行两个指令流。
2. 时间分片(宏观并行,微观串行)
单核CPU通过时钟中断来实现"伪并行":
- CPU每毫秒产生一次时钟中断
- 操作系统在中断处理函数中,可以强制切换当前执行的指令流
这就是你想象中的效果:多个任务看起来在同时推进。
二、操作系统机制:从CPU指令到线程的抽象
核心数据结构:TCB(线程控制块)
操作系统为每个线程维护这样一个结构(简化版):
c
struct thread_control_block {
// 寄存器现场(保存/恢复用)
uint64_t rax, rbx, rcx, rdx; // 通用寄存器
uint64_t rsp; // 栈指针
uint64_t rip; // 指令指针(PC)
// 线程标识
int thread_id;
int process_id;
// 调度信息
int priority;
int state; // RUNNING, READY, BLOCKED
// 资源引用
void* kernel_stack;
void* user_stack;
struct mm_struct* memory_map;
};
上下文切换(Context Switch)的核心操作
当操作系统决定从线程A切换到线程B时:
assembly
; 伪汇编:保存线程A的现场
save_context:
mov [A_tcb.rax], rax
mov [A_tcb.rbx], rbx
mov [A_tcb.rcx], rcx
mov [A_tcb.rsp], rsp
mov [A_tcb.rip], rcx ; 返回地址
; 切换到线程B的地址空间(如果属于不同进程)
mov cr3, [B_tcb.page_table]
; 恢复线程B的现场
mov rax, [B_tcb.rax]
mov rbx, [B_tcb.rbx]
mov rcx, [B_tcb.rcx]
mov rsp, [B_tcb.rsp]
jmp [B_tcb.rip] ; 跳转到B上次停下的位置
关键点 :整个切换过程只耗时几十纳秒到几微秒,你完全感知不到。
三、JVM如何封装OS线程
Java线程的1:1映射模型
java
Thread t = new Thread(() -> {
System.out.println("Hello from thread");
});
t.start();
JVM内部实际调用(简化流程):
cpp
// OpenJDK 源码简化版
JVM_ENTRY(void, JVM_StartThread(JNIEnv* env, jobject jthread))
native_thread = new JavaThread(&thread_entry, stack_size);
os::create_thread(native_thread, THREAD_NORM_PRIORITY);
os::start_thread(native_thread);
JVM_END
// Linux实现
bool os::create_thread(Thread* thread, int priority) {
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setstacksize(&attr, thread->stack_size());
int ret = pthread_create(&thread->pd()->pthread, &attr,
thread_native_entry, thread);
return ret == 0;
}
结论 :Java的Thread对象就是OS原生线程(pthread)的一个包装。
四、为什么能实现"想象中的效果"
你想象中的线程效果应该是:
- 多个任务能同时运行
- 一个任务阻塞(如等IO),不影响其他任务
- 它们共享内存,但又有独立的状态
这背后的实现逻辑:
1. 并发(Concurrency)vs 并行(Parallelism)
- 并行:多核CPU真正同时执行(物理现实)
- 并发:单核通过时间分片制造的幻觉(你想象中的效果)
操作系统通过调度算法(CFS、RR等)让你感知不到切换。
2. 阻塞不占CPU的关键:线程状态迁移
当你执行read()系统调用:
java
socket.read(buffer); // 阻塞等待网络数据
OS内部:
- 线程主动放弃CPU,状态从
RUNNING→BLOCKED - 将线程从运行队列移到等待队列(关联到socket的事件)
- 调度器选择另一个
READY线程运行 - 网卡数据到达时,硬件触发中断,OS将线程移回
READY队列
关键 :阻塞的线程完全不消耗CPU时间片。
3. 共享内存的实现:虚拟地址空间
不同线程属于同一进程,共享页表:
虚拟地址空间(每个线程看到一样的):
0x00000000 ┌─────────────┐
│ text段 │ ← 代码(共享)
├─────────────┤
│ data段 │ ← 全局变量(共享)
├─────────────┤
0x7fffffff │ heap │ ← 堆(共享,new出来的对象)
└─────────────┘
但是每个线程有独立的:
- 栈(虚拟地址不同范围)
- 寄存器(物理上独立)
所以:两个线程可以同时访问同一个堆对象(需要同步),但局部变量互不干扰。
五、一个具体的底层示例
用Linux的clone()系统调用直接创建线程(绕过Java):
c
#define _GNU_SOURCE
#include <sched.h>
#include <stdio.h>
#include <sys/wait.h>
int thread_func(void* arg) {
printf("Thread running with PID: %d\n", getpid());
return 0;
}
int main() {
char stack[1024 * 1024]; // 1MB栈空间
// CLONE_VM: 共享内存空间(关键!)
// CLONE_SIGHAND: 共享信号处理器
// CLONE_FILES: 共享文件描述符
int tid = clone(thread_func, stack + sizeof(stack),
CLONE_VM | CLONE_SIGHAND | CLONE_FILES | SIGCHLD,
NULL);
waitpid(tid, NULL, 0);
return 0;
}
这就是线程的本质 :clone()时指定CLONE_VM标志,让多个执行流共享同一个地址空间。
总结:三层抽象
| 层次 | 实现机制 | 作用 |
|---|---|---|
| 硬件 | 多核 + 时间中断 | 提供同时/分时执行的能力 |
| 操作系统 | TCB + 上下文切换 + 调度算法 | 管理线程生命周期,制造并发幻觉 |
| JVM | 1:1映射到pthread + Java对象封装 | 让Java代码能安全地使用OS线程 |
回答你的核心问题:
为什么能实现想象中的线程效果?
因为:
- 硬件提供了真正的并行能力 (多核)和分时机制(时钟中断)
- 操作系统封装了上下文切换,让你感知不到切花费的时间(微秒级)
- 共享内存加独立栈的设计,既满足协作又满足隔离
你想象中的"多个任务同时运行、一个阻塞不影响其他",本质上是操作系统通过中断、队列、调度算法共同制造的优雅幻觉。
。
2.线程的基本状态
Java线程生命周期与状态:完整深度解析
让我从底层原理 → JVM实现 → 代码验证三个层面,给你彻底讲透线程状态。
一、六种状态总览(Thread.State枚举)
java
public enum State {
NEW, // 新生,尚未启动
RUNNABLE, // 可运行(可能正在执行,也可能在等待CPU)
BLOCKED, // 阻塞(等待获取锁)
WAITING, // 等待(无限期等待被唤醒)
TIMED_WAITING, // 限时等待(有时间期限的等待)
TERMINATED // 终止(执行完毕)
}
核心区别图:
NEW → RUNNABLE → TERMINATED
↓
(进入同步块时)
↓
BLOCKED ←→ RUNNABLE
↓
(调用wait/park/join)
↓
WAITING / TIMED_WAITING
二、每个状态的深度剖析
1. NEW(新生)
触发条件:
java
Thread t = new Thread(() -> {});
// 此时t处于NEW状态
底层特征:
- JVM已分配Thread对象,分配了栈内存(通常1MB)
- 但没有关联操作系统线程(没有pthread_create)
- TCB(线程控制块)尚未创建
验证方法:
java
Thread t = new Thread(() -> {});
System.out.println(t.getState()); // NEW
System.out.println(t.isAlive()); // false
2. RUNNABLE(可运行)
触发条件:
java
t.start(); // NEW → RUNNABLE
底层真相:RUNNABLE实际对应OS的两种状态
c
// Linux内核中的线程状态
enum task_state {
TASK_RUNNING, // 正在运行或就绪(Java的RUNNABLE)
TASK_INTERRUPTIBLE, // 阻塞IO(Java也标记为RUNNABLE!)
TASK_UNINTERRUPTIBLE,
TASK_STOPPED,
TASK_ZOMBIE
};
重要:Java的RUNNABLE ≠ CPU正在运行,而是**"在JVM看来可以运行"**,包括:
- 正在CPU执行
- 在就绪队列等待CPU时间片
- 正在执行系统调用(如socket.read()) ← 这个最容易被误解
验证例子:
java
Thread t = new Thread(() -> {
try {
Thread.sleep(10000); // 调用OS的nanosleep系统调用
} catch (InterruptedException e) {}
});
t.start();
Thread.sleep(100);
System.out.println(t.getState()); // TIMED_WAITING,不是RUNNABLE
3. BLOCKED(阻塞)
触发条件:等待synchronized锁时
java
public class BlockedDemo {
private static final Object lock = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (lock) {
while (true) {} // 死循环持有锁
}
});
Thread t2 = new Thread(() -> {
synchronized (lock) { // t2在这里阻塞
System.out.println("不会被打印");
}
});
t1.start();
Thread.sleep(100); // 确保t1先获得锁
t2.start();
Thread.sleep(100);
System.out.println(t2.getState()); // BLOCKED
}
}
底层实现:
java
// JVM内部,synchronized对应monitorenter/monitorexit字节码
// 当锁被持有时,线程进入ObjectMonitor的_WaitSet或_EntryList
BLOCKED vs WAITING区别:
- BLOCKED:等待其他线程释放锁(被动)
- WAITING:主动调用wait/park等待被其他线程唤醒(主动)
4. WAITING(无限期等待)
触发方式(3种):
java
// 1. Object.wait()
synchronized (lock) {
lock.wait(); // 必须持有锁,释放锁并等待
}
// 2. Thread.join()
thread.join(); // 等待thread执行完毕
// 3. LockSupport.park()
LockSupport.park(); // 最底层实现,不需要锁
底层机制:
java
// Object.wait() 的底层实现(简化)
// 1. 释放synchronized锁
// 2. 线程进入ObjectMonitor的_WaitSet
// 3. 状态设置为WAITING
// 4. 调用os::sleep()让出CPU
完整示例:
java
Thread waiter = new Thread(() -> {
synchronized (lock) {
try {
System.out.println("waiter: 开始等待");
lock.wait(); // 释放锁,进入WAITING
System.out.println("waiter: 被唤醒");
} catch (InterruptedException e) {}
}
});
Thread notifier = new Thread(() -> {
synchronized (lock) {
System.out.println("notifier: 准备唤醒");
lock.notify(); // 唤醒waiter
// 但notifier此时还没释放锁!
Thread.sleep(1000);
}
});
waiter.start();
Thread.sleep(100);
waiter.start();
System.out.println(waiter.getState()); // WAITING
5. TIMED_WAITING(限时等待)
触发方式(5种常见):
java
// 1. Thread.sleep(long millis)
Thread.sleep(1000);
// 2. Object.wait(long timeout)
synchronized (lock) {
lock.wait(1000);
}
// 3. Thread.join(long millis)
thread.join(1000);
// 4. LockSupport.parkNanos(long nanos)
LockSupport.parkNanos(1_000_000_000L);
// 5. LockSupport.parkUntil(long deadline)
LockSupport.parkUntil(System.currentTimeMillis() + 1000);
底层差异:
c
// Linux内核中
// sleep/wait带超时 → 使用高精度定时器(hrtimer)
// 超时后由内核触发中断唤醒线程
与WAITING的唯一区别 :有自动唤醒机制,超时后自动回到RUNNABLE。
6. TERMINATED(终止)
触发条件:
java
// 1. run()方法正常返回
thread.run(); // 执行完毕
// 2. 未捕获的异常抛出
thread.run(); // 抛出异常退出
// 3. 调用stop()(已废弃,不安全)
thread.stop(); // 不推荐
底层清理:
- 释放持有的所有锁(通过monitorexit)
- 线程栈内存被回收(但Thread对象仍在堆中)
- OS线程被销毁(pthread_join或detach)
验证:
java
Thread t = new Thread(() -> {});
t.start();
t.join();
System.out.println(t.getState()); // TERMINATED
System.out.println(t.isAlive()); // false
// 仍然可以访问t的name、priority等属性
三、状态转换的完整路径图
┌─────────────────────────────────────┐
│ │
│ ╔═══╗ │
▼ ║NEW║ │
t.start() ╚═══╝ │
│ │
▼ │
╔════════════╗ │
║ RUNNABLE ║ │
╚════════════╝ │
│ │
┌──────────┼──────────┬──────────────┐ │
│ │ │ │ │
▼ ▼ ▼ ▼ │
获取锁失败 sleep/wait wait/park run()结束 │
/IO阻塞 /join带超时 /无超时 │
│ │ │ │ │
▼ ▼ ▼ ▼ │
╔═══════╗ ╔════════════╗ ╔═══════╗ ╔══════════╗ │
║BLOCKED║ ║TIMED_ ║ ║WAITING║ ║TERMINATED║ │
╚═══════╝ ║WAITING ║ ╚═══════╝ ╚══════════╝ │
╚════════════╝ │ │ │
│ │ │ │
超时/唤醒 被notify/ (终态) │
锁可用 /interrupt │
│ │ │
└────────────┘ │
│ │
└─────────────────────────────────┘
四、容易混淆的关键场景
场景1:Thread.sleep() vs Object.wait()
java
// sleep:不释放锁
synchronized (lock) {
Thread.sleep(1000); // 其他线程无法进入此同步块
}
// wait:释放锁
synchronized (lock) {
lock.wait(1000); // 锁被释放,其他线程可以进入
}
场景2:BLOCKED vs WAITING 的本质区别
java
// BLOCKED: 争夺锁失败
synchronized (lock) { // 如果lock被持有,进入BLOCKED
}
// WAITING: 已经获得锁,主动放弃
synchronized (lock) {
lock.wait(); // 已持有锁,主动进入WAITING
}
场景3:IO操作的状态谜团
java
// 传统IO(BIO):在JVM看来是RUNNABLE
socket.read(); // 底层调用read系统调用,线程阻塞在OS
// JVM不知道OS在阻塞,所以状态仍是RUNNABLE
// NIO(非阻塞)
socketChannel.read(buffer); // 立即返回,不会阻塞
// 通过Selector管理多路复用
五、实战:如何监控和调试状态
1. 使用jstack查看线程状态
bash
jstack <pid>
输出示例:
"pool-1-thread-1" #11 prio=5 os_prio=0 tid=0x00007f... nid=0x3e2c waiting on condition [0x00007f...]
java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:304)
2. 编程式监控
java
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
long[] threadIds = threadMXBean.getAllThreadIds();
for (long id : threadIds) {
ThreadInfo info = threadMXBean.getThreadInfo(id);
System.out.printf("Thread %d: %s\n", id, info.getThreadState());
}
3. 死锁检测
java
// 自动检测死锁
long[] deadlockedIds = threadMXBean.findDeadlockedThreads();
if (deadlockedIds != null) {
System.out.println("Deadlock detected!");
for (long id : deadlockedIds) {
ThreadInfo info = threadMXBean.getThreadInfo(id);
System.out.println(info);
}
}
六、常见面试陷阱题
题1:以下代码输出什么?
java
Thread t = new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {}
});
System.out.println(t.getState()); // NEW
t.start();
System.out.println(t.getState()); // RUNNABLE(可能输出RUNNABLE或TIMED_WAITING?)
Thread.sleep(500);
System.out.println(t.getState()); // TIMED_WAITING(因为sleep还没结束)
题2:synchronized代码块内调用wait()后,线程处于什么状态?
- 答案:WAITING(释放了锁)
题3:线程在socket.read()阻塞时,Java层面是什么状态?
- 答案:RUNNABLE(因为JVM无法感知OS的阻塞)
七、性能影响与调优建议
| 状态 | CPU消耗 | 内存占用 | 典型问题 |
|---|---|---|---|
| RUNNABLE | 高 | 正常 | CPU飙升,上下文切换频繁 |
| BLOCKED | 低 | 正常 | 锁竞争激烈,吞吐量下降 |
| WAITING | 极低 | 正常 | 等待时间过长,响应慢 |
| TIMED_WAITING | 极低 | 正常 | 超时时间不合理 |
调优技巧:
- 减少BLOCKED:使用更细粒度的锁、读写锁、CAS
- 避免长时间WAITING:设置合理超时、使用异步回调
- 监控线程数:过多的WAITING容易隐藏内存泄漏(ThreadLocal)
如果你需要,我可以继续深入:
- 具体状态转换的代码示例集
- 如何通过JNI/Instrumentation观测底层状态
- 虚拟线程(Virtual Thread)的状态模型差异
3.在代码层面,使用线程,应该掌握的信息量
一、基础API层(必须100%掌握)
1.1 线程创建方式及本质区别
java
// 方式1:继承Thread(不推荐,单一继承限制)
class MyThread extends Thread {
@Override
public void run() {
System.out.println("线程执行");
}
}
// 问题:每次执行都要创建新线程,无法复用
// 方式2:实现Runnable(推荐)
Runnable task = () -> System.out.println("执行");
Thread t = new Thread(task);
t.start();
// 方式3:实现Callable(可返回值+抛异常)
Callable<String> callable = () -> {
Thread.sleep(1000);
return "结果";
};
FutureTask<String> futureTask = new FutureTask<>(callable);
new Thread(futureTask).start();
String result = futureTask.get(); // 阻塞获取
// 方式4:线程池(生产唯一选择)
ExecutorService executor = Executors.newFixedThreadPool(10);
Future<String> future = executor.submit(callable);
核心知识点:
run()vsstart():直接调用run()只是在当前线程执行,不会创建新线程- 线程只能start()一次,第二次会抛
IllegalThreadStateException - Runnable vs Callable:Runnable不能返回值也不能抛受检异常
1.2 线程中断机制(最难理解的部分)
java
// 错误认知:interrupt()不是强制停止线程
Thread t = new Thread(() -> {
while (true) {
// 需要手动检查中断标志
if (Thread.currentThread().isInterrupted()) {
System.out.println("收到中断信号,清理资源并退出");
break;
}
}
});
t.start();
t.interrupt(); // 设置中断标志为true
// 阻塞方法的中断响应
Thread t2 = new Thread(() -> {
try {
Thread.sleep(10000); // sleep、wait、join都会响应中断
} catch (InterruptedException e) {
// 注意:抛出异常后,中断标志会被清除!
System.out.println("中断标志:" + Thread.currentThread().isInterrupted()); // false
// 正确做法:重新设置中断标志或退出
Thread.currentThread().interrupt(); // 恢复中断标志
return;
}
});
必须掌握的3个方法:
java
// 1. 静态方法,返回当前线程的中断标志,不清除标志
Thread.interrupted() // 注意:会清除中断标志!
// 2. 实例方法,返回中断标志,不清除
thread.isInterrupted()
// 3. 设置中断标志
thread.interrupt()
1.3 join、yield、sleep的精确语义
java
// join:等待线程结束(底层使用wait实现)
Thread t = new Thread(() -> {
Thread.sleep(1000);
});
t.start();
t.join(); // 当前线程阻塞,等待t执行完
t.join(500); // 最多等待500ms
// yield:让出CPU(仅仅是建议,不保证生效)
Thread.yield(); // 从RUNNING→RUNNABLE,让出时间片
// sleep:休眠(不释放锁!)
synchronized (lock) {
Thread.sleep(1000); // 持有锁进入TIMED_WAITING
// 其他线程无法获取lock
}