彻底理解 ThreadLocal

彻底理解 ThreadLocal

  • 本篇文章主要从一下几个角度来分析 ThreadLocal

1、ThreadLocal 的基本用途
2、ThreadLocal 的具体实现
3、ThreadLocal 使用时的注意事项


ThreadLocal 的基本用途

1、线程隔离

ThreadLocal的主要价值在于线程隔离,ThreadLocal中的数据只属于当前线程,其本地值对别的线程是不可见的,在多线程环境下,可以防止自己的变量被其他线程篡改。另外,由于各个线程
之间的数据相互隔离,避免了同步加锁带来的性能损失,大大提升了并发性的性能。

  • 使用场景 :数据库连接独享、Session 数据管理

2、跨函数传递数据

通常用于同一个线程内,跨类、跨方法传递数据时,如果不用 ThreadLocal,那么相互之间的数据传递势必要靠返回值和参数,这样无形之中增加了这些类或者方法之间的耦合度。由于 ThreadLocal 的特性,同一线程在某些地方进行设置,在随后的任意地方都可以获取到。线程执行过程中所执行到的函数都能读写 ThreadLocal 变量的线程本地值,从而可以方便地实现跨函数的数据传递。使用 ThreadLocal 保存函数之间需要传递的数据,在需要的地方直接获取,也能避免通过参数传递数据带来的高耦合。

  • 使用场景:每个线程绑定一个 Session(用户会话)信息,这样一个线程所有调用到的代码都可以非常方便地访问这个本地会话,而不需要通过参数传递。

ThreadLocal 的具体实现

  • 下面我们主要从以下三个属性来具体分析

1、ThreadLocalMap
2、get()
3、set(T value)


ThreadLocalMap

  • 1、从上图中我们可以看到 ThreadLocalMap 类成员属性中有一个 Entry 的静态内部类,这个类就是我们 TheadLocalMap 用来存储数据的节点 Node
    2、同时 Entry 继承了 WeakReference,并使用 WeakReference 对 key 进行了包装

  • 知识 : 为什么 ThreadLocalMap 的 key 节点要使用 WeakReference 进行包装

    void fn() {
        ThreadLocal<String> local = new ThreadLocal<>();

        local.set("123");

        local.get();
    }
  • 代码内存结构

1、当线程执行完fn()方法后,fn()的方法栈帧将被销毁,强引用local的值也就没有了,但此时线程的 ThreadLocalMap 中对应的Entry的Key引用还指向了 ThreadLocal 实例。如果Entry的Key引用是强引用,就会导致Key引用指向的 ThreadLocal 实例及其Value值都不能被GC回收,这将造成严重的内存泄漏.

  • 当 Key 为强引用时

get()

  • 代码
    public T get() {
        // 获取当前线程
        Thread t = Thread.currentThread();
        // 获取当前线程持有的 ThreadLocalMap 
        ThreadLocalMap map = getMap(t);
        if (map != null) {
             // 根据 key 获取 value
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                // 类型转换
                T result = (T)e.value;
                return result;
            }
        }
        // 数据为空进行初始化防止 NPE
        return setInitialValue();
    }

1、通过 ThreadLocal 的 get 方法我们可以看出,子线程想获取到父线程的 ThreadLocal 的数据是获取不到的,因为在 get 时是获取的父现场的的 Thread。
2、我们可以通过 InheritableThreadLocal 解决上面的问题


set()

  • 代码
    public void set(T value) {
        // 获取当前线程
        Thread t = Thread.currentThread();
       // 获取当前线程持有的 ThreadLocalMap 
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            // 设置数据 key = 当前的 ThreadLocal , v = value
            map.set(this, value);
        } else {
            // 创建 ThreadLocalMap 并设置值
            createMap(t, value);
        }
    }

ThreadLocal 使用时的注意事项

  • 注意事项

1、尽量使用private static final修饰 ThreadLocal 实例。使用 private 与final修饰符主要是尽可能不让他人修改、变更 ThreadLocal 变量的引用,使用static修饰符主要为了确保 ThreadLocal 实例的全
局唯一。

private static final ThreadLocal<?> local = ThreadLocal.withInitial(创建一个初始化对象);

2、ThreadLocal 在使用完之后需要进行 remove(),否则在使用线程池使用 ThreadLocal 时会造成 B 线程使用了 A 线程的脏数据的情况

  • 如果使用线程池,可以定制线程池的 afterExecute 方法,调用 ThreadLocal 的 remove 方法
//线程本地变量,用于记录线程异步任务的开始执行时间
private static final ThreadLocal<Long> START_TIME= new ThreadLocal<>();
ExecutorService pool = new ThreadPoolExecutor(2,
4, 60,
TimeUnit.SECONDS, new LinkedBlockingQueue<>(2)) {
//省略其他
//异步任务执行完成之后的钩子方法
@Override
protected void afterExecute(Runnable target, Throwable t)
{
//省略其他
//清空ThreadLocal实例的本地值
START_TIME.remove();
}
};
posted @ 2023-05-28 23:23  ayiZzzz  阅读(230)  评论(1)    收藏  举报