文章内容
本文内容整理自享学团队直播课《Handler 运行流程源码解析(二)》
一、一个线程只有一个 Handler?
"一个线程有几个 Handler、几个 Looper"这个问题,最常见的答法是两个都答 1。前半个答案是错的,而且很容易当场验证:在一个 Activity 里 new Handler(Looper.getMainLooper()),在同一个 App 的 Service 里再 new 一个,两个对象的 hashCode 不同,h1 == h2 为 false,但 h1.getLooper() == h2.getLooper() 为 true。两个 Handler,一个 Looper。
问题就出在这里:Handler 可以有很多个,Looper 却只有一个。这个"只有一个"是靠什么机制强制住的?系统凭什么能保证一条线程上 new Looper() 不会发生第二次?
一个线程可以有任意多个 Handler(Activity、Service 各自 new Handler,都跑在主线程,共用同一条 MessageQueue);一个线程只能有一个 Looper。唯一性靠 ThreadLocal 保证:Looper.prepare() 先用 sThreadLocal.get() 查当前线程是否已经放过 Looper,拿得到就直接抛 RuntimeException("Only one Looper may be created per thread"),拿不到才 new Looper 并 sThreadLocal.set()。ThreadLocal 是一个 key-value 存储,key 是 ThreadLocal 实例本身(用弱引用持有),value 是 Looper;每个线程持有自己的一份 map,通过 Thread.currentThread() 取到当前线程那份,所以线程与 Looper 天然一一对应。
二、一个线程有几个 Looper:只有一个

同一线程上的所有 Handler,发出的消息都在同一条链表里按 when 排队。
Handler:N 个。它是发送方兼处理方,构造时确定要往哪条队列投消息(来自传入的 Looper)。一个 Activity 一个、一个 Service 一个、一个第三方 SDK 一个,互不干扰。它们各自的 handleMessage 不同,但投递目标是同一条链表。
Looper:1 个。它是那个死循环的驱动者,和线程一一绑定。
MessageQueue:1 个。它挂在 Looper 身上(Looper.mQueue),所以线程里有几个 Looper 就有几条队列,也就是一条。
这个数量关系的后果是:一个 Handler 的 handleMessage 里做了耗时操作,其他所有 Handler 的消息都得排队等着。
// 主线程在 ActivityThread.main 里已 prepareMainLooper + loop,自带一个 Looper// 再 new Handler 只是"发送方/处理方",不会新建 Looper,只是往同一条队列投消息// 第一个 Handler:发送方 A,指向主线程队列Handler h1 = new Handler(Looper.getMainLooper());// 第二个 Handler:发送方 B,与 h1 共用同一条主线程队列Handler h2 = new Handler(Looper.getMainLooper());// 两个 Handler 共用同一条主线程 MessageQueue,只是各自 handleMessage 不同// h1 != h2,但 h1.getLooper() == h2.getLooper() 且队列是同一个为什么 Looper 必须是 1 个?假设一条线程上有两个 Looper,每个各带一条队列。线程只有一个执行流,loop() 是死循环,跑进第一个 Looper 的 loop() 之后就再也出不来,第二条队列里的消息永远不会被消费。
三、如何保证唯一:ThreadLocal

禁止的位置在 Looper.prepare()。这个方法是所有 Looper 的唯一创建入口:Looper 的构造函数是 private 的,外部无法直接 new。
// ThreadLocal 静态实例:每个线程一份,value 存的是真正被绑定的 Looperstatic final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();// prepare:在"当前线程"上创建唯一的 Looperprivate static void prepare(boolean quitAllowed) { // 先查一下当前线程是否已经放过 Looper if (sThreadLocal.get() != null) { // 已存在:说明这条线程已经 prepare 过一次,再次创建抛出异常 throw new RuntimeException("Only one Looper may be created per thread"); // 已存在分支结束 } // 首次创建:new 一个 Looper,并通过 ThreadLocal 绑定到当前线程 sThreadLocal.set(new Looper(quitAllowed)); // 待核实:不同源里 prepare 的 quitAllowed/构造参数有差异// prepare 方法体结束}prepare 只接收一个 quitAllowed 参数(主线程的 Looper 传 false,不允许 quit;普通线程传 true),没有返回值。它做的是在当前线程的那份存储里写入 sThreadLocal → Looper 的映射,同时这个 Looper 的构造函数里已经 new 好了它自己的 MessageQueue。
唯一性靠一次"查-建"完成,判据存在 ThreadLocal 里。sThreadLocal 是 static final 的,整个进程只有这一个实例;但它的 get() 返回什么,取决于调用它的线程。同一句 sThreadLocal.get(),主线程调用返回主线程的 Looper,工作线程调用返回 null。后面三节要拆的,就是这个"同一个对象在不同线程读出不同值"的行为。
共享的是 ThreadLocal 这个容器对象本身,不是它装的值。容器是全局的,值是线程私有的,这正是 ThreadLocal 这个名字的含义。
prepare() 是 private 的,外部调用的是它的两个公开包装:Looper.prepare()(quitAllowed = true)和 Looper.prepareMainLooper()(quitAllowed = false,只在 ActivityThread.main 里调一次)。这也是为什么主线程的 Looper 不能被 quit:退出它等于结束应用主线程。
四、ThreadLocal 是什么:key-value 存储(key=ThreadLocal、value=Looper)

把 ThreadLocal 当成一个 map 来理解最省事:它提供 set(value) 和 get(),但存取的位置不在自己身上。
// set:把 value(Looper)存到"当前线程"专属的存储里public void set(T value) { // 拿到当前线程,ThreadLocal 是每个线程一份 Thread t = Thread.currentThread(); // 取出该线程的 ThreadLocalMap(旧版叫 Values) ThreadLocalMap map = getMap(t); // 该线程已经有 map if (map != null) { // 以"this(当前 ThreadLocal)"为 key,value(Looper) 放进 map map.set(this, value); // 已有 map 分支结束 } else { // 首次:为这个线程建一份 map createMap(t, value); // 首次分支结束 }// set 方法体结束}// get:从"当前线程"专属的存储里取回 valuepublic T get() { // 同样先拿当前线程 Thread t = Thread.currentThread(); // 取它的 map,用 this 作为 key 找 value(Looper) ThreadLocalMap map = getMap(t); // 该线程已经有 map if (map != null) { // 用当前 ThreadLocal 实例作为 key 查条目 ThreadLocalMap.Entry e = map.getEntry(this); // 查到了条目 if (e != null) { // 找到就返回这个线程对应的 Looper return (T) e.value; // 命中分支结束 } // 有 map 分支结束 } // 没查到:走初始值逻辑(默认返回 null) return setInitialValue();// get 方法体结束}set 存的是 value,get 取回同类型对象,两者都作用于 Thread 对象的 threadLocals 字段——这个字段属于 Thread,不属于 ThreadLocal。
数据存在线程身上,ThreadLocal 只是那把钥匙。getMap(t) 的实现就是 return t.threadLocals;,取的是传进来那个 Thread 对象的字段;map.set(this, value) 里的 this 是 ThreadLocal 实例,被当作 key 用。
要是两个线程各自 prepare(),会不会互相覆盖?
不会。两次 set 里的 Thread.currentThread() 返回的是不同的 Thread 对象,getMap(t) 取到的是两份完全独立的 map,写入互不可见。sThreadLocal 这个 key 是同一个没错,但它是在两张不同的表里各占一个槽位。主线程那张表里 sThreadLocal → 主线程 Looper,工作线程那张表里 sThreadLocal → 工作线程 Looper。
再往下一层:既然 map 挂在 Thread 上,那这套机制天生不需要加锁。一份 map 只会被它所属的那条线程访问:写它的是这条线程,读它的也是这条线程。ThreadLocalMap 的实现里没有任何 synchronized,这是它和 ConcurrentHashMap 那类结构在设计上的根本分野(推断:严格说存在通过反射跨线程访问的可能,但正常使用路径上不存在竞争)。
五、底层数组怎么存 key-value:旧版 Values / 新版 ThreadLocalMap
map 内部长什么样,决定了 key 是怎么被持有的(这一点和内存泄漏直接相关)。
// ThreadLocalMap:用数组存 key-value 对;旧版本用单个 Object[] table 按 i、i+1 交替放 key 和 value// 待核实:具体实现(Values vs ThreadLocalMap)依 Android 版本变化private Entry[] table;// 一个条目:key(弱引用持有 ThreadLocal) + value(Looper)private static class Entry extends WeakReference<ThreadLocal<?>> { // 真正放的是 Looper final Object value; // 构造:key 交给弱引用父类,value 强引用持有 Entry(ThreadLocal<?> k, Object v) { // 把 key 作为弱引用的引用对象 super(k); // value 直接赋值,强引用 value = v; // 构造函数结束 }// Entry 类体结束}// set:把 (key=ThreadLocal, value=Looper) 写入 table 的对应槽位private void set(ThreadLocal<?> key, Object value) { // 计算槽位并放入 table[index] = new Entry(key, value);// set 方法体结束}这里没有返回值,作用就是往 table 数组的某个槽位写入一个新 Entry。
关键在存储布局与引用强度:table 是一个 Entry[],Entry 继承 WeakReference<ThreadLocal<?>>,也就是 key 被弱引用持有,value 被强引用持有。定位槽位用的是 ThreadLocal 实例的哈希值取模,冲突时线性探测往后找空位(开放寻址,不是链表法)。旧版 Android 的 Values 用一个 Object[] 把 key 和 value 交替放在 i 和 i+1 上,寻址方式类似,只是没有独立的 Entry 类型。
key 用弱引用是为了让不再被外部引用的 ThreadLocal 能被回收。这带来一个经典问题:key 被回收后 Entry 变成 key 为 null、value 仍被强引用的状态,如果线程长期存活(线程池里的线程就是这样),这个 value 就泄漏了。ThreadLocalMap 在 set / get 的探测过程中会顺手清理这类陈旧 Entry,但清理不是确定性的,所以规范做法仍然是用完调 remove()。
放到 Looper 这个场景上,泄漏风险基本不存在(sThreadLocal 是 static final 的,永远有强引用,key 不会被回收;而 Looper 的生命周期本来就该和线程一样长)。真正需要警惕 ThreadLocal 泄漏的是业务代码里那些临时 ThreadLocal,尤其是在线程池线程上存了大对象又没 remove。
六、set 存了什么:key=ThreadLocal,value=Looper,绑定的是当前线程
Looper.prepare() 里的 sThreadLocal.set(new Looper(quitAllowed)) 展开后,会经过这样一串动作:
new Looper(quitAllowed) 先执行,构造函数里 mQueue = new MessageQueue(quitAllowed)、mThread = Thread.currentThread()(注意 Looper 自己也记了一份线程引用,这是它后来能做 myLooper().getThread() 这类校验的依据)。然后 set 进入:Thread.currentThread() 拿到执行这行代码的线程对象 t;getMap(t) 取 t.threadLocals;首次为 null,走 createMap(t, value),给 t 新建一张表并写入第一条 Entry;非首次则 map.set(this, value),this 就是那个 static final 的 sThreadLocal。
私有的 Looper 构造函数只做两件事:建队列、记线程,这正是"线程与 Looper 绑定"落地的起点:
// Looper 构造函数:绑定当前线程并创建自己的消息队列private Looper(boolean quitAllowed) { // 每个 Looper 都带一条自己的队列 mQueue = new MessageQueue(quitAllowed); // 记录创建它的线程,作为后来 myLooper().getThread() 校验的依据 mThread = Thread.currentThread();// 构造函数结束}输出是一个已经和当前线程绑定好的 Looper:mQueue 是它自己的队列,mThread 指向执行 prepare() 的那条线程。两个字段都是 final,构造时一次性定死,之后不会变——所以"一个 Looper 只属于一条线程"在这个构造函数里就已经写死了。
三个"当前"要对齐:当前线程是执行 prepare() 的那条线程,不是定义 sThreadLocal 的类所在的线程(类没有线程),也不是后来 new Handler 的线程。这是 Looper.prepare() 必须在目标线程内部调用的原因:在 A 线程里调 prepare() 不会给 B 线程建 Looper。HandlerThread 把这件事包装成了 run() 里的 Looper.prepare(); ...; Looper.loop();,正是因为它必须发生在新线程自己的执行流里。
myLooper() 的实现就一句 return sThreadLocal.get();,取的是当前线程那份。所以在工作线程里调 Looper.myLooper() 得到 null,而 Looper.getMainLooper() 在任何线程都返回主线程的 Looper:后者读的是 Looper 类里另一个 static Looper sMainLooper 字段,那是一个真正的全局静态引用,在 prepareMainLooper() 里被赋值,与 ThreadLocal 无关。
两个取 Looper 的入口,底层完全不同,这也是"当前线程"最容易混淆的地方:
// myLooper:从 ThreadLocal 取"当前线程"那份,线程私有public static Looper myLooper() { // 同一句 get,在不同线程读到不同 Looper(或 null) return sThreadLocal.get();// myLooper 结束} // getMainLooper:读类级静态字段,全局唯一,与当前线程无关public static Looper getMainLooper() { // 直接返回全局静态引用,不经过 ThreadLocal return sMainLooper;// getMainLooper 结束} // Looper 里保存的线程引用,是"校验"能力的来源public Thread getThread() { // 返回构造时记下的那条线程 return mThread;// getThread 结束}myLooper() 走 ThreadLocal,结果随调用线程变化;getMainLooper() 读 static Looper sMainLooper,任何线程拿到的是同一个对象。getThread() 是 Looper 自己记下的那份引用,用于校验它和调用线程是否同一条。三者的区别就在于:前者读线程私有的 map,中间读类级静态字段,后者读 Looper 内部的 final 字段。

七、为什么线程与 Looper 一一对应

线程与 Looper 构成双射,由两个方向的约束共同形成。
一个线程至多一个 Looper:来自 prepare() 里那个 get() != null 的判断。同一条线程第二次调用会抛 RuntimeException,异常信息就是 Only one Looper may be created per thread。这是硬失败,不是静默忽略。
一个 Looper 只属于一个线程:来自 ThreadLocal 的存储位置。Looper 被存进了某条 Thread 对象的 threadLocals 里,别的线程 get() 时用自己的 Thread 去取表,取不到这一份。就算把 Looper 对象引用传给别的线程(比如 Looper.getMainLooper() 就是这么用的),那也只是拿到了一个对象引用,不改变"哪条线程在跑它的 loop()"这个事实。
两条合起来,线程与 Looper 构成双射。再加上 Looper 构造时就绑定了 MessageQueue,链条就完整了:一条线程 → 一个 Looper → 一条 MessageQueue → N 个 Handler 往里投消息。
这个结构解释了几个常见现象:
工作线程里直接 new Handler() 抛 Can't create handler inside thread that has not called Looper.prepare(),因为 Handler 构造时会 Looper.myLooper() 取当前线程的 Looper,取到 null 就报错。改成 new Handler(Looper.getMainLooper()) 就没事(显式给了它一条别的线程的队列)。
HandlerThread 的 getLooper() 内部有一个 wait:它得等新线程真正跑到 Looper.prepare() 并把 Looper 存进 ThreadLocal 之后,外部才能拿到非 null 的引用。线程启动和 Looper 就绪之间有一个时间差,这是它需要同步的原因。
同一个 Handler 对象被多个线程调用 sendMessage 是安全的,消息都进同一条队列:队列在构造 Handler 时就定了(mQueue = looper.mQueue),和调用 sendMessage 的线程无关;线程安全由 MessageQueue 里那把锁保证。
八、面试怎么层层追问 + 可验证入口

Q&A:
"一个线程有几个 Handler?"——多个,Activity、Service、SDK 各自持有,但是共用一条队列。
"几个 Looper?"——一个。追问"为什么不能多个",答执行流只有一条,第二个 Looper 的 loop() 永远轮不到。
"怎么保证只有一个?"——Looper.prepare() 里先 sThreadLocal.get(),非 null 直接抛 Only one Looper may be created per thread。
"ThreadLocal 是什么?"——一个以 ThreadLocal 实例为 key、以业务对象为 value 的键值存储,数据实际存在 Thread.threadLocals 上,每线程一份。
"底层怎么存?"——Entry[] 数组,开放寻址;Entry 继承 WeakReference,key 弱引用、value 强引用;旧版 Android 是 Values + 单 Object[] 交替存放。
"弱引用会带来什么问题?"——key 被回收后 value 仍被强引用,长生命周期线程上会泄漏,用完要 remove()。
"ThreadLocal 和 synchronized 有什么区别?"——前者是线程私有存储,数据各线程一份,天然免锁;后者是共享内存,靠同步机制协调访问。
验证入口:
- 主线程和工作线程里分别打印
Looper.myLooper()与Looper.getMainLooper(),比较是否相等:主线程两者相同,工作线程里前者为 null。 - 在同一条线程上连着调两次
Looper.prepare(),第二次会抛RuntimeException: Only one Looper may be created per thread,这是唯一性最直接的证据,异常栈会指到Looper.prepare那一行。 new两个 Handler 都传Looper.getMainLooper(),比较h1 != h2而h1.getLooper() == h2.getLooper(),同时h1.getLooper().getQueue() == h2.getLooper().getQueue(),N 个 Handler、1 个 Looper、1 条队列。- 用调试器展开
Thread.currentThread()的threadLocals字段,能直接看到那张Entry[]表,以及其中 key 为Looper$1(sThreadLocal的匿名或具名实例)、value 为Looper的那一条。
回到最初的问题
一个线程可以有多个 Handler。它们共用目标线程唯一那条 MessageQueue,区别在各自的 handleMessage。
Looper 只能有一个。这个数量不是约定俗成,是 Looper.prepare() 里一行判空加一个 RuntimeException 强制出来的,判据存在 ThreadLocal 里。
ThreadLocal 之所以能当这个判据,是因为它把值存在 Thread 对象自己的 threadLocals 字段上,用 ThreadLocal 实例作 key。全局只有 sThreadLocal 一个容器,但每条线程有自己的一张表。同一句 sThreadLocal.get() 在不同线程读出不同的 Looper,线程与 Looper 的一一对应就是这么落地的。
本文由 AgentWorkFlow 生成