ThreadLocal总结

ThreadLocal的作用

ThreadLocal 是 Java 中提供的一种用于实现线程局部变量的机制。它的主要作用是:ThreadLocal 的核心作用是为每个线程提供独立的变量副本,实现线程间的数据隔离,保证线程安全,简化多线程编程。

具体作用包括:

  1. 线程隔离
    ThreadLocal 为每个线程维护一份独立的变量副本,线程之间互不干扰,避免了多线程访问同一变量时的冲突和数据不一致。
  2. 简化线程安全编程
    通过使用 ThreadLocal,可以避免使用同步锁(synchronized)来保护共享变量,从而简化代码并提高性能。
  3. 适用于线程相关的上下文信息存储
    例如,用户会话信息、数据库连接、事务信息等,可以存储在线程的局部变量中,方便在同一线程的不同方法间共享数据。
  4. 避免参数传递
    在复杂调用链中,可以通过 ThreadLocal 传递数据,避免在方法间显式传递参数。

ThreadLocal的使用场景举例

ThreadLocal 适用于需要在同一线程内共享数据但又避免线程间共享冲突的场景,常见于用户信息存储、数据库连接管理、事务控制、格式化工具缓存和日志追踪等。

以下是一些常见的 ThreadLocal 使用示例,涵盖不同场景

使用场景 说明 示例简述
1. 线程安全的变量存储 为每个线程保存独立变量,避免多线程共享冲突 保存用户ID、请求ID等线程相关信息
2. 数据库连接管理 每个线程维护独立的数据库连接,避免多线程共享连接导致冲突 线程独立持有JDBC Connection,保证线程安全
3. 事务管理 线程内共享事务状态,方便事务的开启、提交和回滚 线程内保存事务对象,跨方法调用共享事务
4. 格式化工具缓存 线程安全的日期格式化工具,避免多线程环境下SimpleDateFormat的线程安全问题 每个线程持有独立的SimpleDateFormat实例
5. 用户会话信息存储 在线程中保存当前用户信息,方便业务逻辑中随时获取 Web请求线程中保存当前登录用户信息
6. 日志追踪ID 线程内保存唯一请求ID,方便日志追踪和关联 生成并保存请求唯一ID,日志打印时自动带
  1. 线程安全的变量存储
java 复制代码
private static ThreadLocal<String> userIdThreadLocal = new ThreadLocal<>();

public void setUserId(String userId) {
    userIdThreadLocal.set(userId);
}

public String getUserId() {
    return userIdThreadLocal.get();
}
  1. 数据库连接管理
java 复制代码
private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<>();

public Connection getConnection() {
    Connection conn = connectionHolder.get();
    if (conn == null) {
        conn = createNewConnection();
        connectionHolder.set(conn);
    }
    return conn;
}
  1. 事务管理
java 复制代码
private static ThreadLocal<Transaction> transactionHolder = new ThreadLocal<>();

public void beginTransaction() {
    Transaction tx = new Transaction();
    transactionHolder.set(tx);
    tx.begin();
}

public void commitTransaction() {
    Transaction tx = transactionHolder.get();
    if (tx != null) {
        tx.commit();
        transactionHolder.remove();
    }
}
  1. 格式化工具缓存
java 复制代码
private static ThreadLocal<SimpleDateFormat> dateFormatHolder = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));

public String formatDate(Date date) {
    return dateFormatHolder.get().format(date);
}
  1. 用户会话信息存储
java 复制代码
private static ThreadLocal<User> currentUser = new ThreadLocal<>();

public void setCurrentUser(User user) {
    currentUser.set(user);
}

public User getCurrentUser() {
    return currentUser.get();
}
  1. 日志追踪ID
java 复制代码
private static ThreadLocal<String> traceId = new ThreadLocal<>();

public void setTraceId(String id) {
    traceId.set(id);
}

public String getTraceId() {
    return traceId.get();
}

ThreadLocal源码走读

ThreadLocal的类关系图如下,他主要是通过内部的ThreadLocalMap实现数据的线程隔离

ThreadLocal源码中提供了共有的接口set、get、remove等,私有的字段threadLocalHashCode、nextHashCode等主要用于搜索定位数据

java 复制代码
public class ThreadLocal<T> {
    // ThreadLocal对象作为key, 通过threadLocalHashCode进行搜索。
    private final int threadLocalHashCode = nextHashCode();

    // 下一个给出的hashcode。自动更新,从0开始
    private static AtomicInteger nextHashCode = new AtomicInteger();

    //连续生成的哈希码之间的差异:将隐式顺序线程本地 ID 转换为接近最优分布的乘法哈希值,适用于 2 的幂大小的表。
    private static final int HASH_INCREMENT = 0x61c88647;
    // 返回下一个hash code
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }
    
    // 初始化,实际是新建了一个SuppliedThreadLocal
    public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
        return new SuppliedThreadLocal<>(supplier);
    }
    
     // 获取值
     public T get() {
        Thread t = Thread.currentThread();
         // 获取通过thread获取ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            // 获取entry
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                // 返回值
                return result;
            }
        }
         // 为空的时候初始化一个ThreadLocalMap
        return setInitialValue();
    }
    
    // 设置值
    public void set(T value) {
        Thread t = Thread.currentThread();
         // 获取通过thread获取ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        if (map != null)
            // 更新值
            map.set(this, value);
        else // 为空时创建ThreadLocalMap
            createMap(t, value);
    }
    
    // 删除值
     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }
    
    // 获取ThreadLocalMap
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
    
}

ThreadLocalMap是ThreadLocal实现功能的主要内部类,他使用Entry数组维护线程数据,Entry是一个弱引用的对象,他是一个Map结构,以ThreadLocal为key,Object为value。

ThreadLocalMap的源码

java 复制代码
static class ThreadLocalMap {

       // <key,value> = <ThreadLocal<?>, Object> , 如果key=null,表明key不再被引用,就可以从table表中删除了
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** 关联到ThreadLocal的值 */
            Object value;
            
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

        /**
         * 初始容量大小。必须是2的幂次方
         */
        private static final int INITIAL_CAPACITY = 16;

        /**
         * Entry表table
         */
        private Entry[] table;

        /**
         * table中元素的数量
         */
        private int size = 0;

        /**
         * 下一个扩容的值
         */
        private int threshold; 

        /**
         * 索引i增加
         */
        private static int nextIndex(int i, int len) {
            return ((i + 1 < len) ? i + 1 : 0);
        }

        /**
         * 索引i减少
         */
        private static int prevIndex(int i, int len) {
            return ((i - 1 >= 0) ? i - 1 : len - 1);
        }

        // 根据key获取Entry
        private Entry getEntry(ThreadLocal<?> key) {
            // 根据key获得索引i
            int i = key.threadLocalHashCode & (table.length - 1);
            // 根据i获取Entry
            Entry e = table[i];
            if (e != null && e.get() == key) // 不为空时直接返回
                return e;
            else // 
                return getEntryAfterMiss(key, i, e);
        }

        /**
         * 循环搜索Entry
         */
        private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
            Entry[] tab = table;
            int len = tab.length;

            while (e != null) {
                ThreadLocal<?> k = e.get();
                if (k == key)
                    return e;
                if (k == null) // k为空时,删除过期的Entry
                    expungeStaleEntry(i);
                else // k不为空时,迭代下一个索引i
                    i = nextIndex(i, len);
                e = tab[i]; // 更新e
            }
            return null;
        }

        /**
         * 设置值
         */
        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]; e != null; e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();

                if (k == key) { // 存在就更新value
                    e.value = value;
                    return;
                }

                if (k == null) { // 不存在,替换老的Entry
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

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

        /**
         * 删除Entry
         */
        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;
                }
            }
        }

         // 其他省略。。。。
    }

ThreadLocal为什么使用不当会发生OOM?

发生OOM的原因

  1. ThreadLocalMap 的设计
    每个线程内部维护一个 ThreadLocalMap,用来存储该线程所有的 ThreadLocal 变量及其对应的值。这个 ThreadLocalMap 的 key 是弱引用(WeakReference)指向 ThreadLocal 对象,value 是强引用指向实际的值。
  2. 弱引用的key被回收,但value未被清理
    当 ThreadLocal 对象不再被外部引用时,弱引用的 key 会被垃圾回收,但对应的 value(实际存储的数据)仍然是强引用,不会自动被回收 。这就导致 ThreadLocalMap 中残留了"key为null,value不为null"的条目。
  3. 残留的value导致内存泄漏
    这些残留的 value 无法被清理,且随着线程长时间存活(如线程池中的线程),这些无用的对象会一直占用内存,最终可能导致内存溢出。
  4. 线程池环境下更易发生
    在线程池中,线程是复用的,ThreadLocalMap 也会一直存在,如果不手动清理 ThreadLocal,残留的无用数据会不断累积,增加OOM风险。

如何避免 ThreadLocal 导致的OOM?

  • 使用完毕后调用 ThreadLocal.remove()
    主动清理当前线程中对应的 ThreadLocal 变量,避免残留。
  • 避免长时间持有大对象
    ThreadLocal 中存储的数据尽量避免占用大量内存,或者及时清理。
  • 谨慎使用 ThreadLocal,尤其在线程池环境中
    线程池线程复用时,ThreadLocal 的清理尤为重要。
相关推荐
OceanBase数据库官方博客3 小时前
当过滤条件不符合最左前缀时,如何有效利用索引? | OceanBase SQL 优化实践
sql·性能优化·oceanbase·分布式数据库
鼠鼠我捏,要死了捏3 小时前
Spark Shuffle性能优化实践指南:提升大数据处理效率
性能优化·spark·shuffle
诗和远方14939562327341 天前
Matrix内存溢出(OOM)监控机制分析
性能优化
程序视点1 天前
2025最佳Windows优化工具推荐:292KB小工具解决Win11右键菜单/自动更新/Defender等12大痛点
windows·性能优化
林开落L1 天前
进程控制:从创建到终结的完整指南
linux·性能优化·进程控制
OEC小胖胖2 天前
性能优化(一):时间分片(Time Slicing):让你的应用在高负载下“永不卡顿”的秘密
前端·javascript·性能优化·web
CodeShare2 天前
Windows 11任务管理器CPU计算逻辑优化
性能优化·操作系统
程序员编程指南2 天前
Qt 移动应用性能优化策略
c语言·开发语言·c++·qt·性能优化
鼠鼠我捏,要死了捏2 天前
MySQL 索引设计与查询性能优化实践指南
数据库·mysql·性能优化