我以为我会线程了,直到Claude问了我四个问题......
文章目录
一、引入
当我们学完了Thread类,已经能够熟练地写出:
new Thread(() -> {
//task
})
,
甚至能够顺利地背出创建线程的几种方式,但问题是:
会写线程,等于你理解线程吗?
那么,当有人向你丢出这样一段代码时,能不能不运行程序,仅凭理解就立刻说出输出结果?
public class question_Code {
public static void main(String[] args) throws InterruptedException {
Runnable task = () ->{
System.out.println(Thread.currentThread().getName());
};
Thread t1 = new Thread(task,"线程1");
Thread t2 = new Thread(task,"线程2");
task.run(); // 1.输出?
Thread.sleep(1000);
t1.start(); // 2. 输出?
Thread.sleep(1000);
t2.run(); // 3.输出?
Thread.sleep(1000);
t2.start(); // 4.输出?
Thread.sleep(1000);
}
}
这些代码看着像是执行任务,但他们真的创建线程了吗?这个问题,我们留到结尾再来公布答案。
二、问题
问题1 —— new Thread() 但不start(),会发生什么?
1.观察
public class question1 {
public static void main(String[] args) {
Thread t = new Thread(()->{
System.out.println("线程启动了!!");
});
System.out.println("main线程结束!!");
}
}
我们可以写这样的一段代码,然后不启动start(),而仅仅是new了一个Thread对象,接着我们运行,就会出现以下的结果:
main线程结束!!
奇了个怪了,为什么我明明把t1实例化了,那为什么并没有打印出我想要的结果?难道说,线程对象的创建,不代表着线程创建并执行
2.解答
既然我们通过new创建了Thread对象,但却没有创建线程并执行,那么就是说明了,new 真的只是创建了Thread对象,而真正向操作系统申请创建线程并执行的另有其人,那就是:
start();
Thread对象本质上保存了线程需要执行的任务(也就是run中的代码),但这些任务什么时候执行,要等start()触发线程创建和调度。
我们需要区分两个概念,什么是Thread对象?什么是操作系统线程?
在Java中,Thread 本质上仍然是一个 Java 对象,存在于堆内存中;
而操作系统线程,是能被CPU调度执行的一个“执行流”,只有当线程被创建出来时,才能够执行;
所以,当我们 new Thread() ,做的是创建了一个Thread对象(出现在堆内存中),但是没有创建我们真正需要的线程,只用我们调用start()方法,让JVM去调用操作系统的API(创建线程的API),才能在内核中真正地创建出来一个执行流,也就是线程,同时并把我们写的run()塞到线程中执行。
因此,这道题的标准回答应该这样:
不会打印"线程1启动了!!",
原因是:new Thread()只是创建了Thread对象,并没有创建真正的线程,而run()需要一个线程去执行它,当线程没有被创建出来时,run()中的任务不会自动在新线程中执行,所以只有调用start(),JVM才会去向操作系统申请创建执行流,并让run()中的任务在这个线程中执行。
问题二 —— 主线程直接调用 t.run() 会怎样?"线程启动了"会打印吗?是谁打印的?
1.观察
public class question2 {
public static void main(String[] args) {
Runnable task = new Runnable() {
@Override
public void run() {
System.out.println("执行对象:" + Thread.currentThread().getName());
//Thread.currentThread().getName() 获取当前线程的名字
}
};
Thread t = new Thread(task,"线程t");
t.run();
}
}
我们可以写这么一段代码来进行观察,当代码运行结束发现,结果是:
执行对象:main
看到这里可能会有点意外:
明明是 t.run(),为什么执行者却是 main?
并不是说,是Thread对象t调用了他,就是线程t在执行,此时创建的只是 Thread 对象,真正执行任务的系统线程还没有被启动。,所以当我们再加入一句:
t.start();
这个时候我们就可以观察到
执行对象:线程t
2.解答
在分别执行run()和start()后,我们得到了以下的结果:
执行对象:main
执行对象:线程t
看到了吧,同一段代码,为什么结果会不一样?原因就是run() 本质上只是普通方法调用,它不会主动创建线程,调用它就是普通方法的调用,没有多线程的发生,谁调用了run(),就在谁的执行流也就是线程里跑,在谁那里跑,自然而然谁就是执行者。
这就是run()和start()本质上的不同:
run():一个普通方法,谁调用我谁执行;
start():会通过 JVM 调用底层能力,创建新的执行线程,并最终让该线程去执行 run() 中的任务。
所以,假设面试官问你:start和run的区分是什么?
我们应该顺理成章地回答:“start会创建新线程去执行run,而run只是普通方法,调用run()的代码会在当前线程里执行,无并发效果”。
问题三——sleep中的线程怎么打断?标志位为什么不行?
1.观察
public class question3 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
System.out.println("线程工作中......");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
System.out.println("线程被打断......");
System.out.println("catch 的标志位:" + Thread.currentThread().isInterrupted());
}
}
System.out.println("线程结束......");
});
t.start();
Thread.sleep(1000);
System.out.println("main准备打断线程......");
t.interrupt();
}
}
我们可以写这么一段代码来观察,当我们运行后,可以观察到:
线程工作中…
线程工作中…
线程工作中…
线程工作中…
main准备打断线程…
线程被打断…
catch 的标志位:false
线程工作中…
线程工作中…
线程工作中…
…
我去,等等?!我不是已经 interrupt 了吗?
为什么标志位是 false?
为什么线程还没停?
interrupt难道不是强制终止线程吗?如果是强制终止,那为什么线程没有停下来?
这就说明了interrupt不是stop;
我调用了interrupt,不是已经把flag = true 了吗?那为什么打印出来的结果却是:false?
那标志位去哪里了?这是我的 true 标志位吗?
2.解答
我们来运行这段新代码:
public class question3_1 {
public static void main(String[] args)
throws InterruptedException {
Thread t = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
System.out.println("线程工作中......");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
System.out.println("线程被打断......");
System.out.println("catch 的标志位:" + Thread.currentThread().isInterrupted());
// 重新设置中断标志
Thread.currentThread().interrupt();
System.out.println("catch 的标志位:" + Thread.currentThread().isInterrupted());
}
}
System.out.println("线程结束......");
});
t.start();
Thread.sleep(1000);
System.out.println("main准备打断线程......");
t.interrupt();
}
}
结果如下:
线程工作中…
main准备打断线程…
线程被打断…
catch 的标志位:false
catch 的标志位:true
线程结束…
这个时候我们惊奇地发现,这次线程被顺利打断了,没有出现上述代码的问题,同时可以观察到两段代码只有一句不同,那就是:
Thread.currentThread().interrupt();
为什么会这样呢?
那是因为,**interrupt()**并不是强制杀死线程,它更像是在发出一个请求——询问线程能不能停下来?
而且**sleep()**有个特殊机制,它并不是只会睡觉。当线程调用sleep()进入睡眠时(sleep() 属于可中断阻塞方法) ,当线程正在 sleep() 时,如果收到了 interrupt() 请求,它会立即结束睡眠,同时抛出 InterruptedException 异常,再顺手把中断标志位给清理了,是的,你没有看错,它顺手给我们设置好的标志位给清理了…
相当于 sleep()在说:“okok,我收到信(interrupt)了”,然后它看完后就把信撕了。
这并不是 sleep() “搞破坏”,而是 Java 对中断机制的设计:
当 sleep() 通过异常告诉你“我被中断了”时,这次中断信号就已经被消费掉了,因此标志位会被清除。
因此,我们也就明白了
interrupt() 不负责停止线程,它只能通知线程停下来;
真正决定要不要停下来,还是取决于线程自己的处理逻辑
所以完整的流程是:
interrupt()-> flag = true -> sleep发现中断 -> 抛出InterruptedException -> sleep自动清除标志位 -> catch里的flag = false -> 再次 interrupt() -> flag = true -> while退出
不过可能有人疑惑:
Thread.currentThread().interrupt();
所以这句话有什么用?就是在sleep()把信撕掉后,也就是sleep() 清除了标志位后,while 再次判断时发现 flag=false,于是误以为没有中断发生,因此循环继续执行。
为什么要这样设计?这种设计的核心思想是:
InterruptedException 已经明确告诉开发者——线程发生了中断。
因此,Java 不再默认保留标志位,而是把决定权交给开发者:
- 退出
- 重新 interrupt
- 忽略
问题四——为什么实际项目几乎不继承Thread?
1.观察
我们先来看两种最常见的写法。第一种,是继承 Thread:
public class question4 {
// 写法一:继承 Thread
static class MyThread extends Thread {
@Override
public void run() {
System.out.println("执行对象:" + Thread.currentThread().getName());
}
}
public static void main(String[] args) {
new MyThread().start();
}
}
第二种,是实现 Runnable,再把它交给 Thread:
public class question4_1 {
// 写法二:实现 Runnable
static class MyTask implements Runnable {
@Override
public void run() {
System.out.println("执行对象:" + Thread.currentThread().getName());
}
}
public static void main(String[] args) {
MyTask task = new MyTask();
new Thread(task, "线程A").start();
new Thread(task, "线程B").start();
}
}
两种都能跑出结果,看起来"都行"。而且继承 Thread 写起来好像还更短一点。那奇怪了——为什么老师、教程、还有网上的代码,几乎都在用第二种(实现 Runnable),很少有人用第一种呢?
2. 解答
其实原因不复杂,说白了就一句话:"要做的事"和"谁去做",最好分开来写。 我们分三点来看。
第一点:Java 里一个类只能继承一个爸爸。
Java 规定,一个类只能 extends 一个父类。你要是用了 extends Thread,就等于把这个"唯一的名额"用掉了,以后这个类想继承别的东西就没办法了。
但接口不一样,一个类可以实现好多个接口。所以你用 Runnable,这个类还能腾出手去继承它本来该继承的东西,更自由。
第二点:继承 Thread 会把"任务"和"线程"捆得死死的。
还记得我们前面反复说的那句话吗——Thread 对象是"跑腿的人",run() 里的代码才是"要办的事"。 这本来是两件事。
用 Runnable 的时候,"要办的事"是一个单独的对象,可以被好几个线程拿去用。你看写法二里,同一个 task 被"线程A"和"线程B"同时拿去执行了。但你要是继承 Thread,任务就被焊死在这个线程里了,一个任务只能配一个线程,想复用都难。
第三点,也是最实在的一点:以后写项目,根本不会自己一个个 new 线程。
真正干活的时候,线程是很"贵"的东西,不停地 new 一个又扔掉,很浪费。所以大家都用一个叫线程池的东西来统一管理线程(现在先记住这个名字就行,后面会专门讲)
关键在于:这个线程池只认 Runnable,你继承出来的 Thread 子类它根本不收。所以你一旦选了 extends Thread,就等于把自己挡在了线程池的门外
所以要是我们被问到:
继承Thread类和实现Runnable有什么区别
就应该这样回答:
因为Java只能继承一个
类,用Runnable能够省下这个名额留给更需要的,而且他把任务和线程分开了,一个任务能给多个线程用,再加上以后常用的线程池只收Runnable
,不收Thread子类,所以实现Runnable更方便,更实用。
三、揭晓答案
现在回到开头那段代码。学到这里,其实你已经能不运行程序,直接"读"出结果了。核心就抓住一句话:run() 是普通方法,谁调用它,就在谁的线程里跑;只有 start() 才会真正开一个新线程。
task.run(); // 1. main 直接调方法 → 在 main 线程里跑
Thread.sleep(1000);
t1.start(); // 2. 真正开了新线程 → 在 "线程1" 里跑
Thread.sleep(1000);
t2.run(); // 3. 又是普通调用 → 还是在 main 里跑
Thread.sleep(1000);
t2.start(); // 4. 又开了新线程 → 在 "线程2" 里跑
Thread.sleep(1000);
所以最后打印出来是:
main 线程1
main 线程2
看明白了吧?
第 1、3 行虽然写着 run(),但因为是 main 在调用,所以跑出来的名字都是 main;只有第 2、4 行用了 start(),才真正开出新线程,打印出 线程1 和 线程2。
最后再补一个有意思的小细节:t2 先被 t2.run() 调了一次,后面又 t2.start() 了一次,这是没问题的,因为 run() 只是普通方法,压根没启动线程。但要注意——如果你对同一个线程连着调用两次 start(),程序就会报错(抛 IllegalThreadStateException)。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)