对ThreadLocal在Handler中的应用的一些理解

本文解析了Android中ThreadLocal的工作原理及其实现方式,重点介绍了如何通过ThreadLocal存储和获取线程内部唯一的Looper对象。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言
JDK源码的ThreadLocal类和Android SDK的ThreadLocal类细节略有不同,但原理和实现的功能是相同的。本文的代码均来自Android SDK源码。
看下Android SDK源码里ThreadLocal的注释:

/**
 * Implements a thread-local storage, that is, a variable for which each thread
 * has its own value. All threads share the same {@code ThreadLocal} object,
 * but each sees a different value when accessing it, and changes made by one
 * thread do not affect the other threads. The implementation supports
 * {@code null} values.
 *
 * @see java.lang.Thread
 * @author Bob Lee
 */

就不翻译了。我的理解,ThreadLocal是“线程内部存储”,它不是一个线程,而是用于存储对象,线程内部获取到的这个对象是唯一的,而不同线程获取的这个对象则是不同的对象。

也即,这个对象的作用域是线程,而不是平常我们使用的包内,类内或者方法内等。

说个题外话,源码注释还有这么一句:

/* Thanks to Josh Bloch and Doug Lea for code reviews and impl advice. */

作者特意在源码注释里表示了对Josh Bloch和Doug Lea的感谢,谢谢他们的review和建议。原来这部分代码还有这两位Java大牛的参与~不多说,下面正式开始分析。

Handler与ThreadLocal
在Android中,一个典型的ThreadLocal的使用场景就是Handler类的实现源码里,ThreadLocal用于存储当前线程的Looper对象,从而把线程和Looper对象实现一一对应的关系。

下面具体看下源码实现:
Looper类里,有个静态变量sThreadLocal,它存储的对象是Looper对象:

static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();

而我们都知道,如果想在一个新线程里自定义Handler并使用的话,就必须先调用Looper.prepare(),然后定义Handler之后,再调用Looper.loop()。

Looper.prepare()里有这么一行代码:

sThreadLocal.set(new Looper(quitAllowed));

这一行的作用,就是把当前线程与新创建的Looper对象对应起来,那具体是怎么实现对应的呢?

如何保证Looper对象在线程里是唯一的
Looper的唯一的构造方法是在prepare()里调用的,当创建了一个Looper对象之后,立刻会被ThreadLocal的set()方法作为参数传入处理。

来看看ThreadLocal的set方法做了什么:

public void set(T value) {
    Thread currentThread = Thread.currentThread();//得到当前线程
    Values values = values(currentThread);// 得到当前线程的localValues变量的值
    if (values == null) {//首次执行时,会进入这里
        values = initializeValues(currentThread);//新创建一个Values对象
    }
    values.put(this, value);
}

1.来看下values()方法;

    Values values(Thread current) {
        return current.localValues;
    }

values()方法就是为了得到当前线程的localValues变量的值。那么这个localValues的值是什么?又是时候被赋值的呢?
看它在Thread类里的定义;

    ThreadLocal.Values localValues;

localValues它是个ThreadLocal的内部类Values类型的一个变量,权限是包内可访问,同时也没有对它的get和set方法。那么ThreadLocal的set方法首次获取当前线程的localValues就为null, 也就会走到initializeValues()方法里。

2.看下这个initializeValues()方法:

    Values initializeValues(Thread current) {
        return current.localValues = new Values();
    }

当前线程的localValues对象就是在这里创建的。

3.获取values对象后,最后是values.put(this, value),这个this就是sThreadLocal对象,value就是新创建的Looper对象。

这个put()方法就不分析了。可以把Values类当成一个Map(实际上Java的实现就是如此,而Android SDK里用的是一个Object数组,下标偶数的作为key,奇数下标的作为value,从而实现key 和value的一一对应。这部分可以理解为,每个线程有一个map, 这个map存储的每个键值对, Threadlocal对象为键, Looper对象为值。而这个线程里如果定义了其他ThreadLocal对象,也存在这个map里
,具体可以看下ThreadLocal的Values内部类的实现)。

也就是说, 对ThreadLocal对象的处理,实际上就是对当前线程的localValues对象里的Object数组的处理,因此操作就局限在了线程内部,也就实现了ThreadLocal可以在不同的线程存储不同的对象。


上面我们讲到,Looper的唯一的构造方法是在prepare()里调用的,当创建了一个Looper对象之后,立刻会被ThreadLocal的set()方法作为参数传入处理。

那么,线程会不会存在多个Looper对象,即Looper的构造方法会被多次调用呢?答案是不会。如果需要多个Looper对象,那就得多次调用Looper.prepare()。

    private static void prepare(boolean quitAllowed) {
        if (sThreadLocal.get() != null) {
            throw new RuntimeException("Only one Looper may be created per thread");
        }
        sThreadLocal.set(new Looper(quitAllowed));
    }

上面是Looper的prepare()的源码,首次调用Looper.prepare()时,sThreadLocal键值对已经生成。当再次进入这个prepare()方法时,先判断sThreadLocal.get() 的值,看下get()的实现:

    public T get() {
        // Optimized for the fast path.
        Thread currentThread = Thread.currentThread();
        Values values = values(currentThread);
        if (values != null) {
            Object[] table = values.table;
            int index = hash & values.mask;
            if (this.reference == table[index]) {
                return (T) table[index + 1];
            }
        } else {
            values = initializeValues(currentThread);
        }

        return (T) values.getAfterMiss(this);
    }

ThreadLocal的 get() 和 set()可以对照查看。都是先获取当前线程,然后获取线程values的值,对values进行处理。在走到get()方法里的时候,如果之前调用过set(),那么values()已经不为null了,因此会返回sThreadLocal作为键,所存储的Looper对象。

sThreadLocal.get()不为null,那么Looper.prepare()就会抛出异常了。也就是重复调用Looper.prepare()会抛出异常,而不会创建多个Looper对象。

既然一个线程有只一个Looper对象,而默认会有一个存储此Looper对象的ThreadLocal。那么有个疑问:存储Looper对象时,所用的键值对的形式,为什么不用当前线程作为键呢?

从之前的分析我们知道,ThreadLocal存储对象实际是用类似Map的方法存储的,可以理解为每个线程有一个map来存储ThreadLocal对象,这个map的键值对, Threadlocal对象为键, Looper对象为值。而这个线程里如果定义了其他ThreadLocal对象,也存在这个map里。一个线程可能有多个ThreadLocal对象(这些ThreadLocal对象我们可以自己创建,并不一定存储Looper对象),但只有这一个ThreadLocal map。因此,如果把线程作为键的话,无法存储不同的对象。

获取线程内唯一的Looper对象
上面的分析中,我们可以知道,一个线程里,sThreadLocal存储的Looper对象是唯一的,怎么获取这个线程唯一的Looper对象呢?
Looper类提供了一个myLooper()方法,用于获取Looper对象:

    public static @Nullable Looper myLooper() {
        return sThreadLocal.get();
    }

这也是获取looper对象唯一的方法:从sThreadLocal进行获取,因此也保证了获取到的Looper对象就是sThreadLocal所存储的对象,是此线程唯一的对象。

<think>好的,我现在需要帮助用户理解Java中的ThreadLocal。用户可能有一定的Java基础,但可能对ThreadLocal的具体使用场景原理不太清楚。首先,我需要回顾一下ThreadLocal的基本概念,它是用来在多线程环境下为每个线程提供独立的变量副本,避免线程间共享变量导致的数据竞争问题。 用户的问题是如何使用ThreadLocal,所以我需要分步骤讲解使用方法,包括初始化、赋值、获取值以及清理操作。同时,用户可能想知道为什么需要ThreadLocal,所以应该先解应用场景,比如数据库连接管理、用户会话信息存储等。 接下来,我需要考虑用户可能的疑问,比如内存泄漏的问题。ThreadLocal如果不正确使用,可能会导致内存泄漏,特别是在使用线程池时,线程会被复用,导致ThreadLocal的值没有被及时清理。因此,需要强调在Java 1.8之后,ThreadLocal的实现优化,使用弱引用减少泄漏风险,但最好还是在用完时调用remove()方法。 另外,用户可能对ThreadLocal的内部实现感兴趣,比如每个线程的ThreadLocalMap是如何存储数据的。不过这部分可能属于进阶内容,如果用户需要,可以简要提及,但主要应集中在使用方法上。 还要注意示例代码的正确性,确保语法正确,并展示典型的使用模式,比如用initialValue()方法初始化,或者使用withInitial()方法。同时,提醒用户避免在异步编程中误用ThreadLocal,比如CompletableFuture中线程切换导致的值丢失。 最后,总结使用步骤和注意事项,帮助用户快速掌握关键点。需要保持回答结构清晰,逐步引导,确保用户能理解并正确应用ThreadLocal。</think>### 一、ThreadLocal 的核心作用 **ThreadLocal** 是 Java 中用于实现**线程隔离**的类,它为每个线程提供独立的变量副本,解决多线程环境下共享变量的线程安全问题。其核心价值在于: - **避免同步锁竞争**(如 `synchronized` 或 `ReentrantLock`)。 - **隐式传递上下文**(如用户身份、数据库连接)。 - **隔离线程资源**(如 `SimpleDateFormat` 非线程安全类的线程级复用)。 --- ### 二、基础用法(4步流程) #### 1. 创建 ThreadLocal 对象 ```java // 定义 ThreadLocal 变量(一般声明为 static final) private static final ThreadLocal<User> userContext = new ThreadLocal<>(); ``` #### 2. 设置线程专属值 ```java // 当前线程设置值(通常在请求入口处) userContext.set(currentUser); ``` #### 3. 获取线程专属值 ```java // 当前线程获取值(在业务逻辑中随时调用) User user = userContext.get(); ``` #### 4. 清理线程专属值 ```java // 必须显式清理(尤其在线程池场景) userContext.remove(); ``` --- ### 三、典型应用场景 #### 1. 数据库连接管理 ```java private static final ThreadLocal<Connection> connHolder = ThreadLocal.withInitial(() -> { return DriverManager.getConnection(DB_URL); }); // 每个线程从 ThreadLocal 获取独立 Connection public static Connection getConnection() { return connHolder.get(); } ``` #### 2. 用户会话传递 ```java // 在 Web 拦截器中设置用户身份 public class AuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { User user = parseUser(request); UserContextHolder.set(user); // UserContextHolder 封装 ThreadLocal return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { UserContextHolder.remove(); // 防止内存泄漏 } } ``` #### 3. 日期格式化工具 ```java // SimpleDateFormat 非线程安全,通过 ThreadLocal 复用 private static final ThreadLocal<SimpleDateFormat> dateFormat = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd")); public static String format(Date date) { return dateFormat.get().format(date); } ``` --- ### 四、底层原理(简化版) #### 1. 数据结构 - 每个 `Thread` 对象内部维护一个 `ThreadLocalMap`(类似哈希表)。 - `ThreadLocalMap` 的 **Key 是 ThreadLocal 实例**(弱引用),Value 是存储的值(强引用)。 #### 2. 读写流程 - **set()**:向当前线程的 `ThreadLocalMap` 插入键值对。 - **get()**:从当前线程的 `ThreadLocalMap` 根据 Key 查找值。 - **remove()**:删除当前线程的 `ThreadLocalMap` 中对应条目。 #### 3. 内存泄漏问题 - **原因**:若未调用 `remove()`,线程池中的线程存活时间长,导致 Key(弱引用)被回收,但 Value(强引用)无法回收。 - **解决方案**: - 使用完必须调用 `remove()`。 - JDK 1.8 后 `ThreadLocalMap` 使用**弱引用 Key**,但 Value 仍需手动清理。 --- ### 五、高级用法:继承性扩展(InheritableThreadLocal) #### 1. 父子线程传值 ```java // 父线程设置值 InheritableThreadLocal<String> context = new InheritableThreadLocal<>(); context.set("parent-data"); // 子线程可继承父线程的值 new Thread(() -> { System.out.println(context.get()); // 输出 "parent-data" }).start(); ``` #### 2. 局限性 - **线程池场景失效**:线程池复用线程,子任务可能不是创建时的父线程。 - **解决方案**:使用阿里开源的 `TransmittableThreadLocal`(支持线程池上下文传递)。 --- ### 六、注意事项 1. **避免滥用**: - 非必要不通过 ThreadLocal 跨层传递数据(破坏代码可读性)。 - 优先考虑方法参数传递。 2. **内存泄漏预防**: - 线程池任务必须调用 `remove()`(可通过 `try-finally` 保证)。 ```java try { userContext.set(currentUser); // 执行业务逻辑... } finally { userContext.remove(); } ``` 3. **初始值设置**: - 推荐覆盖 `initialValue()` 或使用 `withInitial()`: ```java ThreadLocal<AtomicInteger> counter = ThreadLocal.withInitial(() -> new AtomicInteger(0)); ``` --- ### 七、常见面试问题 #### Q1:ThreadLocal 与 Synchronized 的区别? | **维度** | **ThreadLocal** | **Synchronized** | |----------------|--------------------------------|--------------------------------| | **解决思路** | 空间换时间(线程隔离) | 时间换空间(互斥访问) | | **适用场景** | 线程间数据隔离 | 线程间共享数据同步 | | **性能影响** | 无锁,效率高 | 锁竞争可能导致阻塞 | #### Q2:为什么 ThreadLocalMap 的 Key 是弱引用? - **弱引用 Key**:当 ThreadLocal 实例失去强引用时(如置为 `null`),Key 会被 GC 回收,防止因忘记调用 `remove()` 导致的内存泄漏。 - **残留 Value**:Value 仍是强引用,需手动 `remove()` 清理。 --- ### 八、代码示例完整模板 ```java public class ThreadLocalDemo { // 定义 ThreadLocal 并设置初始值 private static final ThreadLocal<SimpleDateFormat> DATE_FORMATTER = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); public static void main(String[] args) { // 线程1 new Thread(() -> { try { Date date = new Date(); String formattedDate = DATE_FORMATTER.get().format(date); System.out.println("Thread-1: " + formattedDate); } finally { DATE_FORMATTER.remove(); // 清理 } }).start(); // 线程2 new Thread(() -> { try { // 修改日期格式(仅影响当前线程) DATE_FORMATTER.set(new SimpleDateFormat("yyyy/MM/dd")); Date date = new Date(); String formattedDate = DATE_FORMATTER.get().format(date); System.out.println("Thread-2: " + formattedDate); } finally { DATE_FORMATTER.remove(); } }).start(); } } ``` --- ### 九、总结 - **核心价值**:线程级变量隔离,避免锁竞争。 - **关键步骤**:`set()` → `get()` → `remove()`。 - **典型场景**:数据库连接、用户会话、非线程安全工具类。 - **注意事项**:内存泄漏、避免滥用、线程池清理。 - **扩展能力**:通过 `InheritableThreadLocal` 或 `TransmittableThreadLocal` 实现父子线程传值。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值