- 教程
- 技术博客
- 文档
【免费下载链接】YCBlogs
技术博客笔记大汇总,包括Java基础,线程,并发,数据结构;Android技术博客等等;常用设计模式;常见的算法;网络协议知识点;部分flutter笔记;还包括平时开发中遇到的bug汇总,当然也在工作之余收集了大量的面试题,长期更新维护并且修正,持续完善……开源的文件是markdown格式的!转载请注明出处,谢谢!
本篇是 YCBlogs 技术博客笔记中「06.线程知识」系列的核心篇目之一,聚焦Thread.start()的完整启动链路:从"为何不能重复 start"的源码校验,到 start 与 run 的本质区别,再到 JVM 与操作系统层面的线程实现,以及线程生命周期六状态的详细流转。读完本文,你将能够从源码角度解释IllegalThreadStateException的产生原理,理清 start/run/join/yield/sleep/interrupt 等 API 对线程状态的影响,并掌握线程创建三种方式、守护线程、中断协作与异常捕获等实战能力。
01 线程启动的入口:start() 与 run() 的本质区别
1.1 一个最经典的疑问
为什么我们调用start()方法时会执行run()方法,为什么我们不能直接调用run()方法?这是非常经典的 Java 多线程面试问题。
当你调用start()方法时,你将创建新的线程,并且执行run()方法里的代码;但如果你直接调用run()方法,它不会创建新的线程,也不会开启新的执行流,只是把该方法当作普通方法在当前线程中执行一遍。
看下面的示例,启动线程必须使用start()方法,它会"告诉 JVM 开启一个线程去调用 run 方法":
public class ThreadDemo { public static void main(String[] args) { // 创建对象 MyThread t1 = new MyThread(); // 启动线程: 需要使用start方法启动线程, // 如果我们在这里调用的是run方法, 那么我们只是把该方法作为普通方法进行执行 // t1.run(); t1.start(); // 告诉jvm开启一个线程调用run方法 // t1.start(); // 一个线程只能被启动一次 } } public class MyThread extends Thread { @Override public void run() { System.out.println("小杨逗比"); } }如果把注释掉的t1.run()放开执行,你会看到它只是像普通方法一样在 main 线程内顺序执行,并没有产生新线程。这正是start()与run()最大的语义差异:run() 是线程的执行体,start() 才是启动线程的开关。
1.2 start() 与 run() 的调用关系
从调用链上看,start()内部最终会通过 JNI 调用到本地代码去创建并启动一个真正的操作系统线程,而新线程启动后的第一件事就是回调该线程对象的run()方法。也就是说:
start()→ 创建新线程 → 新线程调度执行 → 回调run();- 直接调
run()→ 只在当前线程中同步执行方法体,无新线程产生。
02 为何不能重复 start:源码级校验
2.1 问题现象
可以发现即使多次调用start()方法,线程只会被执行一次。那么这个究竟是怎么做到的呢?答案就是start()源码中的状态校验:多次调用 start 将会抛出java.lang.IllegalThreadStateException(线程状态非法异常),这是一种运行时异常,多次调用 start 被认为是编程错误。
2.2 start() 源码逐行解析
下面是 Android 视角下Thread.start()的源码(与 OpenJDK 逻辑一致,仅注释差异):
boolean started = false; public synchronized void start() { // Android-changed: throw if 'started' is true if (threadStatus != 0 || started) throw new IllegalThreadStateException(); group.add(this); started = false; try { nativeCreate(this, stackSize, daemon); started = true; } finally { try { if (!started) { group.threadStartFailed(this); } } catch (Throwable ignore) { /* do nothing. If start0 threw a Throwable then it will be passed up the call stack */ } } }这段代码大致意思是:
- 状态校验:首先校验线程状态
threadStatus != 0 || started,如果不是新建状态或者已经创建过(started 已被标记为 true),则直接抛出IllegalThreadStateException; - 加入线程组:通过
group.add(this)将当前线程添加进线程组(ThreadGroup),线程组是 JVM 统一管理一批线程状态的数据结构; - 调用 native 创建线程:
nativeCreate(this, stackSize, daemon)是本地方法,真正负责在 JVM/操作系统层面创建线程,并记录开启状态started = true; - 失败回滚:如果线程启动失败(
started仍为 false),则在 finally 中调用group.threadStartFailed(this)把当前线程从线程组中移除。
从源码结构可以看出,start()被synchronized修饰,保证同一时刻只有一个线程能执行启动逻辑,started标志与threadStatus共同构成了"只能启动一次"的双重防线。
2.3 为什么线程对象是一次性消耗品
结合源码可推断:线程对象属于一次性消耗品。一般线程执行完run()方法后线程就正常结束、随之报废,不能再次start(),只能新建一个线程对象。第二次调用start()时线程可能处于终止(TERMINATED)或其他非 NEW 状态,但不论如何,都是不可以再次启动的。
03 start 背后发生了什么:从 JVM 到操作系统
3.1 线程是什么
从操作系统的角度,可以简单认为:线程是系统调度的最小单元。一个进程可以包含多个线程,作为任务的真正运作者,线程有自己的栈(Stack)、寄存器(Register)、本地存储(Thread Local)等,但是会和进程内其他线程共享文件描述符、虚拟地址空间等。更完整的概念阐述可参考 01.线程进程基础介绍.md。
3.2 Java 线程的底层实现
在具体实现中,线程还分为内核线程、用户线程,Java 的线程实现其实是与虚拟机相关的。对于 Sun/Oracle JDK,其线程实现经历了一个演进过程:基本上在 Java 1.2 之后,JDK 已经抛弃了所谓的 Green Thread(用户调度的线程),现在的模型是一对一映射到操作系统内核线程。
如果来看Thread的源码,你会发现其基本操作逻辑大都是以 JNI 形式调用的本地代码:
private native void start0(); private native void setPriority0(int newPriority); private native void interrupt0();也就是说,我们调用start()时,Java 层只做状态校验与线程组登记,真正"创建线程"的动作全部交给 native 层完成。这种实现有利有弊:总体上,Java 语言得益于精细粒度的线程和相关的并发操作,其构建高扩展性的大型应用的能力毋庸置疑;但其复杂性也提高了并发编程的门槛,近几年 Go 语言等提供了协程(coroutine),大大提高了构建并发应用的效率;与此同时,Java 也在 Loom 项目中孕育新的类似轻量级用户线程(Fiber)等机制。
3.3 线程的两种调度模型
线程启动后能否立刻运行,取决于调度模型。Java 使用的是抢占式调度模型:
- 分时调度模型:所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间片;
- 抢占式调度模型:优先让优先级高的线程使用 CPU,如果线程的优先级相同,则随机选择一个,优先级高的线程获取的 CPU 时间片相对多一些。
线程优先级通过整型成员变量Priority控制,范围从 1~10,默认优先级为 5,可通过setPriority(int)设置。相关细节可参考 10.线程优先级和调度.md。
04 线程生命周期:六种状态与状态转移
4.1 六种线程状态
在 Java 5 以后,线程状态被明确定义在其公共内部枚举类型java.lang.Thread.State中,分别是:
| 状态 | 说明 |
|---|---|
| 新建(NEW) | 线程被创建出来还没真正启动的状态,可以认为它是个 Java 内部状态 |
| 就绪(RUNNABLE) | 线程已经在 JVM 中执行,由于执行需要计算资源,它可能正在运行,也可能还在等待系统分配 CPU 片段,在就绪队列里面排队。其他一些分析中会额外区分一种 RUNNING 状态,但从 Java API 的角度并不能表示出来 |
| 阻塞(BLOCKED) | 线程在等待 Monitor lock。比如线程试图通过 synchronized 去获取某个锁,但其他线程已经独占,当前线程就会处于阻塞状态 |
| 等待(WAITING) | 正在等待其他线程采取某些操作。常见场景是生产者消费者模式,条件未满足时消费者线程 wait,生产者线程准备好数据后通过 notify 等动作通知;Thread.join()也会令线程进入等待状态 |
| 计时等待(TIMED_WAIT) | 进入条件与等待状态类似,但调用的是存在超时条件的方法,比如 wait 或 join 方法的指定超时版本 |
| 终止(TERMINATED) | 不管是意外退出还是正常执行结束,线程已经完成使命,终止运行,也有人把这个状态叫作死亡 |
其中计时等待对应的典型 API 原型如下:
public final native void wait(long timeout) throws InterruptedException;上文的图片即展示了线程从新建状态通过start()方法进入就绪/运行状态,再到阻塞、死亡的整体流转关系:运行状态进入阻塞状态的触发原因包括进入synchronized块未拿到锁、调用sleep()/join()/wait()等方法;死亡状态的成因包含正常终止(执行完所有工作)、强制终止(调用 stop/destroy)、异常终止(执行过程中发生异常)。
4.2 影响线程状态的因素
从线程生命周期的状态开始展开,在 Java 编程中,有哪些因素可能影响线程的状态呢?主要有三类:
- 线程自身的方法:除了
start,还有多个join方法,等待线程结束;yield是告诉调度器主动让出 CPU;另外就是一些已经被标记为过时的resume、stop、suspend之类方法,在 JDK 最新版本中,destroy/stop方法将被直接移除; - 基类 Object 提供的方法:
wait/notify/notifyAll。如果我们持有某个对象的 Monitor 锁,调用wait会让当前线程处于等待状态,直到其他线程notify或notifyAll,本质上是提供了 Monitor 的获取和释放能力,是基本的线程间通信方式; - 并发类库中的工具:比如
CountDownLatch.await()会让当前线程进入等待状态,直到 latch 计数为 0,这可以看作是线程间通信的 Signal。
Thread 和 Object 的方法听起来简单,但实际应用中被证明非常晦涩、易错,这也是为什么 Java 后来又引入了并发包。有了并发包,大多数情况下我们已经不再需要去调用 wait/notify 之类的方法了。
05 面试考点:两次调用 start 与状态转移
5.1 典型问题与回答
问:一个线程两次调用
start()方法会出现什么情况?谈谈线程的生命周期和状态转移。
典型回答:Java 的线程是不允许启动两次的,第二次调用必然会抛出IllegalThreadStateException,这是一种运行时异常,多次调用 start 被认为是编程错误。在第二次调用start()方法的时候,线程可能处于终止或者其他(非 NEW)状态,但不论如何,都是不可以再次启动的。
5.2 考点问题分析
这个题目算是个常见的面试热身题目。面试官可能会以此为契机,从各种不同角度考察你对线程的掌握:
- 相对理论一些的面试官可能会问你线程到底是什么以及 Java 底层实现方式;
- 线程状态的切换,以及和锁等并发工具类的互动;
- 线程编程时容易踩的坑与建议等。
可以看出,仅仅是一个线程,就有非常多的内容需要掌握。完整的生命周期讲解可参考 02.线程生命周期说明.md。
06 创建线程的三种方式与 start 的配合
6.1 三种创建方式
如何在用户程序中新建一个线程,主要有三种方式:
- 通过继承 Thread 类,重写 run 方法;
- 通过实现 Runnable 接口;
- 通过实现 Callable 接口。
方式一:继承 Thread
定义 Thread 类的子类并重写 run 方法(run 方法的方法体就代表线程要完成的任务,因此 run() 方法也被称为执行体),然后创建子类实例并调用start()启动:
public class ThreadDemo { public static void main(String[] args) { MyThread t1 = new MyThread(); MyThread t2 = new MyThread(); t1.start(); t2.start(); } } public class MyThread extends Thread { @Override public void run() { for (int x = 0; x < 1000; x++) { System.out.println(x); } } }方式二:实现 Runnable
定义 Runnable 接口的实现类并重写 run() 方法,创建该实现类实例作为 Thread 的 target 来创建 Thread 对象(该 Thread 对象才是真正的线程对象),最后调用start()启动:
public static void main(String[] args) { MyThread mt = new MyThread(); Thread t1 = new Thread(mt, "张三"); Thread t2 = new Thread(mt, "李四"); t1.start(); t2.start(); } public class MyThread implements Runnable { @Override public void run() { for (int x = 0; x < 1000; x++) { System.out.println(Thread.currentThread().getName() + "---" + x); } } }方式三:Callable + FutureTask
创建 Callable 接口的实现类并实现 call() 方法(call 方法作为线程执行体且有返回值),使用 FutureTask 类包装 Callable 对象,以 FutureTask 作为 Thread 的 target 创建并启动新线程,最后调用get()获得子线程执行结束后的返回值(get() 会阻塞线程):
public class CallableThread implements Callable<Integer> { @Override public Integer call() throws Exception { int i = 0; for (; i < 100; i++) { System.out.println(Thread.currentThread().getName() + " " + i); } return i; } } public class ThreadDemo { public static void main(String[] args) { CallableThread ctt = new CallableThread(); FutureTask<Integer> ft = new FutureTask<>(ctt); for (int i = 0; i < 100; i++) { System.out.println(Thread.currentThread().getName() + " 的循环变量i的值" + i); if (i == 20) { new Thread(ft, "有返回值的线程").start(); } } try { System.out.println("子线程的返回值:" + ft.get()); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } } }Callable 与 Runnable 的核心区别:Callable 的call()方法可以返回值和抛出异常,而 Runnable 的run()方法没有这些功能;运行 Callable 任务可以拿到 Future 对象表示异步计算的结果,通过 Future 对象可以了解任务执行情况、取消任务执行、获取执行结果。
6.2 三种方式对比
采用实现 Runnable、Callable 接口的方式创建多线程时:
- 优势:线程类只是实现了 Runnable 或 Callable 接口,还可以继承其他类;多个线程可以共享同一个 target 对象,非常适合多个相同线程处理同一份资源的情况,可以将 CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想;
- 劣势:编程稍微复杂,如果要访问当前线程,必须使用
Thread.currentThread()方法。
使用继承 Thread 类的方式创建多线程时:
- 优势:编写简单,如果需要访问当前线程,直接使用
this即可获得当前线程,无需使用Thread.currentThread(); - 劣势:线程类已经继承了 Thread 类,所以不能再继承其他父类。
6.3 Runnable 与线程池的配合
实现 Runnable 的好处是,不会受 Java 不支持类多继承的限制,重用代码实现,当需要重复执行相应逻辑时优点明显,而且也能更好地与现代 Java 并发库中的 Executor 框架结合使用。比如将"启动线程、等待结束"的逻辑写成:
Runnable task = () -> {System.out.println("Hello World!");}; Thread myThread = new Thread(task); myThread.start(); myThread.join();也可以完全交给线程池:
Future future = Executors.newFixedThreadPool(1) .submit(task) .get();这样我们就不用操心线程的创建和管理,也能利用 Future 等机制更好地处理执行结果。注意:线程生命周期通常和业务之间没有本质联系,混淆实现需求和业务需求,就会降低开发效率。
07 启动之外的线程 API 使用要点
7.1 守护线程(Daemon Thread)
有的时候应用中需要一个长期驻留的服务程序,但是不希望其影响应用退出,就可以将其设置为守护线程。如果 JVM 发现只有守护线程存在时,将结束进程。注意:必须在线程启动之前设置。
Thread daemonThread = new Thread(); daemonThread.setDaemon(true); daemonThread.start();守护线程是一种特殊的线程,就像系统的守护者,在后台默默守护一些系统服务,比如垃圾回收线程、JIT 线程就可以理解为守护线程;与之对应的用户线程可以认为是系统的工作线程,会完成整个系统的业务操作。当一个 Java 应用只有守护线程的时候,虚拟机就会自然退出。
这里有两个关键注意事项:
setDaemon(true)必须在线程启动前调用,否则会抛出IllegalThreadStateException;- 守护线程在退出的时候并不会执行 finally 块中的代码,所以将释放资源等操作不要放在 finally 块中执行,这种操作是不安全的。
一个典型示例(main 线程结束后,守护线程即退出,不会出现死循环):
public class DaemonDemo { public static void main(String[] args) { Thread daemonThread = new Thread(new Runnable() { @Override public void run() { while (true) { try { System.out.println("i am alive"); Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } finally { System.out.println("finally block"); } } } }); daemonThread.setDaemon(true); daemonThread.start(); try { Thread.sleep(800); } catch (InterruptedException e) { e.printStackTrace(); } } }输出结果中 main 线程结束后 daemonThread 退出,此时只打印了 "i am alive" 并没有打印 finally 块中的内容,印证了上述注意事项。
7.2 Spurious wakeup(虚假唤醒)
尤其是在多核 CPU 的系统中,线程等待存在一种可能:在没有任何线程广播或发出信号的情况下线程就被唤醒。如果处理不当就可能出现诡异的并发问题,所以等待条件过程中建议采用循环判断模式:
// 推荐 while (isCondition()) { waitForAConfition(...); } // 不推荐,可能引入bug if (isCondition()) { waitForAConfition(...); }7.3 Thread.onSpinWait()
Thread.onSpinWait()是 Java 9 中引入的特性。"自旋锁"(spin-wait, busy-waiting)也可以认为其不算是一种锁,而是一种针对短期等待的性能优化技术。onSpinWait()没有任何行为上的保证,而是对 JVM 的一个暗示,JVM 可能会利用 CPU 的 pause 指令进一步提高性能,性能特别敏感的应用可以关注。
7.4 慎用 ThreadLocal
ThreadLocal 是 Java 提供的一种保存线程私有信息的机制,因为其在整个线程生命周期内有效,所以可以方便地在一个线程关联的不同业务模块之间传递信息,比如事务 ID、Cookie 等上下文相关信息。它的实现结构可以参考源码:数据存储于线程相关的 ThreadLocalMap,其内部条目是弱引用:
static class ThreadLocalMap { static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } // … }当 Key 为 null 时,该条目就变成"废弃条目",相关 value 的回收往往依赖于几个关键点,即 set、remove、rehash。下面是 set 的示例(精简注释版):
private void set(ThreadLocal<?> key, Object value) { Entry[] tab = table; int len = tab.length; int i = key.threadLocalHashCode & (len - 1); for (Entry e = tab[i];; …) { //… if (k == null) { // 替换废弃条目 replaceStaleEntry(key, value, i); return; } } tab[i] = new Entry(key, value); int sz = ++size; // 扫描并清理发现的废弃条目,并检查容量是否超限 if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); // 清理废弃条目,如果仍然超限,则扩容(加倍) }具体的清理逻辑实现在cleanSomeSlots和expungeStaleEntry之中。
08 与 start 密切相关的线程协作 API
8.1 join:等待线程结束
join()方法:public final void join(),等待该线程执行完毕以后其他线程才能再次执行。Thread 的非静态方法 join() 让一个线程 B"加入"到另外一个线程 A 的尾部,在 A 执行完毕之前,B 不能工作。它保证当前线程停止执行,直到该线程所加入的线程完成为止;如果它加入的线程没有存活,则当前线程不需要停止。
t1.start(); t1.join(); t2.start();join 的实现原理是:通过调用线程的 wait 方法来达到同步的目的。例如 A 线程中调用了 B 线程的 join 方法,则相当于在 A 线程中调用了 B 线程的 wait 方法,当 B 线程执行完(或者到达等待时间),B 线程会自动调用自身的 notifyAll 方法唤醒 A 线程,从而达到同步的目的。
join 源码如下:
public final synchronized void join(long millis) throws InterruptedException { long base = System.currentTimeMillis(); long now = 0; if (millis < 0) { throw new IllegalArgumentException("timeout value is negative"); } if (millis == 0) { while (isAlive()) { wait(0); } } else { while (isAlive()) { long delay = millis - now; if (delay <= 0) { break; } wait(delay); now = System.currentTimeMillis() - base; } } }由源码可以看到三个分支:参数为 0 时调用isAlive()一直检测线程是否存活(执行完毕),存活就调用 wait 阻塞;参数为负数直接报错 "timeout value is negative";参数大于 0 时 while 中不断判断存活与剩余等待时间,等待时间小于等于 0 就跳出循环,否则继续 wait。join 方法必须在线程 start 方法调用之后调用才有意义——如果一个线程都没有 start,那它也就无法同步了,因为执行完 start 方法才会创建线程。
经典面试题:现在有 T1、T2、T3 三个线程,怎样保证 T2 在 T1 执行完后执行,T3 在 T2 执行完后执行?答案就是连续使用 start + join:
t1.start(); try { t1.join(); } catch (InterruptedException e) { e.printStackTrace(); } t2.start(); try { t2.join(); } catch (InterruptedException e) { e.printStackTrace(); } t3.start(); try { t3.join(); } catch (InterruptedException e) { e.printStackTrace(); }注意:如果先连续启动三个线程再连续 join,并不能保证按顺序执行,因为三个线程已经被同时调度运行了。除 join 外,也可以使用 ReentrantLock + Condition 或标志位轮询的方式实现顺序执行,详见 06.线程中join原理深入.md。
8.2 yield:线程礼让
yield()方法:public static void yield(),暂停当前正在执行的线程对象,并执行其他线程。这是一个静态方法,一旦执行,它会使当前线程让出 CPU,但是让出的 CPU 并不代表当前线程不再运行了——如果在下一次竞争中又获得了 CPU 时间片,当前线程依然会继续运行。另外,让出的时间片只会分配给与当前线程相同优先级的线程。
线程礼让的原理:暂停当前的线程,让 CPU 去执行其他的线程,这个暂停的时间是相当短暂的;当某一个线程暂停完毕以后,如果其他线程还没有抢占到 CPU 的执行权,那么当前线程会和其他线程再次抢占 CPU 的执行权。
yield 与 sleep 的区别:yield()方法和sleep()方法类似,也不会释放"锁标志",区别在于它没有参数,即 yield() 方法只是使当前线程重新回到可执行状态,所以执行 yield() 的线程有可能在进入可执行状态后马上又被执行;另外 yield() 方法只能使同优先级或者高优先级的线程得到执行机会,这也和 sleep() 不同——sleep() 交出来的时间片其他线程都可以去竞争。详见 07.线程中yield原理分析.md。
8.3 sleep:线程休眠
sleep(long millis)在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。它让其他线程有机会继续执行,但并不释放对象锁——如果有 synchronized 同步块,其他线程仍然不能访问共享数据。注意该方法要捕获异常(throws InterruptedException)。
sleep 源码中有几个要点:
- millis 或 nanos 为负数会抛出
IllegalArgumentException; - 当 millis 和 nanos 都为 0 时,如果线程被标记了中断(
Thread.interrupted()返回 true),则会抛出InterruptedException——这就是为什么 sleep 需要捕获异常; - 参数非 0 时会基于
currentThread().lock加锁,通过循环等待直到休眠时长真正流逝(应对 wait 提前返回的情况)。
Thread.sleep(0)的作用是"触发操作系统立刻重新进行一次 CPU 竞争",竞争结果也许是当前线程仍然获得 CPU 控制权,也许会换成别的线程获得 CPU 控制权,这也是在大循环里经常写一句Thread.sleep(0)的原因——给其他线程(比如 UI 绘制线程)获得 CPU 控制权的机会,避免界面假死。
wait 与 sleep 的核心区别:
- 最大的不同是在等待时wait 会释放锁,而 sleep 一直持有锁;wait 通常被用于线程间交互,sleep 通常被用于暂停执行;
- sleep() 是 Thread 的静态方法,而 wait 是 Object 实例方法;
- sleep 睡眠后不出让系统资源,wait 让出系统资源其他线程可以占用 CPU;
- sleep(milliseconds) 需要指定一个睡眠时间,时间一到会自动唤醒;wait() 必须等待
Object.notify/notifyAll通知后才会离开等待池; - wait() 方法必须要在同步方法或者同步块中调用(必须已经获得对象锁),sleep() 没有这个限制。
更完整的 sleep 源码与 wait/sleep 对比分析见 05.线程中sleep原理深入.md。
8.4 interrupt 与线程的优雅退出
结束 Thread 线程的几种方法:
- 使用退出标志,使线程正常退出(run 方法完成后线程终止);
- 使用
interrupt()方法中断线程; - 使用
stop()方法强行终止线程(不推荐使用,可能发生不可预料的结果)。
前两种方法都可以实现线程的正常退出(优雅结束线程);第 3 种相当于电脑断电关机,是不安全的方法。
使用退出标志终止线程:使用一个 volatile boolean 变量控制循环,保证同一时刻只能由一个线程修改退出标志的值:
public class ThreadSafe extends Thread { public volatile boolean exit = false; public void run() { while (!exit) { //do something } } }使用 interrupt() 终止线程要分两种情况:
- 线程处于阻塞状态(如使用了 sleep、wait、socket 的 receiver、accept 等):调用 interrupt() 时系统会抛出
InterruptedException,通过捕获异常然后 break 跳出循环使线程正常结束。很多人认为只要调用 interrupt 方法线程就会结束,实际上是错的,一定要先捕获 InterruptedException 异常之后通过 break 跳出循环才能正常结束 run 方法; - 线程未进入阻塞状态:使用
isInterrupted()判断线程的中断标志来退出循环。
为什么必须区分这两种情况?因为阻塞状态发生时,如果有 interrupt() 发生,系统除了抛出InterruptedException外还会调用interrupted()函数(获取中断状态后复位为 false),所以异常抛出之后通过 isInterrupted() 是获取不到 true 状态的,从而不能退出循环。因此最好的方式是两种情况都考虑:
public class ThreadSafe extends Thread { public void run() { while (!isInterrupted()) { //非阻塞过程中通过判断中断标志来退出 try { Thread.sleep(5 * 1000); //阻塞过程捕获中断异常来退出 } catch (InterruptedException e) { e.printStackTrace(); break; //捕获到异常之后,执行break跳出循环 } } } }stop() 的危险性:调用thread.stop()会强行终止线程,就像突然关闭计算机电源而非正常关机一样,可能产生不可预料的结果。stop()调用之后创建子线程的线程会抛出 ThreadDeath 错误,并且会释放子线程所持有的所有锁。加锁的代码块都是为了保护数据的一致性,如果调用 stop() 导致线程所持有的所有锁突然释放(不可控制),被保护数据就有可能呈现不一致性,其他线程使用这些被破坏的数据时可能导致奇怪的应用程序错误。因此并不推荐使用 stop 方法来终止线程。完整分析见 08.线程中stop原理深入.md。
09 从 start 看线程异常处理机制
9.1 为什么不能抛出到外部线程捕获
在 Java 中,线程中的异常是不能抛出到调用该线程的外部方法中捕获的。JVM 的这种设计源自于这样一种理念:因为线程是独立执行的代码片段,线程的问题应该由线程自己来解决,而不要委托到外部。基于这样的设计理念,线程方法的异常(无论是 checked 还是 unchecked exception)都应该在线程代码边界之内(run 方法内)进行 try catch 并处理掉——换句话说,我们不能捕获从线程中逃逸的异常。
这种限制通过java.lang.Runnable.run()方法声明(方法声明上没有 throw exception 部分)进行了约束。如果在线程中抛出了 unchecked exception(运行时异常),线程会立即终结;而对于主线程和其他线程完全不受影响,且完全感知不到某个线程抛出的异常(也就是完全无法 catch 到这个异常)。
9.2 Thread.UncaughtExceptionHandler 统一捕获
JDK 5 之前的解决办法是线程组,JDK 5 之后新引进了一个接口Thread.UncaughtExceptionHandler,作用是为每个创建的线程都附着异常处理器。为了配合使用,又引入了ThreadFactory接口,在newThread方法中为每个新创建的线程设置Thread.UncaughtExceptionHandler,然后将带有异常处理器的线程返回:
// 第一步:创建 Thread.UncaughtExceptionHandler 实例 class MyUncaughtExceptionhandler implements Thread.UncaughtExceptionHandler { @Override public void uncaughtException(Thread t, Throwable e) { System.out.println("caught " + e); } } // 第二步:创建 ThreadFactory,为每个线程设置异常处理器 class HandelThreadFactory implements ThreadFactory { @Override public Thread newThread(Runnable r) { System.out.println(this + " creating new Thread"); Thread t = new Thread(r); System.out.println("created " + t); t.setUncaughtExceptionHandler(new MyUncaughtExceptionhandler()); System.out.println("eh = " + t.getUncaughtExceptionHandler()); return t; } } // 测试:把 ThreadFactory 实现类作为参数传给线程池 ExecutorService exc = Executors.newCachedThreadPool(new HandelThreadFactory()); exc.execute(new ExceptionThread()); exc.shutdown();这样就能捕获到线程内部抛出的运行时异常(控制台输出caught java.lang.RuntimeException),避免了由于检查不到的运行时错误导致线程意外死亡,方便统一管理;并且如果线程中需要释放资源,可以写在 UncaughtExceptionHandler 实现类的uncaughtException方法中,使线程独立执行、问题自行解决。完整案例见 12.线程捕获异常的分析.md。
10 总结与延伸阅读
Thread.start()虽小,却是理解 Java 并发世界的第一块基石。本文围绕它梳理了一条完整知识链:
- start vs run:start 创建新线程并回调 run,直接调 run 只是普通方法调用;
- 源码校验:
threadStatus != 0 || started抛IllegalThreadStateException,线程对象一次性使用; - 底层实现:native 层创建线程,JDK 1.2 后一对一映射内核线程,抢占式调度;
- 生命周期:NEW → RUNNABLE → BLOCKED/WAITING/TIMED_WAIT → TERMINATED 的六状态流转;
- 配套 API:join/yield/sleep/interrupt/setDaemon 对线程状态的精确影响;
- 异常与协作:run 内异常自处理 + UncaughtExceptionHandler 统一兜底。
本仓库「06.线程知识」目录下还收录了完整配套内容:线程与进程的基础概念见 01.线程进程基础介绍.md,生命周期详解见 02.线程生命周期说明.md,API 深度解读见 03.线程Api深度原理分析.md,以及 09.线程通信交互实践.md、13.ThreadLocal分析.md、14.线程池的基础介绍.md 等进阶主题,可作为面试复习与源码深挖的路线图。
- 教程
- 技术博客
- 文档
【免费下载链接】YCBlogs
技术博客笔记大汇总,包括Java基础,线程,并发,数据结构;Android技术博客等等;常用设计模式;常见的算法;网络协议知识点;部分flutter笔记;还包括平时开发中遇到的bug汇总,当然也在工作之余收集了大量的面试题,长期更新维护并且修正,持续完善……开源的文件是markdown格式的!转载请注明出处,谢谢!
相关推荐
type-challenges 中等题精讲:为 `Promise.all` 编写类型安全的 `PromiseAll<T>` 函数
type challenges 中等题精讲:为 Promise.all 编写类型安全的 PromiseAll<T 函数 导读 本篇指南围绕 type chall
教程技术博客文档Java Thread 源码深入解析:线程状态流转、生命周期与 ThreadLocal 的底层基石
Java Thread 源码深入解析:线程状态流转、生命周期与 ThreadLocal 的底层基石 Thread 是 Java 多线程编程的根基,几乎所有的并发
文档教程知识库YCBlogs Java 线程知识:线程与进程基础、状态转换、死锁与用户态内核态深度解析
YCBlogs Java 线程知识:线程与进程基础、状态转换、死锁与用户态内核态深度解析 本文基于 java/06.线程知识/01.线程进程基础介绍.md ht
教程技术博客文档
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考