多线程基础——ThreadLocal

本文介绍了Java中的ThreadLocal类,用于线程内的局部变量存储。ThreadLocal通过为每个线程提供独立的变量副本,减少了多线程间共享变量带来的复杂性。文章详细讲解了ThreadLocal的工作原理,包括ThreadLocalMap的实现,Entry结构,以及多线程环境下可能出现的Hash冲突和内存泄漏问题,并给出了使用ThreadLocal的最佳实践建议。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

概念

/**
 * This class provides thread-local variables.  These variables differ from
 * their normal counterparts in that each thread that accesses one (via its
 * {@code get} or {@code set} method) has its own, independently initialized
 * copy of the variable.  {@code ThreadLocal} instances are typically private
 * static fields in classes that wish to associate state with a thread (e.g.,
 * a user ID or Transaction ID).
 */

Threadlocal是一个线程内部的存储类,可以在指定线程内存储数据,数据存储以后,只有指定线程通过get和set方法就可以得到当前线程对应的存储数据。

ThreadLocal是JDK包提供的,很多地方叫做线程本地变量,也有些地方叫做线程本地存储,ThreadLocal 的作用 是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或 者组件之间一些公共变量的传递的复杂度

实现原理

如果自己来设计ThreadLocal对象,那么,一般的实现思路:设计一个线程安全的Map,key就是当前线程对象,Value就是线程本地变量的值。

然而,JDK的实现思路:

让每个Thread对象,自身持有一个Map,这个Map的Key就是当前ThreadLocal对象,Value是本地线程变量值。相对于加锁的实现方式,这样做可以提升性能,其实是一种以时间换空间的思路。

ThreadLocalMap

ThreadLocal类有个getMap()方法,其实就是返回Thread对象自身的Map——threadLocals。

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}
/* ThreadLocal values pertaining to this thread. This map is maintained
*  by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;

threadLocals是一种ThreadLocal.ThreadLocalMap类型的数据结构,作为内部类定义在ThreadLocal类中,其内部采用一种WeakReference的方式保存键值对。

Entry

static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;
    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

//ThreadLocalMap构造方法
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);
}

实例化ThreadLocalMap时创建了一个长度为16的Entry数组。通过hashCode与length位运算确定出一个索引值i,这个i就是被存储在table数组中的位置。

每个线程Thread持有一个ThreadLocalMap类型的实例threadLocals,结合此处的构造方法可以理解成每个线程Thread都持有一个Entry型的数组table,而一切的读取过程都是通过操作这个数组table完成的。

多个ThreadLocal

//在某一线程声明了ABC三种类型的ThreadLocal
ThreadLocal<A> sThreadLocalA = new ThreadLocal<A>();
ThreadLocal<B> sThreadLocalB = new ThreadLocal<B>();
ThreadLocal<C> sThreadLocalC = new ThreadLocal<C>();

对于一个Thread来说只有持有一个ThreadLocalMap,所以多个ThreadLocal对应同一个ThreadLocalMap对象。为了管理多个ThreadLocal,于是将他们存储在一个数组的不同位置,而这个数组就是上面提到的Entry型的数组table。

多个ThreadLocal在table中的索引的计算代码:

/**
         * Set the value associated with key.
         *
         * @param key the thread local object
         * @param value the value to be 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);

            ........
        }

在ThreadLocalMap中的set方法与构造方法能看到以下代码片段。

  • int i = key.threadLocalHashCode & (len-1)
  • int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1)

简而言之就是将threadLocalHashCode进行一个位运算得到索引i

使用注意

Hash冲突

ThreadLocalMap中解决Hash冲突采用线性探测的方式。所谓线性探测:

就是根据初始key的hashcode值确定元素在table数组中的位置,如果发现这个位置上已经有其他key值的元素被占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。

ThreadLocalMap采用线性探测的方式解决Hash冲突的效率很低(简单地步长+1),所以如果有大量不同的ThreadLocal对象放入map中时发送冲突,则效率很低。

使用建议

每个线程只存一个变量,这样的话所有的线程存放到map中的Key都是相同的ThreadLocal,如果一个线程要保存多个变量,就需要创建多个ThreadLocal,多个ThreadLocal放入Map中时会极大的增加Hash冲突的可能。

内存泄漏

ThreadLocal在ThreadLocalMap中是以一个弱引用类型被Entry中的Key引用的,因此如果ThreadLocal没有外部强引用来引用它,那么ThreadLocal会在下次JVM垃圾收集时被回收。

这个时候就会出现Entry中Key已经被回收,出现一个null Key的情况,外部读取ThreadLocalMap中的元素是无法通过null Key来找到Value的。

因此如果当前线程的生命周期很长,一直存在,那么其内部的ThreadLocalMap对象也一直生存下来,这些null key就存在一条强引用链的关系一直存在:Thread --> ThreadLocalMap-->Entry-->Value,这条强引用链会导致Entry不会回收,Value也不会回收,但Entry中的Key却已经被回收的情况,造成内存泄漏。

使用建议

每次使用完ThreadLocal,都调用它的remove()方法,清除数据。

参考文献

https://segmentfault.com/a/1190000015558915

ThreadLocal - 简书

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值