
文章目录一、引入二、问题问题1 —— new Thread() 但不start()会发生什么1.观察2.解答问题二 —— 主线程直接调用 t.run() 会怎样线程启动了会打印吗是谁打印的1.观察2.解答问题三——sleep中的线程怎么打断标志位为什么不行1.观察2.解答问题四——为什么实际项目几乎不继承Thread?1.观察2. 解答三、揭晓答案一、引入当我们学完了Thread类已经能够熟练地写出newThread(()-{//task})甚至能够顺利地背出创建线程的几种方式但问题是会写线程等于你理解线程吗那么当有人向你丢出这样一段代码时能不能不运行程序仅凭理解就立刻说出输出结果publicclassquestion_Code{publicstaticvoidmain(String[]args)throwsInterruptedException{Runnabletask()-{System.out.println(Thread.currentThread().getName());};Threadt1newThread(task,线程1);Threadt2newThread(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.观察publicclassquestion1{publicstaticvoidmain(String[]args){ThreadtnewThread(()-{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()中的任务不会自动在新线程中执行所以只有调用startJVM才会去向操作系统申请创建执行流并让run中的任务在这个线程中执行。问题二 —— 主线程直接调用 t.run() 会怎样线程启动了会打印吗是谁打印的1.观察publicclassquestion2{publicstaticvoidmain(String[]args){RunnabletasknewRunnable(){Overridepublicvoidrun(){System.out.println(执行对象Thread.currentThread().getName());//Thread.currentThread().getName() 获取当前线程的名字}};ThreadtnewThread(task,线程t);t.run();}}我们可以写这么一段代码来进行观察当代码运行结束发现结果是执行对象main看到这里可能会有点意外明明是 t.run()为什么执行者却是 main并不是说是Thread对象t调用了他就是线程t在执行此时创建的只是 Thread 对象真正执行任务的系统线程还没有被启动。所以当我们再加入一句t.start();这个时候我们就可以观察到执行对象线程t2.解答在分别执行run和start后我们得到了以下的结果执行对象main执行对象线程t看到了吧同一段代码为什么结果会不一样原因就是run() 本质上只是普通方法调用它不会主动创建线程调用它就是普通方法的调用没有多线程的发生谁调用了run就在谁的执行流也就是线程里跑在谁那里跑自然而然谁就是执行者。这就是run和start本质上的不同run一个普通方法谁调用我谁执行start会通过 JVM 调用底层能力创建新的执行线程并最终让该线程去执行 run() 中的任务。所以假设面试官问你start和run的区分是什么我们应该顺理成章地回答“start会创建新线程去执行run而run只是普通方法调用run的代码会在当前线程里执行无并发效果”。问题三——sleep中的线程怎么打断标志位为什么不行1.观察publicclassquestion3{publicstaticvoidmain(String[]args)throwsInterruptedException{ThreadtnewThread(()-{while(!Thread.currentThread().isInterrupted()){System.out.println(线程工作中......);try{Thread.sleep(3000);}catch(InterruptedExceptione){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.解答我们来运行这段新代码publicclassquestion3_1{publicstaticvoidmain(String[]args)throwsInterruptedException{ThreadtnewThread(()-{while(!Thread.currentThread().isInterrupted()){System.out.println(线程工作中......);try{Thread.sleep(3000);}catch(InterruptedExceptione){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 的标志位falsecatch 的标志位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 再次判断时发现 flagfalse于是误以为没有中断发生因此循环继续执行。为什么要这样设计这种设计的核心思想是InterruptedException 已经明确告诉开发者——线程发生了中断。因此Java 不再默认保留标志位而是把决定权交给开发者退出重新 interrupt忽略问题四——为什么实际项目几乎不继承Thread?1.观察我们先来看两种最常见的写法。第一种是继承Threadpublicclassquestion4{// 写法一继承 ThreadstaticclassMyThreadextendsThread{Overridepublicvoidrun(){System.out.println(执行对象Thread.currentThread().getName());}}publicstaticvoidmain(String[]args){newMyThread().start();}}第二种是实现Runnable再把它交给Threadpublicclassquestion4_1{// 写法二实现 RunnablestaticclassMyTaskimplementsRunnable{Overridepublicvoidrun(){System.out.println(执行对象Thread.currentThread().getName());}}publicstaticvoidmain(String[]args){MyTasktasknewMyTask();newThread(task,线程A).start();newThread(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 线程1main 线程2看明白了吧第 1、3 行虽然写着 run()但因为是 main 在调用所以跑出来的名字都是 main只有第 2、4 行用了 start()才真正开出新线程打印出 线程1 和 线程2。最后再补一个有意思的小细节t2 先被 t2.run() 调了一次后面又 t2.start() 了一次这是没问题的因为 run() 只是普通方法压根没启动线程。但要注意——如果你对同一个线程连着调用两次 start()程序就会报错抛 IllegalThreadStateException。