了解ThreadLocal,这一篇文章就够了

2023年 9月 5日 13.6k 0

作者 | 蔡柱梁

审校 | 重楼

一、前言

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

二、ThreadLocal 是用来干什么的

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

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

  • 使用 zk 实现选举,采用单例 zkClient,但是对于里面一些全局变量就会存在线程安全问题,这时会希望这些特定的全局变量可以跟线程绑定。
  • 项目UUC(统一认证中心),不同的用户登录,系统是如何确保当前用户的信息不会被张冠李戴的呢?其实都是通过 ThreadLocal 实现的(不过在 UUC 中,笔者使用的是 InheritableThreadLocal,这个会有点区别)。
  • 参数传递,比如流水生成的方法里面的重试机制,假设限制重试 5 次,生成流水号的方法内部很多地方都可能失败需要重试(并发冲突或者 db 异常),最传统的方式就是将重试的次数传递。这种方式不够优雅,我们可以使用 ThreadLocal 来实现传递。
  • 总的来说,当你需要和线程绑定的变量时,就可以考虑使用 ThreadLocal 啦!

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

    三、ThreadLocal 如何使用

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

  • 及时清理
  • 确保在线程结束时,及时清理 ThreadLocal 中存储的数据。可以通过在使用完 ThreadLocal 后调用 remove() 方法来清理对应的数据。例如,可以使用 ThreadLocal.remove() 或在 finally 块中进行清理操作。
  • 使用弱引用(WeakReference)
  • 可以使用 ThreadLocal 的变体,如 InheritableThreadLocal 或 WeakThreadLocal,它们使用了弱引用来存储数据。这样,在没有其他强引用指向被存储的对象时,垃圾回收器可以自动清理该对象,避免内存泄漏。
  • 避免长时间存储大量数据
  • 尽量避免在 ThreadLocal 中存储大量数据,特别是对于长时间运行的线程。因为 ThreadLocal 的值在线程的整个生命周期中都存在,如果存储大量数据,可能会导致内存占用过高。
  • 及时释放资源
  • 如果你在 ThreadLocal 中存储了需要手动释放的资源,确保在不再需要时及时释放资源。可以通过在使用完资源后显式地调用资源的释放方法或使用 try-with-resources 语句来实现。
  • 防止线程池中的内存泄漏
  • 当使用线程池时,要特别小心使用 ThreadLocal。确保在任务完成后清理 ThreadLocal 中的数据,以避免线程重用时的数据干扰和潜在的内存泄漏问题。可以在任务的开始和结束处使用 ThreadLocal 进行数据绑定和解绑。
  • 总之,要正确使用 ThreadLocal 并避免内存泄漏问题,需要注意适时清理、使用弱引用、避免存储过多数据、及时释放资源,并在使用线程池时特别小心。

    四、ThreadLocal 的实现原理

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

    public class ThreadLocalExample {
     private static final ThreadLocal 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

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论