教程技术博客文档【免费下载链接】YCBlogs技术博客笔记大汇总包括Java基础线程并发数据结构Android技术博客等等常用设计模式常见的算法网络协议知识点部分flutter笔记还包括平时开发中遇到的bug汇总当然也在工作之余收集了大量的面试题长期更新维护并且修正持续完善……开源的文件是markdown格式的转载请注明出处谢谢项目地址https://gitcode.com/gh_mirrors/yc/YCBlogs点击查看免费下载本篇是 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 CallableInteger { 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(); FutureTaskInteger 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 慎用 ThreadLocalThreadLocal 是 Java 提供的一种保存线程私有信息的机制因为其在整个线程生命周期内有效所以可以方便地在一个线程关联的不同业务模块之间传递信息比如事务 ID、Cookie 等上下文相关信息。它的实现结构可以参考源码数据存储于线程相关的 ThreadLocalMap其内部条目是弱引用static class ThreadLocalMap { static class Entry extends WeakReferenceThreadLocal? { /** 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 密切相关的线程协作 API8.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 joint1.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 让出系统资源其他线程可以占用 CPUsleep(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 runstart 创建新线程并回调 run直接调 run 只是普通方法调用源码校验threadStatus ! 0 || started抛IllegalThreadStateException线程对象一次性使用底层实现native 层创建线程JDK 1.2 后一对一映射内核线程抢占式调度生命周期NEW → RUNNABLE → BLOCKED/WAITING/TIMED_WAIT → TERMINATED 的六状态流转配套 APIjoin/yield/sleep/interrupt/setDaemon 对线程状态的精确影响异常与协作run 内异常自处理 UncaughtExceptionHandler 统一兜底。本仓库「06.线程知识」目录下还收录了完整配套内容线程与进程的基础概念见 01.线程进程基础介绍.md生命周期详解见 02.线程生命周期说明.mdAPI 深度解读见 03.线程Api深度原理分析.md以及 09.线程通信交互实践.md、13.ThreadLocal分析.md、14.线程池的基础介绍.md 等进阶主题可作为面试复习与源码深挖的路线图。赞分享教程技术博客文档【免费下载链接】YCBlogs技术博客笔记大汇总包括Java基础线程并发数据结构Android技术博客等等常用设计模式常见的算法网络协议知识点部分flutter笔记还包括平时开发中遇到的bug汇总当然也在工作之余收集了大量的面试题长期更新维护并且修正持续完善……开源的文件是markdown格式的转载请注明出处谢谢项目地址https://gitcode.com/gh_mirrors/yc/YCBlogs点击查看免费下载相关推荐type-challenges 中等题精讲为 Promise.all 编写类型安全的 PromiseAllT 函数type challenges 中等题精讲为 Promise.all 编写类型安全的 PromiseAllT 函数 导读 本篇指南围绕 type chall教程技术博客文档Java Thread 源码深入解析线程状态流转、生命周期与 ThreadLocal 的底层基石Java Thread 源码深入解析线程状态流转、生命周期与 ThreadLocal 的底层基石 Thread 是 Java 多线程编程的根基几乎所有的并发文档教程知识库YCBlogs Java 线程知识线程与进程基础、状态转换、死锁与用户态内核态深度解析YCBlogs Java 线程知识线程与进程基础、状态转换、死锁与用户态内核态深度解析 本文基于 java/06.线程知识/01.线程进程基础介绍.md ht教程技术博客文档上一篇Pundit与Rails Mailers邮件内容的权限控制下一篇maths-cs-ai-compendium 分布式深度学习完全指南从内存墙到混合精度、数据/张量/流水线并行与 Ring All-Reduce创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考