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

标题是深入理解,其实这个知识点并不难啦,咱一块去研究研究。这是原理篇,实战篇 点这里

ThreadLocal是什么?

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

ThreadLocal有什么用?

  • 我们知道,在多线程并发执行时,一方面,需要进行数据共享,于是才有了volatile变量解决多线程间的数据可见性,也有了锁的同步机制,使变量或代码块在某一时该,只能被一个线程访问,确保数据共享的正确性。另一方面,并不是所有数据都需要共享的,这些不需要共享的数据,让每个线程单独去维护就行了,ThreadLocal就是用于线程间的数据隔离的。
  • 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,用来实现弱引用,被弱引用关联的对象只能生存到下一次垃圾收集发生之前,WeakReference不了解的可以点这里,并且使用ThreadLocal作为key值,其实是根据ThreadLocal对象的HashCode的散列值计算得出的Entry数组的下标 i,这里不同对象可能存在相同的下标 i,set()方法处理逻辑是下标直接往后加一,直到能插入。

总结一下

  • 每个线程Thread内部有一个ThreadLocalMap类型的成员变量threadLocals,这个ThreadLocalMap成员变量主要是用Entry数组来存储数据,其中数组下标是通过ThreadLocal对象计算得出,而且ThreadLocal对象被标记为弱引用对象,数组的value就是要存储的变量。
  • 上面我们提到,每个线程第一次调用ThreadLocal.get方法时,内部会走到setInitialValue方法返回一个初始value,其实这个默认初始value为null,这里要注意的一个是,null赋给基本数据类型时会抛空指针。

网传的ThreadLocal的内存泄露问题

  • 首先什么是内存泄漏?内存泄漏(Memory Leak)是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果(百科解读)。
  • 所以我的观点是:ThreadLocal不存在内存泄漏问题
  • 代码中生成的ThreadLocal对象被WeakReference进行一个弱引用,也就是说只要有垃圾回收,该对象都是会被回收掉的,不存在内存泄漏的情况。
  • 但是如果线程变量中的数据没有进行一个remove()操作,在线程池的情况下也是会出现问题的,比如 看这里

最后再补充一段很有意思的代码

就是我们上面提到的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会自动回收掉无用的对象
                    expungeStaleEntry(i);
                else
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;
        }
发布了112 篇原创文章 · 获赞 303 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/qq_36221788/article/details/94773664