Java ThreadLocal源码解析 有图有真相

前言

今天来发第一个源码相关的博客。先发个最简单的ThreadLocal试试水吧... 大佬轻喷

经常遇到的问题

ThreadLocal了解吗

弱引用说一下

ThreadLocal会造成内存泄漏吗

...

之前刷面经经常看到这些问题,所以自己看了一下JDK1.8的ThreadLocal的实现。最近正好复习,把之前没写的博客补上

正文

看源码很容易一头雾水,觉得不知道我在说什么的往下翻,有一张processOn做的图,看完就懂了,所以我们先自己构思一下如果让我们自己实现这样一个东西我们要怎么实现,然后看一下类的结构 包括继承关系等,最后带着问题去看就会容易一些。

.......省略思考过程

我们使用ThreadLocal,最常用的api就是set... get...

所以现在我们从set入手,看看set的过程中发生了什么事

    /**
     * Sets the current thread's copy of this thread-local variable
     * to the specified value.  Most subclasses will have no need to
     * override this method, relying solely on the {@link #initialValue}
     * method to set the values of thread-locals.
     *
     * @param value the value to be stored in the current thread's copy of
     *        this thread-local.
     */
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

  可以看到这里有一个getMap(t),然后我们的value被放到了这个map里。我们来看看这个getMap发生了什么

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

清楚地看到返回了t.threadLocals,那么这个threadLocals是个什么小饼干。

public class Thread implements Runnable {

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

这是个null。回到ThreadLocal的set(T value)方法。接下来要执行的一定是createmap(t,value)。见名知意,这里一定是给threadLocals赋值。

所以我们看看createmap方法

    /**
     * Create the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     *
     * @param t the current thread
     * @param firstValue value for the initial entry of the map
     */
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

嗯,这是个啥,点进去

        /**
         * Construct a new map initially containing (firstKey, firstValue).
         * ThreadLocalMaps are constructed lazily, so we only create
         * one when we have at least one entry to put in it.
         */
        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);
        }

好的我们看到了一个Entry,这个table被赋值成了一个Entry数组, 默认长度为16,并且要求必须是2的幂次

然后给table[i]上创建了一个Entry节点,我们的ThreadLocal引用作为Key传了进去,而我们的value作为v传了进去。

我们继续跟踪Entry。这里是Entry类的全部代码

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

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

哇! 金色传说!好了 到此set方法就完事了。可以看到这里Entry是一个弱引用,指向ThreadLocal,并且它还有一个强引用指向我们存进去的那个值。

来画个图看看现在的关系是怎样的

Java ThreadLocal源码解析 有图有真相_第1张图片

这个图到底对不对呢,本人水平有限。但我得到的信息就是这样。所以暂且认为它没问题

思考一下为什么这里是一个弱引用

这样一看就很明显了。如果我们的强引用threadLocal不再指向ThreadLocal对象,那么Entity中的弱引用将不会影响垃圾回收器回收ThreadLocal对象。

所以现在你知道ThreadLocal到底会不会造成内存泄漏了吗

答案是当然会,因为如果你不手动释放掉Entity指向“我们set进去”的对象的话,这个对象是不会被GC的。

现在让我们来看看remove方法做了什么,是不是跟我们想的一样,释放掉我们的对象呢(醒醒 你没有对象)

    /**
     * Removes the current thread's value for this thread-local
     * variable.  If this thread-local variable is subsequently
     * {@linkplain #get read} by the current thread, its value will be
     * reinitialized by invoking its {@link #initialValue} method,
     * unless its value is {@linkplain #set set} by the current thread
     * in the interim.  This may result in multiple invocations of the
     * {@code initialValue} method in the current thread.
     *
     * @since 1.5
     */
     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

嗯,它调用了上图中threadLocals的remove方法,并将当前ThreadLocal的引用传了进去。所以我们还得继续跟进去看看发生了什么。但从这个代码来看,我们猜的应该是正确的。

        private void remove(ThreadLocal key) {
            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                if (e.get() == key) {
                    e.clear();
                    expungeStaleEntry(i);
                    return;
                }
            }
        }

上面这个代码就是ThreadLocalMap中的代码了。可以看到它用set中同样的方法计算了hash值,然后找到我们的ThreadLocal指向的那个槽位(slot)。然后调用了expungeStaleEntry方法(expunge v. 删除; stale adj. 过期的)即删除过期的Entry,并将下标传了过去

那么我们再看看expungeStaleEntry干了什么

 /**
         * Expunge a stale entry by rehashing any possibly colliding entries
         * lying between staleSlot and the next null slot.  This also expunges
         * any other stale entries encountered before the trailing null.  See
         * Knuth, Section 6.4
         *
         * @param staleSlot index of slot known to have null key
         * @return the index of the next null slot after staleSlot
         * (all between staleSlot and this slot will have been checked
         * for expunging).
         */
        private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;

            // expunge entry at staleSlot
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;

            // Rehash until we encounter null
            Entry e;
            int i;
            for (i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal k = e.get();
                if (k == null) {
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
                    int h = k.threadLocalHashCode & (len - 1);
                    if (h != i) {
                        tab[i] = null;

                        // Unlike Knuth 6.4 Algorithm R, we must scan until
                        // null because multiple entries could have been stale.
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
            return i;
        }

好了通过这里我们看到了,它确实让我们的entry不再指向我们创建的对象,于是我们创建的对象成为了没有引用指向的垃圾。下一次GC就会带走它了。同时可以看到它不止清除了这个slot,还顺便清除了下一个null之前的slot,并且做了rehash。勤劳!

最后想说的话

对没错就这么突然的结束了,用大腿想一想也知道get方法做了什么吧!第一次写源码分析的博客... 如果有错误请各位大佬指正... 觉得说得还可以的话就点个赞吧! 大家好,我是装... yes!

你可能感兴趣的:(java,jdk)