深入理解 Java 本地线程变量 ThreadLocal

这是原理篇,实战篇请参考:ThreadLocal 实战之踩坑笔记

ThreadLocal 是什么?


  • ThreadLocal 字面意思是本地线程,其实更准确来说是线程局部变量,线程类 Thread 有个变量叫做 threadLocals,其类型就是ThreadLocal.ThreadLocalMap 类型,他其实不是一个 Map 类型,但可以暂时理解它是一个Map,键为 ThreadLocal 对象,值就是要存入的value。

ThreadLocal 有什么用?


  • 我们知道,在多线程并发执行时,一方面,需要进行数据共享,于是才有了 volatile 变量解决多线程间的数据可见性,也有了锁的同步机制,使变量或代码块在某一时该,只能被一个线程访问,确保数据共享的正确性。另一方面,并不是所有数据都需要共享的,这些不需要共享的数据,让每个线程单独去维护就行了,ThreadLocal 就是用于线程间的数据隔离的。
  • ThreadLocal 提供线程内部的局部变量,在本线程内随时随地可取,隔离其他线程,获取保存的值时非常方便,ThreadLocal 为变量在每个线程中都创建了一个副本,每个线程就可以很方便的访问自己内部的副本变量。

源码解析


我们先来看看 ThreadLocal 类是如何为每个线程创建一个变量的副本的。

  • 首先 get 方法的实现:
    /**
     * Returns the value in the current thread's copy of this
     * thread-local variable.  If the variable has no value for the
     * current thread, it is first initialized to the value returned
     * by an invocation of the {@link #initialValue} method.
     *
     * @return the current thread's value of this thread-local
     */
    public T get() {
        Thread t = Thread.currentThread();
        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();
    }

方法里面第一行获取当前线程,然后通过 getMap(t) 方法获取 ThreadLocal.ThreadLocalMap,所有的变量数据都存在该 map,map 的具体类型是一个 Entry 数组。

然后接着下面获取到 Entry 键值对,注意这里获取 Entry 时参数传进去的是 this,即 ThreadLocal 实例,而不是当前线程 t。如果获取成功,则返回 value 值。

如果 map 为空,则调用 setInitialValue 方法返回一个初始 value,其实这个默认初始 value 为 null。

  • 接着来看一下 getMap 方法做了什么:
	/* ThreadLocal values pertaining to this thread. This map is maintained by the ThreadLocal class. */
	ThreadLocal.ThreadLocalMap threadLocals = null;
    /**
     * Get the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     *
     * @param  t the current thread
     * @return the map
     */
	ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

在 getMap 中,是调用当期线程 t,返回当前线程t中的一个成员变量 threadLocals,类型为 ThreadLocal.ThreadLocalMap。就是上面提到的每一个线程都自带一个 ThreadLocalMap 类型的成员变量。

  • 继续来看 ThreadLocalMap 的实现:
	static class ThreadLocalMap {
        /**
         * The entries in this hash map extend WeakReference, using
         * its main ref field as the key (which is always a
         * ThreadLocal object).  Note that null keys (i.e. entry.get()
         * == null) mean that the key is no longer referenced, so the
         * entry can be expunged from table.  Such entries are referred to
         * as "stale entries" in the code that follows.
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

ThreadLocalMap 是 ThreadLocal 的一个静态内部类,其内部主要是一个 Entry 数组存储数据(并不是一个 map 类型),ThreadLocalMap 的 Entry 继承了 WeakReference,用来实现弱引用,被弱引用关联的对象(其实就是 ThreadLocal 对象)只能生存到下一次垃圾收集发生之前,WeakReference 不了解的可以点这里,并且使用 ThreadLocal 对象的 HashCode 的散列值计算得出的 Entry 数组的下标 i,这里不同对象可能存在相同的下标 i,set() 方法处理逻辑是:下标加一,直到第一个要插入的位置为空。

内存泄露问题


我们知道,ThreadLocal 是基于 ThreadLocalMap 实现的,这个 Map 的 Entry 继承了 WeakReference,而 Entry 对象中的 key 使用了 WeakReference 封装,也就是说 Entry 中的 key 是一个弱引用类型,而弱引用类型只能存活在下次 GC 之前。

如果一个线程调用 ThreadLocal 的 set 设置变量,当前 ThreadLocalMap 则新增一条记录,但发生一次垃圾回收,此时 key 值被回收,而 value 值依然存在内存中,由于当前线程一直存在,所以 value 值将一直被引用。

这些被垃圾回收掉的 key 就存在一条引用链的关系:Thread-->ThreadLocalMap-->Entry-->Value,这条引用链会导致 Entry 不会回收,Value 也不会回收,但 Entry 中的 Key 却已经被回收的情况,造成内存泄漏。

我们只需要在使用完该 key 值之后,通过 remove 方法 remove 掉,就可以防止内存泄漏了。

总结


每个线程 Thread 内部有一个 ThreadLocalMap 类型的成员变量 threadLocals,这个 ThreadLocalMap 成员变量主要是用 Entry 数组来存储数据,其中数组下标是通过 ThreadLocal 对象计算得出,而且 ThreadLocal 对象被标记为弱引用对象,数组的 value 就是要存储的变量。

上面我们提到,每个线程第一次调用 ThreadLocal.get 方法时,内部会走到 setInitialValue 方法返回一个初始 value,其实这个默认初始 value 为 null,这里要注意的一个是,null 赋给基本数据类型时会抛空指针。

附加


  • 我们上面提到的 get() 里 map.getEntry(this) 的逻辑,这里很有讲究,直接附上源码,其中注释都补上了,大家自行享用:
        /**
         * Get the entry associated with key.  This method
         * itself handles only the fast path: a direct hit of existing
         * key. It otherwise relays to getEntryAfterMiss.  This is
         * designed to maximize performance for direct hits, in part
         * by making this method readily inlinable.
         *
         * @param  key the thread local object
         * @return the entry associated with key, or null if no such
         */
        private Entry getEntry(ThreadLocal<?> key) {
        	// 不同的 ThreadLocal 有可能会产生一样的 i,这个时候 set() 那边的逻辑是,直接把他的下标加一,直到所在下标没有数据
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            if (e != null && e.get() == key)
	            // 散列到的位置,刚好就是要查找的对象
                return e;
            else
            	// 直接散列到的位置没找到,那么顺着 hash 表递增(循环)地往下找
                return getEntryAfterMiss(key, i, e);
        }
  • getEntryAfterMiss() 方法,这里就有我们上面内存泄漏问题提到的遍历清除 Key 为 null 的 Entry 逻辑
        /**
         * Version of getEntry method for use when key is not found in
         * its direct hash slot.
         *
         * @param  key the thread local object
         * @param  i the table index for key's hash code
         * @param  e the entry at table[i]
         * @return the entry associated with key, or null if no such
         */
        private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
            Entry[] tab = table;
            int len = tab.length;

			// 下标依次加一进行查找直到找到,或者下一个数组为 null 说明并没有存储过该对象对应的值
            while (e != null) {
                ThreadLocal<?> k = e.get();
                if (k == key)
                    return e;
                if (k == null)
	                // 如果 Key 为 null,将所有 value 都设为 null,jvm 就会回收掉之前泄露的 value 了
                    expungeStaleEntry(i);
                else
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;
        }
  • 也就是说,我们上面提到内存泄漏问题也不是永久的泄露,而是短暂的,只要访问命中泄露位置的 Entry,通过判断 key == null 就能调用 expungeStaleEntry 方法,删除掉 value 和 Entry,从而能够让这些泄露的对象被回收掉。
  • 4
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

发飙的蜗牛咻咻咻~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值