ThreadLocal底层原理是什么?

简介: 讲讲ThreadLocal底层原理
文章已收录Github精选,欢迎Starhttps://github.com/yehongzhi/learningSummary

什么是ThreadLocal

ThreadLocal提供线程的局部变量,这种变量与普通变量的区别在于,每个访问这种变量的线程都有自己的、独立的变量副本。用于解决多线程间的数据隔离问题。

使用场景

其实ThreadLocal在很多开源框架中都有应用:

  • Spring中的事务管理器,比如TransactionSynchronizationManager等。
  • Mybatis中的ErrorContext类,使用ThreadLocal实现线程安全的单例。
  • 存储session中的一些参数,比如用户信息等。

API

ThreadLocal提供了4个常用方法:

  • set()方法,设置当前线程中变量的副本。
  • get()方法,获取 ThreadLocal在当前线程中保存的变量副本。
  • remove()方法,清空当前线程中变量的副本。
  • initialValue()是一个 protected方法,一般是用来重写的,如果在没有set的时候就调用 get,会调用 initialValue方法初始化内容。
private static ThreadLocal<SimpleDateFormat> simpleDateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>(){
    //重写此方法,初始化ThreadLocal的value
    @Override
    protected SimpleDateFormat initialValue() {
        return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    }
};

原理

那么怎么实现数据隔离的,我们从源码的角度进行分析。

我们先看ThreadLocal类的get()方法。

public T get() {
    Thread t = Thread.currentThread();
    //通过当前线程获取ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

private T setInitialValue() {
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}
//返回Thread实例的成员变量threadLocals
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

//给Thread实例的成员变量threadLocals赋值
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

从源码可以看出,数据是存在于Thread类的成员变量threadLocals

/* ThreadLocal values pertaining to this thread. This map is maintained
 * by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;

上面写了一段注释,翻译过来就是,关于该线程的ThreadLocal的值,由ThreadLocal类进行维护。

所以很清楚了,数据隔离的实现是因为ThreadLocal类操作的是Thread的成员变量threadLocals。每个线程Thread都有自己的threadLocals,从而互相不影响。

threadLocals这个成员变量的本质又是ThreadLocalMap类,它是ThreadLocal的内部类,下面我们研究一下这个内部类的数据结构。

数据结构

先看一下源码:

static class ThreadLocalMap {
    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;

        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
    //初始化容量
    private static final int INITIAL_CAPACITY = 16;
    //散列表
    private Entry[] table;
    //有效数量
    private int size = 0;
    //负载因子
    private int threshold;
    
    private void setThreshold(int len) {
        threshold = len * 2 / 3;
    }
    //构造器
    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);
    }
}

这一看跟HashMap还有几分相似,但是哈希冲突的处理方式,ThreadLocalMap采用的是开放寻址法(自行百度一下,这里不多解释了),大概长这个样子:

所以这里可以看出ThreadLocal的引用可以定位到ThreadLocalMap里散列表table[]里的值。

内存泄漏问题

我们从源码中可以看到Entry是继承WeakReference类,key是弱引用,value是强引用。为什么要设计成弱引用?不如反过来想,如果设置成强引用会有什么效果。

如果Entry对象的Key每个都强引用到ThreadLocal对象的话,那么这个ThreadLocal对象就会因为和Entry对象存在强引用关联而无法被GC回收,造成内存泄漏,除非线程结束后,线程被回收了,ThreadLocalMap才会跟着回收。

当作为Key的ThreadLocal对象设置成弱引用对象后,在系统GC的时候,ThreadLocal对象就会被回收。

但是这样就能防止内存泄漏吗?

其实不然!因为Value还是强引用对象,当Key被回收后,key变成了null值,而Value依然存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,而这块value也永远不会被访问到了,最终造成内存泄漏。

所以在设计ThreadLocalMap时就考虑到这个问题,在ThreadLocal的get()、set()、remove()的时候都会清除线程ThreadLocalMap里所有key为null的value。

总结

  • 其实ThreadLocal并没有解决多线程间数据共享的问题,而是使数据在不同线程有不同的副本,那么就不需要解决共享数据的问题。
  • 每个线程持有一个ThreadLocalMap对象,该ThreadLocalMap对象只会被持有它的线程访问,所以不存在线程安全问题。
  • ThreadLocalMap的数据结构类似HashMap,里面由Entry[]数组、size、负载因子等组成,采用开放寻址法解决哈希冲突。
  • ThreadLocalMap的Entry对ThreadLocal对象是弱引用,GC回收后,会产生一些key为null的value无法被访问,也无法被回收,最终导致内存泄漏。预防措施是调用ThreadLocal的remove()方法,清除掉ThreadLocalMap里面key为null的value。

非常感谢你的阅读,希望这篇文章能给到你帮助和启发。

觉得有用就点个赞吧,你的点赞是我创作的最大动力~

我是一个努力让大家记住的程序员。我们下期再见!!!

能力有限,如果有什么错误或者不当之处,请大家批评指正,一起学习交流!
相关文章
|
消息中间件 SQL 存储
超详细的RabbitMQ入门,看这篇就够了!
RabbitMQ入门,看这篇就够了
216943 68
|
前端开发 网络协议 Dubbo
超详细Netty入门,看这篇就够了!
本文主要讲述Netty框架的一些特性以及重要组件,希望看完之后能对Netty框架有一个比较直观的感受,希望能帮助读者快速入门Netty,减少一些弯路。
91330 32
超详细Netty入门,看这篇就够了!
|
存储 缓存 监控
美团面试:说说OOM三大场景和解决方案? (绝对史上最全)
小伙伴们,有没有遇到过程序突然崩溃,然后抛出一个OutOfMemoryError的异常?这就是我们俗称的OOM,也就是内存溢出 本文来带大家学习Java OOM的三大经典场景以及解决方案,保证让你有所收获!
5401 0
美团面试:说说OOM三大场景和解决方案? (绝对史上最全)
|
5月前
|
数据库 对象存储
2025年 | 5月云大使推广奖励规则
【近期云大使规则升级】①上线老用户返佣规则。②企业云大使提现功能上线。③优化推广奖励限制。④调整用户关联周期。⑤新增沉睡用户返利 。⑥推荐企业认证新用户首购最高奖励45%。
|
算法 Java 程序员
5千字详细讲解java并发编程的AQS
本文讲解AQS的组成,实现原理,应用,源码解析
5千字详细讲解java并发编程的AQS
|
存储 数据挖掘 Linux
探索Linux的ls命令:深入解析与实用指南
**探索Linux的`ls`命令:简明指南** `ls`命令用于列出目录内容,是Linux用户的基础工具。它提供灵活的参数定制输出,如 `-l` 显示详细信息,`-a` 显示隐藏文件,`-h` 以易读格式显示大小,`-R` 递归列出子目录。结合其他命令和管道,`ls`能用于数据分析。注意权限和使用最佳实践,如避免多余参数,谨慎使用通配符,并利用`man ls`查阅手册以深入学习。善用`ls`能提升Linux操作效率。
|
消息中间件 微服务
RabbitMQ入门指南(十):延迟消息-死信交换机
RabbitMQ是一个高效、可靠的开源消息队列系统,广泛用于软件开发、数据传输、微服务等领域。本文主要介绍了死信交换机、死信交换机实现延迟消息等内容。
387 0
|
Java 程序员 索引