ThreadLocal总结
ThreadLocal的作用
ThreadLocal 是 Java 中提供的一种用于实现线程局部变量的机制。它的主要作用是:ThreadLocal 的核心作用是为每个线程提供独立的变量副本,实现线程间的数据隔离,保证线程安全,简化多线程编程。
具体作用包括:
- 线程隔离
ThreadLocal 为每个线程维护一份独立的变量副本,线程之间互不干扰,避免了多线程访问同一变量时的冲突和数据不一致。 - 简化线程安全编程
通过使用 ThreadLocal,可以避免使用同步锁(synchronized)来保护共享变量,从而简化代码并提高性能。 - 适用于线程相关的上下文信息存储
例如,用户会话信息、数据库连接、事务信息等,可以存储在线程的局部变量中,方便在同一线程的不同方法间共享数据。 - 避免参数传递
在复杂调用链中,可以通过 ThreadLocal 传递数据,避免在方法间显式传递参数。
ThreadLocal的使用场景举例
ThreadLocal 适用于需要在同一线程内共享数据但又避免线程间共享冲突的场景,常见于用户信息存储、数据库连接管理、事务控制、格式化工具缓存和日志追踪等。
以下是一些常见的 ThreadLocal 使用示例,涵盖不同场景
使用场景 | 说明 | 示例简述 |
---|---|---|
1. 线程安全的变量存储 | 为每个线程保存独立变量,避免多线程共享冲突 | 保存用户ID、请求ID等线程相关信息 |
2. 数据库连接管理 | 每个线程维护独立的数据库连接,避免多线程共享连接导致冲突 | 线程独立持有JDBC Connection,保证线程安全 |
3. 事务管理 | 线程内共享事务状态,方便事务的开启、提交和回滚 | 线程内保存事务对象,跨方法调用共享事务 |
4. 格式化工具缓存 | 线程安全的日期格式化工具,避免多线程环境下SimpleDateFormat的线程安全问题 | 每个线程持有独立的SimpleDateFormat实例 |
5. 用户会话信息存储 | 在线程中保存当前用户信息,方便业务逻辑中随时获取 | Web请求线程中保存当前登录用户信息 |
6. 日志追踪ID | 线程内保存唯一请求ID,方便日志追踪和关联 | 生成并保存请求唯一ID,日志打印时自动带 |
- 线程安全的变量存储
private static ThreadLocal<String> userIdThreadLocal = new ThreadLocal<>();public void setUserId(String userId) {userIdThreadLocal.set(userId);
}public String getUserId() {return userIdThreadLocal.get();
}
- 数据库连接管理
private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<>();public Connection getConnection() {Connection conn = connectionHolder.get();if (conn == null) {conn = createNewConnection();connectionHolder.set(conn);}return conn;
}
- 事务管理
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();}
}
- 格式化工具缓存
private static ThreadLocal<SimpleDateFormat> dateFormatHolder = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));public String formatDate(Date date) {return dateFormatHolder.get().format(date);
}
- 用户会话信息存储
private static ThreadLocal<User> currentUser = new ThreadLocal<>();public void setCurrentUser(User user) {currentUser.set(user);
}public User getCurrentUser() {return currentUser.get();
}
- 日志追踪ID
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等主要用于搜索定位数据
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 codeprivate static int nextHashCode() {return nextHashCode.getAndAdd(HASH_INCREMENT);}// 初始化,实际是新建了一个SuppliedThreadLocalpublic static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {return new SuppliedThreadLocal<>(supplier);}// 获取值public T get() {Thread t = Thread.currentThread();// 获取通过thread获取ThreadLocalMapThreadLocalMap map = getMap(t);if (map != null) {// 获取entryThreadLocalMap.Entry e = map.getEntry(this);if (e != null) {@SuppressWarnings("unchecked")T result = (T)e.value;// 返回值return result;}}// 为空的时候初始化一个ThreadLocalMapreturn setInitialValue();}// 设置值public void set(T value) {Thread t = Thread.currentThread();// 获取通过thread获取ThreadLocalMapThreadLocalMap map = getMap(t);if (map != null)// 更新值map.set(this, value);else // 为空时创建ThreadLocalMapcreateMap(t, value);}// 删除值public void remove() {ThreadLocalMap m = getMap(Thread.currentThread());if (m != null)m.remove(this);}// 获取ThreadLocalMapThreadLocalMap getMap(Thread t) {return t.threadLocals;}}
ThreadLocalMap是ThreadLocal实现功能的主要内部类,他使用Entry数组维护线程数据,Entry是一个弱引用的对象,他是一个Map结构,以ThreadLocal为key,Object为value。
ThreadLocalMap的源码
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获取Entryprivate Entry getEntry(ThreadLocal<?> key) {// 根据key获得索引iint i = key.threadLocalHashCode & (table.length - 1);// 根据i获取EntryEntry 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为空时,删除过期的EntryexpungeStaleEntry(i);else // k不为空时,迭代下一个索引ii = 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) { // 存在就更新valuee.value = value;return;}if (k == null) { // 不存在,替换老的EntryreplaceStaleEntry(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的原因
- ThreadLocalMap 的设计
每个线程内部维护一个ThreadLocalMap
,用来存储该线程所有的 ThreadLocal 变量及其对应的值。这个ThreadLocalMap
的 key 是弱引用(WeakReference)指向 ThreadLocal 对象,value 是强引用指向实际的值。 - 弱引用的key被回收,但value未被清理
当 ThreadLocal 对象不再被外部引用时,弱引用的 key 会被垃圾回收,但对应的 value(实际存储的数据)仍然是强引用,不会自动被回收。这就导致ThreadLocalMap
中残留了“key为null,value不为null”的条目。 - 残留的value导致内存泄漏
这些残留的 value 无法被清理,且随着线程长时间存活(如线程池中的线程),这些无用的对象会一直占用内存,最终可能导致内存溢出。 - 线程池环境下更易发生
在线程池中,线程是复用的,ThreadLocalMap 也会一直存在,如果不手动清理 ThreadLocal,残留的无用数据会不断累积,增加OOM风险。
如何避免 ThreadLocal 导致的OOM?
- 使用完毕后调用
ThreadLocal.remove()
主动清理当前线程中对应的 ThreadLocal 变量,避免残留。 - 避免长时间持有大对象
ThreadLocal 中存储的数据尽量避免占用大量内存,或者及时清理。 - 谨慎使用 ThreadLocal,尤其在线程池环境中
线程池线程复用时,ThreadLocal 的清理尤为重要。