rokevin
移动
前端
语言
  • 基础

    • Linux
    • 实施
    • 版本构建
  • 应用

    • WEB服务器
    • 数据库
  • 资讯

    • 工具
    • 部署
开放平台
产品设计
  • 人工智能
  • 云计算
计算机
其它
GitHub
移动
前端
语言
  • 基础

    • Linux
    • 实施
    • 版本构建
  • 应用

    • WEB服务器
    • 数据库
  • 资讯

    • 工具
    • 部署
开放平台
产品设计
  • 人工智能
  • 云计算
计算机
其它
GitHub
  • ThreadLocal

  • 面试问题
    • 1. 什么是 ThreadLocal?用来解决什么问题的?
    • 2. 对 ThreadLocal 的理解
    • 3. ThreadLocal 是如何实现线程隔离的?
    • 4. 为什么 ThreadLocal 会造成内存泄露?如何解决?
      • 内存泄露原因:
      • 解决方案:
    • 5. ThreadLocal 的应用场景
  • ThreadLocal简介
  • ThreadLocal理解
  • ThreadLocal原理
    • 如何实现线程隔离
    • ThreadLocalMap对象是什么
  • ThreadLocal 核心作用:线程数据隔离
  • ThreadLocal 底层原理:如何实现线程隔离?
  • 核心方法解析:set ()、get ()、remove ()
    • 1. set (T value):为当前线程设置变量副本
    • 2. get ():获取当前线程的变量副本
    • 3. remove ():删除当前线程的变量副本
  • 典型应用场景
    • 框架底层线程绑定(核心场景)
      • 1. Android Looper/Handler 机制
      • 2. 数据库连接池(如 JDBC)
    • 上下文信息传递(避免参数透传)
      • 1. Web 应用用户会话管理
      • 2. 分布式追踪(Trace ID 传递)
    • 线程私有工具类或配置
      • 1. 线程安全的日期格式化
      • 2. 线程私有配置参数
    • 测试与并发场景隔离
  • ThreadLocal造成内存泄露的问题
  • 再看ThreadLocal应用场景
    • 每个线程维护了一个“序列号”
    • Session的管理
    • 在线程内部创建ThreadLocal
    • java 开发手册中推荐的 ThreadLocal
  • 使用 ThreadLocal 的注意事项
    • 总结
      • 1. 泄漏原因
      • 2. 解决方案
  • ThreadLocal 与 synchronized 的区别
  • 总结
  • 参考

ThreadLocal

ThreadLocal 是 Java 中用于线程本地存储的工具类,它允许每个线程拥有变量的独立副本,从而避免多线程并发访问时的同步问题。在 Android 中,ThreadLocal 被广泛用于底层框架(如 Handler 机制中的 Looper 绑定线程),是理解线程隔离的核心概念。

面试问题

1. 什么是 ThreadLocal?用来解决什么问题的?

ThreadLocal 是 Java 中的一个线程本地存储工具类,它允许每个线程创建并持有变量的独立副本,线程间的变量副本互不干扰。

解决的核心问题:

在多线程环境中,避免共享变量的并发冲突。传统方案通过加锁(如 synchronized)保证线程安全,但会导致性能损耗;而 ThreadLocal 采用 “线程隔离” 思路,让每个线程操作自己的变量副本,无需同步即可保证线程安全,适用于 “线程私有数据” 场景。

2. 对 ThreadLocal 的理解

ThreadLocal 的核心思想是 “线程数据隔离”,它并非解决多线程共享数据的问题,而是让数据成为线程私有,从而天然避免竞争。

  • 设计初衷:为线程提供一个 “私有存储容器”,方便在线程的多个方法间传递数据(如上下文信息),同时避免参数透传导致的代码冗余。
  • 底层依赖:每个 Thread 实例内部有一个 ThreadLocalMap(哈希表),用于存储该线程的所有 ThreadLocal 变量副本,键为 ThreadLocal 对象,值为线程私有变量。
  • 使用原则:适用于 “线程私有且需跨方法访问” 的数据,不适合多线程共享数据场景。

3. ThreadLocal 是如何实现线程隔离的?

ThreadLocal 的隔离能力基于以下底层设计:

  1. Thread 类的 threadLocals 字段

    每个线程(Thread 实例)内部维护一个 ThreadLocalMap 类型的变量 threadLocals,该 map 仅属于当前线程,用于存储线程私有变量。

  2. ThreadLocalMap 的键值对存储

    ThreadLocalMap 是 ThreadLocal 的静态内部类,本质是哈希表,其 Entry 以 ThreadLocal 对象为键、线程私有变量为值。当调用 ThreadLocal.set(value) 时:

    • 先获取当前线程的 ThreadLocalMap;
    • 以当前 ThreadLocal 为键,将 value 存入 map 中。
  3. get () 方法的线程绑定

    调用 ThreadLocal.get() 时,仅从当前线程的 ThreadLocalMap 中查询对应的值,因此只能获取到本线程存入的副本。

简言之:每个线程操作自己的 ThreadLocalMap,不同线程的 map 相互独立,从而实现变量隔离。

4. 为什么 ThreadLocal 会造成内存泄露?如何解决?

内存泄露原因:

  • ThreadLocalMap 中的 Entry 对 ThreadLocal 的引用是弱引用(Entry extends WeakReference<ThreadLocal<?>>),当 ThreadLocal 外部引用被销毁(如 tl = null),GC 会回收 key(弱引用特性)。
  • 但 Entry 的 value(变量副本)是强引用,若线程未结束(如线程池中的核心线程长期存活),value 会被线程的 threadLocals 持续引用,导致 value 无法被 GC 回收,造成内存泄露。

解决方案:

  • 主动调用 remove() 方法:在线程退出前(如方法执行完毕、任务结束时)调用 threadLocal.remove(),清除 ThreadLocalMap 中的 value 引用。
  • 避免使用静态 ThreadLocal 长期持有大对象:静态 ThreadLocal 生命周期与类一致,若绑定到长期存活的线程(如主线程),需特别注意及时清理。

5. ThreadLocal 的应用场景

ThreadLocal 适用于 “线程私有数据” 且 “需跨方法传递” 的场景,典型案例包括:

  1. 框架底层线程绑定
    • Android 的 Looper 通过 ThreadLocal 与线程绑定,确保 “一个线程一个 Looper”,是 Handler 机制的基础。
    • 数据库连接池:每个线程持有独立的 Connection,避免并发操作冲突。
  2. 上下文信息传递
    • Web 应用中存储用户会话(UserSession),在拦截器中设置,在业务逻辑中直接获取,无需参数透传。
    • 分布式追踪:存储全局 Trace ID,方便日志串联整个调用链。
  3. 线程安全的工具类
    • 封装非线程安全的工具(如 SimpleDateFormat),为每个线程创建独立实例,避免格式错误。
  4. 测试与并发场景隔离
    • 单元测试中隔离不同线程的测试数据,避免相互干扰。

ThreadLocal简介

我们在Java 并发 - 并发理论基础总结过线程安全(是指广义上的共享资源访问安全性,因为线程隔离是通过副本保证本线程访问资源安全性,它不保证线程之间还存在共享关系的狭义上的安全性)的解决思路:

  • 互斥同步: synchronized 和 ReentrantLock
  • 非阻塞同步: CAS, AtomicXXXX
  • 无同步方案: 栈封闭,本地存储(Thread Local),可重入代码

本地存储(Thread Local)。官网的解释是这样的:

This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its {@code get} or {@code set} method) has its own, independently initialized copy of the variable. {@code ThreadLocal} instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID) 该类提供了线程局部 (thread-local) 变量。这些变量不同于它们的普通对应物,因为访问某个变量(通过其 get 或 set 方法)的每个线程都有自己的局部变量,它独立于变量的初始化副本。ThreadLocal 实例通常是类中的 private static 字段,它们希望将状态与某一个线程(例如,用户 ID 或事务 ID)相关联。

总结而言:ThreadLocal是一个将在多线程中为每一个线程创建单独的变量副本的类, 当使用ThreadLocal来维护变量时, ThreadLocal会为每个线程创建单独的变量副本, 避免因多线程操作共享变量而导致的数据不一致的情况。

ThreadLocal理解

提到ThreadLocal被提到应用最多的是session管理和数据库链接管理,这里以数据访问为例帮助你理解ThreadLocal:

  • 如下数据库管理类在单线程使用是没有任何问题的
class ConnectionManager {
    private static Connection connect = null;

    public static Connection openConnection() {
        if (connect == null) {
            connect = DriverManager.getConnection();
        }
        return connect;
    }

    public static void closeConnection() {
        if (connect != null)
            connect.close();
    }
}

很显然,在多线程中使用会存在线程安全问题:第一,这里面的2个方法都没有进行同步,很可能在openConnection方法中会多次创建connect;第二,由于connect是共享变量,那么必然在调用connect的地方需要使用到同步来保障线程安全,因为很可能一个线程在使用connect进行数据库操作,而另外一个线程调用closeConnection关闭链接。

  • 为了解决上述线程安全的问题,第一考虑:互斥同步

你可能会说,将这段代码的两个方法进行同步处理,并且在调用connect的地方需要进行同步处理,比如用Synchronized或者ReentrantLock互斥锁。

  • 这里再抛出一个问题:这地方到底需不需要将connect变量进行共享?

事实上,是不需要的。假如每个线程中都有一个connect变量,各个线程之间对connect变量的访问实际上是没有依赖关系的,即一个线程不需要关心其他线程是否对这个connect进行了修改的。即改后的代码可以这样:

class ConnectionManager {
    private Connection connect = null;

    public Connection openConnection() {
        if (connect == null) {
            connect = DriverManager.getConnection();
        }
        return connect;
    }

    public void closeConnection() {
        if (connect != null)
            connect.close();
    }
}

class Dao {
    public void insert() {
        ConnectionManager connectionManager = new ConnectionManager();
        Connection connection = connectionManager.openConnection();

        // 使用connection进行操作

        connectionManager.closeConnection();
    }
}

这样处理确实也没有任何问题,由于每次都是在方法内部创建的连接,那么线程之间自然不存在线程安全问题。但是这样会有一个致命的影响:导致服务器压力非常大,并且严重影响程序执行性能。由于在方法中需要频繁地开启和关闭数据库连接,这样不仅严重影响程序执行效率,还可能导致服务器压力巨大。

  • 这时候ThreadLocal登场了

那么这种情况下使用ThreadLocal是再适合不过的了,因为ThreadLocal在每个线程中对该变量会创建一个副本,即每个线程内部都会有一个该变量,且在线程内部任何地方都可以使用,线程之间互不影响,这样一来就不存在线程安全问题,也不会严重影响程序执行性能。下面就是网上出现最多的例子:

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

public class ConnectionManager {

    private static final ThreadLocal<Connection> dbConnectionLocal = new ThreadLocal<Connection>() {
        @Override
        protected Connection initialValue() {
            try {
                return DriverManager.getConnection("", "", "");
            } catch (SQLException e) {
                e.printStackTrace();
            }
            return null;
        }
    };

    public Connection getConnection() {
        return dbConnectionLocal.get();
    }
}
  • 再注意下ThreadLocal的修饰符

ThreaLocal的JDK文档中说明:ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread。如果我们希望通过某个类将状态(例如用户ID、事务ID)与线程关联起来,那么通常在这个类中定义private static类型的ThreadLocal 实例。

但是要注意,虽然ThreadLocal能够解决上面说的问题,但是由于在每个线程中都创建了副本,所以要考虑它对资源的消耗,比如内存的占用会比不使用ThreadLocal要大。

ThreadLocal原理

如何实现线程隔离

主要是用到了Thread对象中的一个ThreadLocalMap类型的变量threadLocals, 负责存储当前线程的关于Connection的对象, dbConnectionLocal(以上述例子中为例) 这个变量为Key, 以新建的Connection对象为Value; 这样的话, 线程第一次读取的时候如果不存在就会调用ThreadLocal的initialValue方法创建一个Connection对象并且返回;

具体关于为线程分配变量副本的代码如下:

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap threadLocals = getMap(t);
    if (threadLocals != null) {
        ThreadLocalMap.Entry e = threadLocals.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}
  • 首先获取当前线程对象t, 然后从线程t中获取到ThreadLocalMap的成员属性threadLocals
  • 如果当前线程的threadLocals已经初始化(即不为null) 并且存在以当前ThreadLocal对象为Key的值, 则直接返回当前线程要获取的对象(本例中为Connection);
  • 如果当前线程的threadLocals已经初始化(即不为null)但是不存在以当前ThreadLocal对象为Key的的对象, 那么重新创建一个Connection对象, 并且添加到当前线程的threadLocals Map中,并返回
  • 如果当前线程的threadLocals属性还没有被初始化, 则重新创建一个ThreadLocalMap对象, 并且创建一个Connection对象并添加到ThreadLocalMap对象中并返回。

如果存在则直接返回很好理解, 那么对于如何初始化的代码又是怎样的呢?

private T setInitialValue() {
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}
  • 首先调用我们上面写的重载过后的initialValue方法, 产生一个Connection对象
  • 继续查看当前线程的threadLocals是不是空的, 如果ThreadLocalMap已被初始化, 那么直接将产生的对象添加到ThreadLocalMap中, 如果没有初始化, 则创建并添加对象到其中;

同时, ThreadLocal还提供了直接操作Thread对象中的threadLocals的方法

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

这样我们也可以不实现initialValue, 将初始化工作放到DBConnectionFactory的getConnection方法中:

public Connection getConnection() {
    Connection connection = dbConnectionLocal.get();
    if (connection == null) {
        try {
            connection = DriverManager.getConnection("", "", "");
            dbConnectionLocal.set(connection);
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
    return connection;
}

那么我们看过代码之后就很清晰的知道了为什么ThreadLocal能够实现变量的多线程隔离了; 其实就是用了Map的数据结构给当前线程缓存了, 要使用的时候就从本线程的threadLocals对象中获取就可以了, key就是当前线程;

当然了在当前线程下获取当前线程里面的Map里面的对象并操作肯定没有线程并发问题了, 当然能做到变量的线程间隔离了;

现在我们知道了ThreadLocal到底是什么了, 又知道了如何使用ThreadLocal以及其基本实现原理了是不是就可以结束了呢? 其实还有一个问题就是ThreadLocalMap是个什么对象, 为什么要用这个对象呢?

ThreadLocalMap对象是什么

本质上来讲, 它就是一个Map, 但是这个ThreadLocalMap与我们平时见到的Map有点不一样

  • 它没有实现Map接口;
  • 它没有public的方法, 最多有一个default的构造方法, 因为这个ThreadLocalMap的方法仅仅在ThreadLocal类中调用, 属于静态内部类
  • ThreadLocalMap的Entry实现继承了WeakReference<ThreadLocal<?>>
  • 该方法仅仅用了一个Entry数组来存储Key, Value; Entry并不是链表形式, 而是每个bucket里面仅仅放一个Entry;

要了解ThreadLocalMap的实现, 我们先从入口开始, 就是往该Map中添加一个值:

private void set(ThreadLocal<?> key, Object value) {

    // We don't use a fast path as with get() because it is at
    // least as common to use set() to create new entries as
    // it is to replace existing ones, in which case, a fast
    // path would fail more often than not.

    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);

    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();

        if (k == key) {
            e.value = value;
            return;
        }

        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

先进行简单的分析, 对该代码表层意思进行解读:

  • 看下当前threadLocal的在数组中的索引位置 比如: i = 2, 看 i = 2 位置上面的元素(Entry)的Key是否等于threadLocal 这个 Key, 如果等于就很好说了, 直接将该位置上面的Entry的Value替换成最新的就可以了;
  • 如果当前位置上面的 Entry 的 Key为空, 说明ThreadLocal对象已经被回收了, 那么就调用replaceStaleEntry
  • 如果清理完无用条目(ThreadLocal被回收的条目)、并且数组中的数据大小 > 阈值的时候对当前的Table进行重新哈希 所以, 该HashMap是处理冲突检测的机制是向后移位, 清除过期条目 最终找到合适的位置;

了解完Set方法, 后面就是Get方法了:

private Entry getEntry(ThreadLocal<?> key) {
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key)
        return e;
    else
        return getEntryAfterMiss(key, i, e);
}

先找到ThreadLocal的索引位置, 如果索引位置处的entry不为空并且键与threadLocal是同一个对象, 则直接返回; 否则去后面的索引位置继续查找。

ThreadLocal 核心作用:线程数据隔离

在多线程环境中,若多个线程共享一个变量,可能导致数据混乱(如线程 A 修改后被线程 B 覆盖)。此时通常需要加锁(synchronized 或 Lock)保证线程安全,但会牺牲性能。

ThreadLocal 提供了另一种思路:为每个线程创建变量的独立副本,线程只操作自己的副本,无需同步,天然线程安全。

示例场景:

多线程处理用户请求时,每个线程需要存储当前用户 ID,且互不干扰:

// 创建 ThreadLocal 变量(泛型指定存储类型)
private static ThreadLocal<String> currentUserId = new ThreadLocal<>();

// 线程 1 操作
new Thread(() -> {
    currentUserId.set("user_1001"); // 线程 1 的副本
    System.out.println("线程1的用户ID:" + currentUserId.get()); // 输出 user_1001
}).start();

// 线程 2 操作
new Thread(() -> {
    currentUserId.set("user_1002"); // 线程 2 的副本
    System.out.println("线程2的用户ID:" + currentUserId.get()); // 输出 user_1002
}).start();

两个线程操作的是同一个 currentUserId 变量,但各自的 set() 和 get() 互不影响,实现了数据隔离。

ThreadLocal 底层原理:如何实现线程隔离?

ThreadLocal 的隔离能力源于其数据存储结构,核心依赖两个关键类:

  1. Thread 类的 threadLocals 字段

    每个线程(Thread 实例)内部有一个 ThreadLocalMap 类型的变量 threadLocals,用于存储该线程的所有 ThreadLocal 变量副本。

    结构如下(简化):

    public class Thread {
        // 每个线程独有的 ThreadLocalMap,初始为 null
        ThreadLocal.ThreadLocalMap threadLocals = null;
    }
    
  2. ThreadLocalMap:线程内部的哈希表

    ThreadLocalMap 是 ThreadLocal 的静态内部类,本质是一个哈希表(类似 HashMap),用于存储 <ThreadLocal 对象,变量副本> 的键值对。

    其核心是一个 Entry 数组,Entry 以 ThreadLocal 为 key,以变量副本为 value:

    static class ThreadLocalMap {
        private Entry[] table;
    
        // 静态内部类:弱引用包装 ThreadLocal 作为 key
        static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value; // 线程的变量副本
            Entry(ThreadLocal<?> k, Object v) {
                super(k); // key 是弱引用,避免内存泄漏
                value = v;
            }
        }
    }
    

核心方法解析:set ()、get ()、remove ()

ThreadLocal 的核心操作通过以下方法实现,底层都围绕 ThreadLocalMap 展开:

1. set (T value):为当前线程设置变量副本

public void set(T value) {
    // 1. 获取当前线程
    Thread t = Thread.currentThread();
    // 2. 获取线程的 ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        // 3. 若 map 存在,以当前 ThreadLocal 为 key,存储 value
        map.set(this, value);
    } else {
        // 4. 若 map 不存在,为线程创建 map 并初始化
        createMap(t, value);
    }
}
  • 关键逻辑:每个线程的变量副本存储在自己的 ThreadLocalMap 中,key 是当前 ThreadLocal 对象,因此不同线程的副本互不干扰。

2. get ():获取当前线程的变量副本

public T get() {
    // 1. 获取当前线程
    Thread t = Thread.currentThread();
    // 2. 获取线程的 ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        // 3. 以当前 ThreadLocal 为 key,查找对应的 Entry
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result; // 4. 返回当前线程的副本
        }
    }
    // 5. 若 map 不存在或无数据,返回初始值(可通过 initialValue() 自定义)
    return setInitialValue();
}
  • 若线程未调用 set(),get() 会返回 initialValue() 的结果(默认返回 null,可重写该方法自定义初始值)。

3. remove ():删除当前线程的变量副本

public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null) {
        m.remove(this); // 从当前线程的 map 中删除以当前 ThreadLocal 为 key 的 entry
    }
}
  • 必须主动调用 remove() 清理资源,否则可能导致内存泄漏(见下文分析)。

典型应用场景

框架底层线程绑定(核心场景)

1. Android Looper/Handler 机制

Android 中,Looper 通过 ThreadLocal 与线程绑定,确保 “一个线程对应一个 Looper”:

public final class Looper {
    // 用 ThreadLocal 存储当前线程的 Looper
    static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<>();

    // 初始化时将 Looper 存入当前线程的 ThreadLocal
    private static void prepare() {
        sThreadLocal.set(new Looper());
    }

    // 获取当前线程的 Looper
    public static Looper myLooper() {
        return sThreadLocal.get();
    }
}

通过 ThreadLocal,Handler 能确保消息处理在绑定的线程中执行,是线程间通信的基础。

2. 数据库连接池(如 JDBC)

多线程操作数据库时,每个线程需要独立的数据库连接(Connection),避免并发冲突:

java

运行

// 线程私有连接存储
private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() {
    @Override
    protected Connection initialValue() {
        return DriverManager.getConnection(DB_URL); // 为线程创建专属连接
    }
};

// 线程获取自己的连接
public static Connection getConnection() {
    return connectionHolder.get();
}

每个线程操作自己的 Connection,无需考虑同步,简化了连接管理。

上下文信息传递(避免参数透传)

在复杂调用链中(如 Web 请求、分布式追踪),需传递上下文信息(如用户会话、请求 ID),但通过方法参数层层传递会导致代码冗余。ThreadLocal 可简化这类场景:

1. Web 应用用户会话管理

// 存储当前请求的用户上下文
private static ThreadLocal<UserContext> userContextHolder = new ThreadLocal<>();

// 过滤器中设置上下文(请求开始时)
public void doFilter(ServletRequest request, ServletResponse response) {
    UserContext context = extractUserContext(request);
    userContextHolder.set(context); // 绑定到当前线程
    chain.doFilter(request, response);
}

// 业务逻辑中直接获取(无需参数传递)
public void businessService() {
    UserContext currentUser = userContextHolder.get(); // 任意层级可访问
    // 使用用户信息处理业务...
}

// 过滤器收尾时清理(避免线程池复用导致数据残留)
public void destroy() {
    userContextHolder.remove();
}

2. 分布式追踪(Trace ID 传递)

在微服务调用链中,用 ThreadLocal 存储全局 Trace ID,方便日志追踪:

// 存储当前调用链的 Trace ID
private static ThreadLocal<String> traceIdHolder = new ThreadLocal<>();

// 调用开始时生成并设置 Trace ID
public void startTrace() {
    String traceId = generateTraceId();
    traceIdHolder.set(traceId);
}

// 日志打印时自动附加 Trace ID
public void log(String message) {
    String traceId = traceIdHolder.get();
    System.out.println("[" + traceId + "] " + message);
}

线程私有工具类或配置

某些工具类需要为不同线程维护独立状态(如日期格式化、序列化器),避免多线程竞争:

1. 线程安全的日期格式化

SimpleDateFormat 是非线程安全的,多线程共享会导致格式错误。用 ThreadLocal 为每个线程创建独立实例:

// 线程私有 SimpleDateFormat
private static ThreadLocal<SimpleDateFormat> sdfHolder = new ThreadLocal<SimpleDateFormat>() {
    @Override
    protected SimpleDateFormat initialValue() {
        return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    }
};

// 线程安全的格式化方法
public static String format(Date date) {
    return sdfHolder.get().format(date); // 每个线程使用自己的实例
}

2. 线程私有配置参数

在多线程任务中,每个线程可能需要不同的配置(如超时时间、重试次数),通过 ThreadLocal 存储避免混淆:

// 存储当前线程的任务配置
private static ThreadLocal<TaskConfig> taskConfigHolder = new ThreadLocal<>();

// 任务执行前设置配置
public void setTaskConfig(TaskConfig config) {
    taskConfigHolder.set(config);
}

// 任务中获取自己的配置
private void executeTask() {
    TaskConfig config = taskConfigHolder.get();
    // 根据配置执行任务...
}

测试与并发场景隔离

在单元测试或压力测试中,需隔离不同线程的测试数据,避免相互干扰:

// 存储当前线程的测试上下文
private static ThreadLocal<TestContext> testContextHolder = new ThreadLocal<>();

// 每个测试线程初始化自己的上下文
@BeforeEach
void setUp() {
    TestContext context = new TestContext();
    testContextHolder.set(context);
}

// 测试方法中使用线程私有数据
@Test
void testCase() {
    TestContext context = testContextHolder.get();
    // 操作线程私有测试数据...
}

ThreadLocal造成内存泄露的问题

网上有这样一个例子:

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ThreadLocalDemo {
    static class LocalVariable {
        private Long[] a = new Long[1024 * 1024];
    }

    // (1)
    final static ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(5, 5, 1, TimeUnit.MINUTES,
            new LinkedBlockingQueue<>());
    // (2)
    final static ThreadLocal<LocalVariable> localVariable = new ThreadLocal<LocalVariable>();

    public static void main(String[] args) throws InterruptedException {
        // (3)
        Thread.sleep(5000 * 4);
        for (int i = 0; i < 50; ++i) {
            poolExecutor.execute(new Runnable() {
                public void run() {
                    // (4)
                    localVariable.set(new LocalVariable());
                    // (5)
                    System.out.println("use local varaible" + localVariable.get());
                    localVariable.remove();
                }
            });
        }
        // (6)
        System.out.println("pool execute over");
    }
}

如果用线程池来操作ThreadLocal 对象确实会造成内存泄露, 因为对于线程池里面不会销毁的线程, 里面总会存在着<ThreadLocal, LocalVariable>的强引用, 因为final static 修饰的 ThreadLocal 并不会释放, 而ThreadLocalMap 对于 Key 虽然是弱引用, 但是强引用不会释放, 弱引用当然也会一直有值, 同时创建的LocalVariable对象也不会释放, 就造成了内存泄露; 如果LocalVariable对象不是一个大对象的话, 其实泄露的并不严重, 泄露的内存 = 核心线程数 * LocalVariable对象的大小;

所以, 为了避免出现内存泄露的情况, ThreadLocal提供了一个清除线程中对象的方法, 即 remove, 其实内部实现就是调用 ThreadLocalMap 的remove方法:

private void remove(ThreadLocal<?> key) {
    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        if (e.get() == key) {
            e.clear();
            expungeStaleEntry(i);
            return;
        }
    }
}

找到Key对应的Entry, 并且清除Entry的Key(ThreadLocal)置空, 随后清除过期的Entry即可避免内存泄露。

再看ThreadLocal应用场景

除了上述的数据库管理类的例子,我们再看看其它一些应用:

每个线程维护了一个“序列号”

再回想上文说的,如果我们希望通过某个类将状态(例如用户ID、事务ID)与线程关联起来,那么通常在这个类中定义private static类型的ThreadLocal 实例。

每个线程维护了一个“序列号”

public class SerialNum {
    // The next serial number to be assigned
    private static int nextSerialNum = 0;

    private static ThreadLocal serialNum = new ThreadLocal() {
        protected synchronized Object initialValue() {
            return new Integer(nextSerialNum++);
        }
    };

    public static int get() {
        return ((Integer) (serialNum.get())).intValue();
    }
}

Session的管理

经典的另外一个例子:

private static final ThreadLocal threadSession = new ThreadLocal();  
  
public static Session getSession() throws InfrastructureException {  
    Session s = (Session) threadSession.get();  
    try {  
        if (s == null) {  
            s = getSessionFactory().openSession();  
            threadSession.set(s);  
        }  
    } catch (HibernateException ex) {  
        throw new InfrastructureException(ex);  
    }  
    return s;  
}  

在线程内部创建ThreadLocal

还有一种用法是在线程类内部创建ThreadLocal,基本步骤如下:

  • 在多线程的类(如ThreadDemo类)中,创建一个ThreadLocal对象threadXxx,用来保存线程间需要隔离处理的对象xxx。
  • 在ThreadDemo类中,创建一个获取要隔离访问的数据的方法getXxx(),在方法中判断,若ThreadLocal对象为null时候,应该new()一个隔离访问类型的对象,并强制转换为要应用的类型。
  • 在ThreadDemo类的run()方法中,通过调用getXxx()方法获取要操作的数据,这样可以保证每个线程对应一个数据对象,在任何时刻都操作的是这个对象。
public class ThreadLocalTest implements Runnable{
    
    ThreadLocal<Student> StudentThreadLocal = new ThreadLocal<Student>();

    @Override
    public void run() {
        String currentThreadName = Thread.currentThread().getName();
        System.out.println(currentThreadName + " is running...");
        Random random = new Random();
        int age = random.nextInt(100);
        System.out.println(currentThreadName + " is set age: "  + age);
        Student Student = getStudentt(); //通过这个方法,为每个线程都独立的new一个Studentt对象,每个线程的的Studentt对象都可以设置不同的值
        Student.setAge(age);
        System.out.println(currentThreadName + " is first get age: " + Student.getAge());
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println( currentThreadName + " is second get age: " + Student.getAge());
        
    }
    
    private Student getStudentt() {
        Student Student = StudentThreadLocal.get();
        if (null == Student) {
            Student = new Student();
            StudentThreadLocal.set(Student);
        }
        return Student;
    }

    public static void main(String[] args) {
        ThreadLocalTest t = new ThreadLocalTest();
        Thread t1 = new Thread(t,"Thread A");
        Thread t2 = new Thread(t,"Thread B");
        t1.start();
        t2.start();
    }
    
}

class Student{
    int age;
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    
}

java 开发手册中推荐的 ThreadLocal

看看阿里巴巴 java 开发手册中推荐的 ThreadLocal 的用法:

import java.text.DateFormat;
import java.text.SimpleDateFormat;
 
public class DateUtils {
    public static final ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>(){
        @Override
        protected DateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd");
        }
    };
}

然后我们再要用到 DateFormat 对象的地方,这样调用:

DateUtils.df.get().format(new Date());

使用 ThreadLocal 的注意事项

  1. 必须及时清理:线程池中的线程会复用,若不调用 remove(),ThreadLocal 中的数据会残留并被其他任务读取,导致数据混乱。
  2. 避免存储大对象:长期存活的线程(如主线程)持有大对象会导致内存占用过高。
  3. 不适合共享数据:ThreadLocal 用于 “线程私有” 场景,多线程需共享的数据仍需用锁机制。

总结

ThreadLocal 适用于 “线程私有数据” 且 “需在多个方法间传递” 的场景,核心价值是:

  • 简化线程安全代码(无需加锁);
  • 避免参数透传导致的代码冗余;
  • 实现框架级别的线程绑定(如 Looper、连接池)。

实际使用时需平衡便利性与内存管理,尤其注意线程池环境下的 remove() 调用。五、内存泄漏风险与解决方案

ThreadLocal 若使用不当,可能导致内存泄漏,需重点关注:

1. 泄漏原因

  • ThreadLocalMap 中的 Entry 以 弱引用 存储 ThreadLocal 作为 key:当 ThreadLocal 外部引用被销毁(如 tl = null),GC 会回收 key(弱引用特性)。
  • 但 Entry 的 value(变量副本)是强引用,若线程未结束(如线程池中的核心线程),value 会一直被线程的 threadLocals 引用,导致内存泄漏。

2. 解决方案

  • 主动调用 remove():在线程退出前(如方法执行完毕、拦截器收尾时)调用 threadLocal.remove(),清除 value 引用。
  • 避免使用静态 ThreadLocal 长期持有大对象:静态 ThreadLocal 生命周期与类一致,若绑定到长期存活的线程(如主线程),需特别注意及时清理。

ThreadLocal 与 synchronized 的区别

特性ThreadLocalsynchronized
核心思想每个线程存储变量副本,避免共享通过加锁让多线程排队访问共享变量
线程安全实现方式无锁,线程隔离有锁,同步互斥
性能无锁竞争,性能好可能存在锁竞争,性能较低(尤其是高并发)
适用场景线程私有数据(如上下文、连接)多线程共享数据(如计数器、缓存)

总结

ThreadLocal 是实现线程数据隔离的高效工具,其核心原理是通过每个线程的 ThreadLocalMap 存储变量副本,避免多线程共享带来的同步问题。在 Android 中,它是 Handler 机制的底层支撑(Looper 线程绑定),也是解决线程安全问题的重要方案。

使用时需注意:

  • 及时调用 remove() 避免内存泄漏;
  • 明确 ThreadLocal 适用于 “线程私有数据”,而非 “多线程共享数据”;
  • 理解其与锁机制的本质区别,根据场景选择合适的线程安全方案。

参考

  • https://blog.csdn.net/vking_wang/article/details/14225379
  • https://mp.weixin.qq.com/s/mo3-y-45_ao54b5T7ez7iA
  • https://www.xttblog.com/?p=3087
  • https://blog.csdn.net/whut2010hj/article/details/81413887
  • https://segmentfault.com/a/1190000018399795
  • 彻底理解ThreadLocal
最近更新:: 2025/10/22 15:36
Contributors: luokaiwen