JUC学习笔记(一)
JUC学习笔记(一)
线程运行原理
每个线程运行时,JVM会为其分配一块栈内存,执行下面这段代码时,JVM的内存结构如图所示:
pulic class TestFrames {
public static void main(String[] args) {
method1(10);
}
private static void method1(int x) {
int y = x + 1;
Object m = method2();
System.out.println(m);
}
private static Object method2() {
Object n = new Object();
return n;
}
}

核心区域划分
JVM中每个线程有一个私有的栈,即虚拟机栈。每个虚拟机栈包含了多个栈帧(Stack Frame),栈帧保存了每个线程的局部变量表、返回地址、锁记录以及操作数栈。每当调用一个方法,就会往栈里压入(Push)一个栈帧;方法执行结束,栈帧弹出(Pop)。图中清晰地画出了三个栈帧,对应当前的调用链:main (底部) -> method1 (中间) -> method2 (顶部)。根据代码的执行顺序,main是最先入栈的,而method2是最先出栈的。
在JVM中还有堆 (Heap),所有 new出来的对象都存放在这里(如 new Object())。堆是线程共享的。
此外还有方法区,存储类的信息、常量、静态变量以及编译后的代码指令。
关键细节解读
图中用箭头和色块生动地解释了引用传递和值传递的区别:
栈帧结构:
每个栈帧里包含局部变量表、返回地址、锁记录等。
method1 的栈帧:
x=10:参数传递。y=11:局部变量计算结果。m:这是一个引用。注意看那条弯曲的线,m并没有存对象本身,而是指向了堆中的new Object()。
method2 的栈帧:
n:同样是一个引用,指向堆中刚刚创建的new Object()。
引用传递 (箭头含义):
注意 method2 中的变量 n 和 method1 中的变量 m,它们都有箭头指向堆中同一个紫色的 new Object()。
这说明:当 method2 return n 时,它实际上返回的是对象的内存地址。method1 拿到地址后,赋给变量 m。因此,m 和 n 指向的是同一个对象实例。
PC 程序计数器:
程序计数器它记录当前线程执行到了哪一行字节码指令(对应右侧方法区的代码)。
两阶段终止模式
为什么要“分两阶段”?
Java 中早期的 Thread.stop() 方法已经被废弃(Deprecated),因为它太暴力了,会直接终结线程(类似于直接拔电源),导致资源无法正确释放或数据不一致。
两阶段终止模式将停止过程分为两个步骤:
-
第一阶段(发出信号): 其他线程(如
main线程)向目标线程发出“终止指令”(通常是调用interrupt()方法)。 -
第二阶段(响应信号): 目标线程在运行过程中,检查到了终止指令,于是决定停止运行,但在停止前,它有机会进行资源清理(如释放锁、关闭连接、保存状态),然后安全退出
run()方法。
代码模板
public class Test {
public static void main(String[] args) throws InterruptedException {
TwoPhaseTermination tpt = new TwoPhaseTermination();
tpt.start();
Thread.sleep(3500);
tpt.stop();
}
}
@Slf4j(topic = "c.TwoPhaseTermination")
class TwoPhaseTermination {
private Thread monitor;
//启动监控线程
public void start() {
monitor = new Thread(() -> {
while (true) {
Thread current = Thread.currentThread();
if (current.isInterrupted()) {
log.debug("料理后事");
break;
}
try {
Thread.sleep(1000);
log.debug("执行监控记录");
} catch (InterruptedException e) {
e.printStackTrace();
// 【关键点!】
// 如果线程在 sleep 期间被 interrupt(),会抛出异常并清除打断标记
// 所以必须在这里手动重新设置打断标记,否则下一次循环开头无法检测到
current.interrupt();
}
}
});
monitor.start();
}
//停止监控线程
public void stop() {
monitor.interrupt();
}
}
核心细节总结
interrupt():它不会暂停线程,它只是把线程内部的一个布尔值(打断标记)置为 true。isInterrupted():读取这个标记,不会清除它。InterruptedException:如果线程在sleep、wait、join时被打断,会抛出这个异常,并且JVM 会自动把打断标记清除(置为false)。
这就是为什么在 catch 块中必须写 current.interrupt(),否则线程可能会继续死循环,停不下来。
适用场景
- 后台监控采集系统(如每隔几秒上报一次心跳)。
- 长时间运行的任务,需要支持由用户点击“取消”按钮来停止。
interupted()和isInterupted()方法区别
这两个方法都用于检测线程是否被中断,但它们有 两个核心区别:一个是静态方法 vs 实例方法,另一个是是否清除中断标记。
这是面试中非常容易混淆的考点,记住下面这句话就够了:
isInterrupted()是“只看不动”的检查;interrupted()是“看完就擦”的检查。
核心区别对比表
| 特性 | public boolean isInterrupted() |
public static boolean interrupted() |
|---|---|---|
| 方法类型 | 实例方法 (Instance Method) | 静态方法 (Static Method) |
| 检测对象 | 调用该方法的线程对象 (如 t1.isInterrupted()) |
当前正在执行这行代码的线程 (Thread.currentThread()) |
| 副作用 | 无。不改变中断标记的状态。 | 有。会清除中断标记 (重置为 false)。 |
| 主要用途 | 用于判断某个线程的状态,通常用于监控或逻辑判断。 | 用于当前线程自我检查,并希望检查后重置状态以便后续任务重新处理。 |
代码演示
isInterrupted():只读取,不清除
这是我们在 两阶段终止模式 中最常用的方法。
Thread t1 = new Thread(() -> {
while (true) {
// ... 业务逻辑
}
});
t1.start();
t1.interrupt(); // 打断 t1
// 第一次检查
System.out.println(t1.isInterrupted()); // 输出: true
// 第二次检查(标记依然存在)
System.out.println(t1.isInterrupted()); // 输出: true
结论: 无论调用多少次,只要不手动清除或抛出异常,它一直返回 true。
interrupted():读取并清除
这是一个静态方法,它作用于当前线程。
// 先给自己打个标记
Thread.currentThread().interrupt();
// 第一次检查:返回 true,并且【自动清除标记】
System.out.println(Thread.interrupted()); // 输出: true
// 第二次检查:因为上次已经清除了,所以这里返回 false
System.out.println(Thread.interrupted()); // 输出: false
结论: 它是“一次性”的。如果你连续调用两次,第二次通常是 false。
一个巨大的坑(面试常考)
请看下面的代码,你觉得会输出什么?
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
// t1 啥也不干
});
t1.start();
t1.interrupt(); // 把 t1 设置为中断状态
// 【陷阱在这里】
// 我们在 main 线程里,用 t1 对象调用了静态方法 interrupted()
System.out.println(t1.interrupted());
}
答案是:false。
为什么?
因为 interrupted() 是 静态方法 (Static)!
虽然代码写的是 t1.interrupted(),但在 Java 编译器眼中,这等同于 Thread.interrupted()。
它检查的不是 t1 的状态,而是当前运行这行代码的线程(即 main 线程)的状态。main 线程从来没有被中断过,所以返回 false。
总结与应用场景
-
什么时候用
isInterrupted()?
绝大多数情况。特别是当你写while (!Thread.currentThread().isInterrupted())这种循环终止条件时。你想保留中断状态,以便后续的代码也能知道“哦,这个线程被请求停止了”。 -
什么时候用
interrupted()?
当你处理完中断逻辑后,想要重置状态,以便让线程继续执行其他非中断逻辑时。或者在复杂的框架底层,你需要“消费”掉这个中断信号,防止它影响后续的任务执行。
更多推荐



所有评论(0)