写点什么

入坑 ThreadLocal,这一篇文章就够了

  • 2023-09-04
    福建
  • 本文字数:3311 字

    阅读完需:约 11 分钟

入坑ThreadLocal,这一篇文章就够了

一、前言


很多 Java 开发一般都是做中台较多,并发编程使用的不多。因此,对 ThreadLocal 不太熟悉,所以笔者这里想让大家了解它,知道它是用来干什么的。

二、ThreadLocal 是用来干什么的


ThreadLocal 是 Java 中一种线程封闭技术,它提供了一种线程本地变量的机制,使得每个线程都拥有一个独立的变量副本,这样可以避免多个线程访问同一个变量时产生的并发问题。


ThreadLocal 在工作中还是蛮常用的,笔者使用到的一些场景如下:

  1. 使用 zk 实现选举,采用单例 zkClient,但是对于里面一些全局变量就会存在线程安全问题,这时会希望这些特定的全局变量可以跟线程绑定。

  2. 项目 UUC(统一认证中心),不同的用户登录,系统是如何确保当前用户的信息不会被张冠李戴的呢?其实都是通过 ThreadLocal 实现的(不过在 UUC 中,笔者使用的是 InheritableThreadLocal,这个会有点区别)。

  3. 参数传递,比如流水生成的方法里面的重试机制,假设限制重试 5 次,生成流水号的方法内部很多地方都可能失败需要重试(并发冲突或者 db 异常),最传统的方式就是将重试的次数传递。这种方式不够优雅,我们可以使用 ThreadLocal 来实现传递。


总的来说,当你需要和线程绑定的变量时,就可以考虑使用 ThreadLocal 啦!


至于线程安全问题,大家不妨想想我们平常说线程安全问题都是出现在什么场景?同一时间有两个或两个以上的线程对同一个变量进行修改,才有可能出现线程安全问题。但是使用 ThreadLocal,每个线程是独享自己的变量副本的,哪里还有线程安全问题呢?

三、ThreadLocal 如何使用


这个上网一搜一大堆,笔者就说下注意事项好了,用完后一定要释放,避免内存泄漏,提供几个点给大家参考:


  1. 及时清理:确保在线程结束时,及时清理 ThreadLocal 中存储的数据。可以通过在使用完 ThreadLocal 后调用 remove() 方法来清理对应的数据。例如,可以使用 ThreadLocal.remove() 或在 finally 块中进行清理操作。

  2. 使用弱引用(WeakReference):可以使用 ThreadLocal 的变体,如 InheritableThreadLocal 或 WeakThreadLocal,它们使用了弱引用来存储数据。这样,在没有其他强引用指向被存储的对象时,垃圾回收器可以自动清理该对象,避免内存泄漏。

  3. 避免长时间存储大量数据:尽量避免在 ThreadLocal 中存储大量数据,特别是对于长时间运行的线程。因为 ThreadLocal 的值在线程的整个生命周期中都存在,如果存储大量数据,可能会导致内存占用过高。

  4. 及时释放资源:如果你在 ThreadLocal 中存储了需要手动释放的资源,确保在不再需要时及时释放资源。可以通过在使用完资源后显式地调用资源的释放方法或使用 try-with-resources 语句来实现。

  5. 防止线程池中的内存泄漏:当使用线程池时,要特别小心使用 ThreadLocal。确保在任务完成后清理 ThreadLocal 中的数据,以避免线程重用时的数据干扰和潜在的内存泄漏问题。可以在任务的开始和结束处使用 ThreadLocal 进行数据绑定和解绑。


总之,要正确使用 ThreadLocal 并避免内存泄漏问题,需要注意适时清理、使用弱引用、避免存储过多数据、及时释放资源,并在使用线程池时特别小心。

四、ThreadLocal 的实现原理


下面是一个简单的示例代码:

public class ThreadLocalExample { private static final ThreadLocal<Object> threadLocal = new ThreadLocal<>();
public static void main(String[] args) { Thread workerThread = new Thread(() -> { try { // 在线程中设置ThreadLocal值 threadLocal.set(new Object());
// 执行业务逻辑 // ...
} finally { // 在线程结束时清理ThreadLocal值 threadLocal.remove(); } });
workerThread.start(); // 等待线程结束 try { workerThread.join(); } catch (InterruptedException e) { e.printStackTrace(); } }}
复制代码


在示例代码中,线程 workerThread 和 ThreadLocal 实例是一个怎样的关系呢?set 方法和 remove 方法都做了什么呢?为什么会有内存泄漏的情况呢?我们带着疑问一起往下看。

4.1 java.lang.ThreadLocal#set


我们直接从源码开始分析 ThreadLocal。


public void set(T value) { // 获取当前线程 Thread t = Thread.currentThread(); // 通过当前线程获取ThreadLocalMap  ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else  createMap(t, value); }
ThreadLocalMap getMap(Thread t) { return t.threadLocals; }
void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) { table = new Entry[INITIAL_CAPACITY]; int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); table[i] = new Entry(firstKey, firstValue); size = 1; setThreshold(INITIAL_CAPACITY); }
static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value;
Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }
复制代码


结合示例代码来看,这里是当前线程 A 在 main 方法中通过 threadLocal 实例调用 threadLocal.set 方法,而 set 方法会给当前线程创建一个 ThreadLocalMap(如果没有的话),并使用 threadLocal 实例作为 key。


它们的关系如下图:

4.2 内存泄漏问题


这里应该分成两种情况看:无线程复用和有线程复用。


  1. 无线程复用

当 workerThread 结束后,没有强引用的 ThreadLocalMap 自然而然也会被垃圾回收器回收,不会出现内存泄漏。


  1. 有线程复用

这里也要分开看,有释放和无释放的情况。如果发生内存泄漏,当然就是我们没有释放导致的(释放可以通过调用 set、get、remove 方法释放)。当我们使用线程池,线程会被复用时,ThreadLocalMap 的生命周期与它绑定的线程是一样的,所以不会被回收。如果这时发生了 gc,那么 Entry 的 key 是弱引用,key 会变成 null,而 value 将继续存活。如果该线程一直不调用 set/get/remove 方法,那么 value 一直得不到释放,就会发生内存泄漏的现象。


那为什么使用 set/get/remove 可以避免内存泄漏呢?因为 set/get 在根据当前线程找到对应 Entry 元素后(这里是刚好是碰到了 key==null 的 entry[i],碰不到是不会顺手释放旧 value 的。因此,最好还是使用完后调用 remove 释放),发现 key == null,就会调用 java.lang.ThreadLocal.ThreadLocalMap#expungeStaleEntry 释放引用,所以就不会发生内存泄漏了。这里就不再展示源码了,有兴趣的可以自己去看下。

五、哈希冲突问题


上面看到 ThreadLocalMap 使用了 Hash,是不是马上就想到了哈希冲突呢?HashMap 遇到哈希冲突,在 key 不相同的情况下,会使用链表解决。但是 ThreadLocalMap 的 Entry 没有 next 指针,因此它明显不会采用链表,那么它是如何解决哈希冲突的呢?


请看 java.lang.ThreadLocal.ThreadLocalMap#set 源码,笔者添加了注释,可以看到是怎么解决哈希冲突的。


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; // 存在哈希冲突的话,会往下走,如果超过数组长度,就会回到0 e = tab[i = nextIndex(i, len)]) { ThreadLocal<?> k = e.get();
if (k == key) { // 找到存储自己的entry,更新value e.value = value; return; }
if (k == null) { // 因为 gc 导致 key 被回收了,这个 Entry 会被新的 Entry 取代(新的Entry的key和value就是这里的传参),旧的会被释放 replaceStaleEntry(key, value, i); return; } }
tab[i] = new Entry(key, value); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }
复制代码

总结

到这里相信大家对 ThreadLocal 都有了一定的了解。有什么想交流可以留言或私信笔者。

发布于: 刚刚阅读数: 6
用户头像

IT领域从业者 分享见解 2023-06-19 加入

还未添加个人简介

评论

发布
暂无评论
入坑ThreadLocal,这一篇文章就够了_Java_树上有只程序猿_InfoQ写作社区