第038篇 Thread 与 Runnable:线程生命周期全解

简介: Thread与Runnable本质是“任务”与“执行单元”的分离:Runnable仅定义要做的事(无返回、不抛检异常),Thread负责调度执行(含状态、优先级、生命周期控制)。`start()`才真正启新线程,`run()`只是普通方法调用。常见坑包括误调`run`导致伪并发、异常静默终止、持有Activity引发内存泄漏。工程中应优先使用线程池而非裸Thread。

Thread 与 Runnable 属于那种"人人以为简单、追问就露怯"的题。基础问答谁都答得上,可面试官顺着往下问两句——start 和 run 到底差在哪、Runnable 返回值去了哪里、线程状态怎么从 RUNNABLE 变成 BLOCKED——能接住的就不多了。这题的上限不在记忆,而在能不能把"任务"与"执行单元"这两个概念拆开。

先把结论放在前面:Runnable 只描述"要做什么",Thread 负责"由谁去做"。Runnable 是一个函数式接口,只有一个 run() 方法,没有返回值也不允许抛受检异常——所以任务无法把结果回传给调用者,这是它与 Callable 的分水岭。 Thread 则封装了线程状态、优先级、守护标记与线程组,并提供 start、join、interrupt 这一整套生命周期控制方法。理解了"任务与执行单元分离"这条主线,后面的追问都能顺着推出来。

机制拆解

讲清 start 与 run 的本质差异,这是最常被追问的一处。run 只是普通方法调用,执行在当前线程里,代码串行跑完,什么并发都没发生;start 才是向 JVM 申请新线程,由新线程的 run 执行入口去回调这个方法体。 可以用 Thread.currentThread().getName() 在 run 内部打印名字来验证:直接调 run 拿到的是 main,调 start 拿到的是另一个名字。同理,一个 Thread 对象只能 start 一次,第二次会抛 IllegalThreadStateException,因为线程只能被创建一次。

这些坑的正确绕法

最常见的坑是 new Thread 后直接 run,代码在当前线程串行执行,还以为是并发。 表现是"代码里的 Thread.sleep 明明写了,界面却一点没卡",或者"两个任务的结果总按调用顺序输出"。定位方法很简单:在 run 里打印线程名,与 main 对比即可确认。写异步代码时顺手检查这一点,能省掉不少"明明起了线程却不生效"的困惑。

其次是线程里未捕获异常静默终止,不设置 UncaughtExceptionHandler 就没有日志可查。 线程没有调用方的栈帧去接住异常,异常会直接交给线程的 UncaughtExceptionHandler;默认实现只是把栈打到标准错误流,而 Android 上这个流经常不落日志,于是线程"悄悄死了",任务再也没人推进。 对策是给线程池设置全局的 UncaughtExceptionHandler,或提交任务时用 submit 而非 execute——前者会把异常封进 Future,能被 get 时重新抛出。

还有一个更隐蔽的坑:子线程持有 Activity 引用,页面销毁后任务还在跑,内存泄漏与 UI 崩溃一起来。 后台任务里 Log.d("tag", userName) 这种写法间接引用了外层对象,Activity 迟迟无法回收;更糟的是任务回调里调 runOnUiThread 更新已经销毁的界面。修法是任务只持有纯数据与弱引用,界面更新前检查生命周期状态。

给 Thread 与 Runnable 建一份边界清单,注释里写清楚,单测覆盖边界与异常路径。 具体的约定可以是:耗时操作一律交给线程池而非手工 new Thread;任务只接收参数化数据、不持有界面对象;所有异步回调都要能容忍"回来时数据已过期"。

代码里见真章

看一段能直接跑的代码,把上面的机制落到具体写法上:

// Thread 与 Runnable:任务与执行单元分离
Thread t = new Thread(() -> {                // Runnable 只描述任务
    System.out.println(Thread.currentThread().getName());
}, "worker-1");                              // 自定义线程名,排查时很有用
t.start();                                   // start 才创建新线程
t.join(1000);                                // 等最多 1 秒
// 生命周期:NEW → RUNNABLE → BLOCKED/WAITING → TERMINATED
// run() 是普通方法调用,直接调会在当前线程串行执行

这段代码值得盯三处:第一处,构造器的第二个参数是线程名,线上排查时靠它定位是谁的线程;第二处,start 与 run 的区别决定了并发是否真的发生;第三处,join 的语义是"等对方结束",带超时参数版本更适合兜底场景。面试讲到这一层,基本就稳了。

这题在面试里怎么问、怎么答

"请简单介绍一下 Thread 与 Runnable,它在 Android 开发中起什么作用?"先各给一句定位:Runnable 是任务定义,Thread 是执行单元。再补一个真实场景——把一次网络请求拆成任务交给线程池,任务本身不关心由哪个线程执行。再落一个细节:需要返回结果就用 Callable 加 Future,需要异常回传就用 submit 而不是 execute。

"Thread 与 Runnable 的底层原理是什么?能不能详细说一下?"按动机、机制、代价三层答。动机是复用线程、控制并发度;机制是 start 最终走到 JVM 的 start0 原生方法,由操作系统创建线程后回调 run;代价是线程切换有开销、线程数过多会耗尽栈与内存。 再补状态机:BLOCKED 专指等 synchronized 监视器,而 wait、join、sleep 进入的是 WAITING/TIMED_WAITING,这个区分是高频追问点。

"在使用 Thread 与 Runnable 时遇到过什么问题?"拿真实案例。一个典型案例:埋点上报用了 new Thread(...).start(),某次上报里读取了页面上下文,页面销毁后回调里更新 UI 引发崩溃;定位到线程持有 Activity 引用且未做生命周期检查;修复为任务只携带埋点数据、上报完成后不触碰界面;验证是线上崩溃率归零。

"Thread 与 Runnable 和相关的替代方案相比,有什么优劣?"与 ExecutorService 比,手工 new Thread 没有复用、没有队列、没有拒绝策略,是应当被淘汰的写法;与 Handler 比,Handler 适合与消息队列绑定的场景(跨线程更新 UI),线程池适合纯后台任务。 选型结论是:新代码默认用线程池,Thread 只在需要精细控制线程属性时才手工创建。选型时把"异常静默终止、持有界面引用"这类代价摆到台面上,再决定是否引入。

再补一个工程上值得讲清的点:Callable 与 Future 补上了 Runnable 的两块缺口。Callable 允许返回值与抛受检异常,Future 则提供了 get(阻塞取结果)、isDone、cancel 这套控制接口。 但要注意三个实践细节:一是 get() 会阻塞,忘记设超时会造成线程卡死,稳妥做法是 get(timeout, unit);二是 cancel(true) 是"请求中断",任务是否真停下来取决于任务内部有没有响应中断,所以循环里要检查 Thread.currentThread().isInterrupted();三是任务抛出的异常会被封进 Future,只有调用 get 时才重新抛出,若从不 get,异常就一直沉默着——这一点与前面 UncaughtExceptionHandler 的坑本质相同。 能把这两者串起来讲,说明对"异步结果回收"这件事有完整认识。

给正在准备面试的你

把线程这条线画成一张图:横轴是状态(NEW / RUNNABLE / BLOCKED / WAITING / TERMINATED),纵轴标出触发转换的方法——start 进 RUNNABLE、抢不到锁进 BLOCKED、wait/join/sleep 进 WAITING、run 结束进 TERMINATED,再把 interrupt 能打断哪些状态标出来。 面试中关于 Thread 与 Runnable 的问题,关键在于能够从原理、应用、踩坑三个层面给出有深度的回答。

再补工程案例与踩坑——应用落点是把项目里所有 new Thread().start() 收敛到一个受控线程池,并加一个埋点统计线程池的活跃数、队列长度与拒绝次数。

复习时别孤立刷题:synchronized 原理——锁与线程状态强绑定,BLOCKED 与 WAITING 的区别就出自这里。

划两句重点:Runnable 描述任务、Thread 承载执行,start 才开新线程;异步任务的异常会随 Future 被封存,取不出就一直沉默。

下一篇聊 synchronized 原理:对象头、锁升级与重量级锁——沿着今天这条主线继续往前走。


如果这篇文章对你有帮助,欢迎点赞、在看、转发三连。你的支持就是这个系列持续更新的动力。

「Android软件开发面试·从入门到精通」连载系列

上一篇:动态代理:AOP-与框架的基石

下一篇预告:synchronized-原理:对象头、锁升级与重量级锁

有任何问题欢迎在评论区留言交流。

相关文章
|
13小时前
|
监控 Java Android开发
第045篇 JVM 内存区域:堆、栈、方法区与程序计数器
JVM内存区域是面试分水岭:非死记硬背,而要理解线程私有(栈、PC计数器、本地方法栈)与共享(堆、元空间)的本质差异,能据OOM类型精准定位根因——栈溢出看调用链、堆OOM析对象引用、元空间OOM查类加载、直接内存OOM盯NIO缓冲。
16 0
|
13小时前
|
SQL 缓存 安全
第058篇 单例模式六种写法:线程安全与懒加载的平衡
单例模式是面试高频考点,六种写法各具特点:饿汉式线程安全但非懒加载;懒汉式需同步;DCL需`volatile`防重排;静态内部类最推荐(JVM类初始化锁保障懒加载与线程安全);枚举天然防反射与序列化。Android中优先用静态内部类,持Context时务必用`getApplicationContext()`,避免内存泄漏。
24 0
|
14小时前
|
缓存 安全 测试技术
第032篇 LinkedHashMap 与 LRU:图片缓存的原型
Android面试高阶题常以LinkedHashMap与LRU为切入点,考察“顺序”这一关键维度:它通过双向链表维护插入/访问序(accessOrder=true时get/put自动移至尾部),配合removeEldestEntry可几行实现线程不安全但高效的LRU缓存。LruCache即基于此构建。
16 0
|
13小时前
|
SQL Java 编译器
第040篇 volatile:可见性、有序性与禁止重排
`volatile` 是Java轻量级同步机制,核心解决**可见性**与**有序性**(靠内存屏障实现),但**不保证原子性**。典型适用:状态标志、引用发布、DCL单例;禁用场景:`count++`、多变量一致性、检查后动作——这些须用原子类或锁。
15 0
|
14小时前
|
缓存 JSON Java
第035篇 反射基础:Class 对象与运行时类型信息
Java反射是运行期类型自省机制,通过Class对象动态获取并操作类成员(Method/Field/Constructor),支撑注解处理、DI、序列化等框架。需注意:`getDeclaredXxx`获取全部成员(含私有),`setAccessible(true)`突破访问限制(JDK9+受模块系统约束),`invoke`返回Object需手动强转,且应缓存Method以避免重复查找开销。
16 0
|
13小时前
|
安全 Java 编译器
第075篇 when 表达式:比 switch 强在哪里
Kotlin 的 `when` 是强大表达式,远超 Java `switch`:支持值、区间、类型、集合、任意条件五种分支;具备智能转换、顺序匹配、穷举检查(对 sealed/enum 编译期报错);作为表达式需覆盖所有路径,慎用空 `else`。核心价值:将分支逻辑结构化、可赋值、可审查。
21 0
|
13小时前
|
缓存 安全 Android开发
第065篇 Elvis 运算符与 let:空值处理的组合拳
Kotlin 中 `?:`(Elvis)与 `let` 是被低估却极富表现力的空安全利器:`?:` 是懒求值表达式,专用于优雅兜底或提前返回;`let` 是内联作用域函数,在非空时以参数形式安全处理值。二者常组合为 `x?.let { ... } ?: default`,将“有则处理、无则兜底”升华为可组合、可读、零开销的声明式逻辑。关键在理解求值时机与意图选型——缺失用 `?:`,非空处理用 `let`,状态语义复杂时请用 sealed class 显式建模。
16 0
|
14小时前
|
安全 Java 编译器
第025篇 泛型通配符与 PECS:一句话讲清 extends 与 super
泛型通配符与PECS(Producer Extends, Consumer Super)是泛型核心难点。上界`? extends T`用于只读(生产者),下界`? super T`用于只写(消费者)。讲清“为何上界不能add、下界不能get”,结合`Collections.copy`等JDK源码实践,才能体现真理解——而非死记口诀。
17 0
|
13小时前
|
安全 Java Android开发
第055篇 Optional 与空值处理:比判空更优雅的表达
`Optional<T>` 是 Java 8 引入的容器类,核心价值是**将“可能为空”显式表达在类型中**,提升空安全与可读性。仅限用作**方法返回值**,禁用于字段、参数及高频路径;需搭配 `ofNullable`、`map`/`flatMap`、`orElseGet` 等规范使用,避免序列化、性能与语义陷阱。
23 0
|
15小时前
|
缓存 监控 Java
第021篇 异常体系 Throwable:Checked 与 Unchecked 的边界
Android面试高频题:Throwable异常体系,需透彻理解Error/Exception区别、受检/非受检划分逻辑,以及finally执行机制。重点在于“为什么这样设计”和“用错的后果”——如吞异常致线上脏数据、滥用异常控流程拖垮性能、finally抛异常掩盖根因等。真懂者必踩过坑、用过、复盘过。
17 0