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

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

ThreadLocal是什么?

  • ThreadLocal字面意思是本地线程,其实更准确来说是线程局部变量,线程类Thread有个变量叫做threadLocals,他的类型就是ThreadLocal.ThreadLocalMap类型,其实它就是一个map类型,key是当前线程的ThreadLocal对象,值就是你要保存的数据

ThreadLocal有什么用?

  • 我们知道,在多线程并发执行时,一方面,需要进行数据共享,于是才有了volatile变量解决多线程间的数据可见性,也有了锁的同步机制,使变量或代码块在某一时该,只能被一个线程访问,确保数据共享的正确性。其中,Synchronized用于线程间的数据共享的。另一方面,并不是所有数据都需要共享的,这些不需要共享的数据,让每个线程单独去维护就行了,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)方法获取到一个map,map的类型为ThreadLocal.ThreadLocalMap。

然后接着下面获取到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的一个静态内部类,ThreadLocalMap的Entry继承了WeakReference,用来实现弱引用,WeakReference不了解的可以点这里,并且使用ThreadLocal作为key值。也就是说WeakReference封装了ThreadLocal,并作为了ThreadLocalMap的Entry的Key。

总结一下

  • 每个线程Thread内部有一个ThreadLocalMap类型的成员变量threadLocals,这个ThreadLocalMap成员变量的Entry的Key为,当前ThreadLocal变量的WeakReference封装,value为要存储的变量。
  • 为何ThreadLocalMap的键值为ThreadLocal对象? 因为每个线程中可能需要有多个threadLocal变量,也就是ThreadLocalMap里面可能会有多个Entry。
  • 上面我们提到,每个线程第一次调用ThreadLocal.get方法时,内部会走到setInitialValue方法返回一个初始value,其实这个默认初始value为null,这里要注意的一个是,null赋给基本数据类型时会抛空指针。

ThreadLocal的内存泄露问题

  • 我们知道一个线程使用完之后并不会销毁,而是会回到线程池进行复用,也就是说,你保存在当前线程中的变量实例还是绑定在线程上的
  • 由于WeakReference封装了ThreadLocal,并作为了ThreadLocalMap的Entry的Key。如果在某些时候ThreadLocal对象被赋Null的话,弱引用会被GC收集,这样就会导致Entry的Value对象找不到,线程被复用后如果有调用ThreadLocal.get方法的话,方法里面会去做遍历清除Key为null的Entry,但如果一直没调用ThreadLocal.get方法的话就会导致内存泄漏了。
  • 所以一般线程用完ThreadLocal后,要调用threadLocal.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) {
            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;

            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;
        }

你可能感兴趣的:(Java技术杂谈)