线程运行原理

每个线程运行时,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 中的变量 nmethod1 中的变量 m,它们都有箭头指向堆中同一个紫色的 new Object()

这说明:当 method2 return n 时,它实际上返回的是对象的内存地址。method1 拿到地址后,赋给变量 m。因此,mn 指向的是同一个对象实例。

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:如果线程在 sleepwaitjoin 时被打断,会抛出这个异常,并且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()
    当你处理完中断逻辑后,想要重置状态,以便让线程继续执行其他非中断逻辑时。或者在复杂的框架底层,你需要“消费”掉这个中断信号,防止它影响后续的任务执行。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐