(二十二) ThreadLocal

前言:今天突发奇想复习了下(一)handler 的知识,然后发现一个当时遗忘的知识点ThreadLocal,当时是真不懂,经过这一两个月的学习,或多或少开始有点了解了,现在总结一下。

ThreadLocal:MessageQueue对象,和Looper对象在每个线程中都只会有一个对象,怎么能保证它只有一个对象,就通过ThreadLocal来保存。Thread Local是一个线程内部的数据存储类,通过它可以在指定线程中存储数据,数据存储以后,只有在指定线程中可以获取到存储到数据,对于其他线程来说则无法获取到数据。(这个当时不是很明白,现在我明白啦,因为一个线程一旦调用Looper.prepare,就会把Looper保存在该线程单独持有的局部变量ThreadLocalMap里,以该线程作为key值,以Looper作为values值,当然不能共享给其他线程,并且由于prepare的特殊性,looper只会保存一次,这就保证了looper只有一个,且就是prepare后的那一个;Looper唯一,messageQueue作为它的成员变量自然也唯一)


参考知乎回答:winwill2012的回答


1. 简介

这个回答比较好的地方在于是结合源码注释、实例讲解和源码讲解的,既循序渐进又有说服力。我也比较喜欢在有简单了解的基础后从源码注释开始学习。

源码注释如下,简单说来就是说ThreadLocal提供了线程级的局部变量,这些变量和普通局部变量的不同在于每个线程都有只属于自己的变量,单独的初始化,可以通过get()和set()进行读写。ThreadLocal 一般是一个与Thread关联的private static的变量。每个线程只要还存活就有一个隐式的对ThreadLocal的引用,如果线程挂了,那边所有的ThreadLocal如果没有其他地方引用它的话就会被垃圾回收。

/**
 * 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).
 *
 * <p>For example, the class below generates unique identifiers local to each
 * thread.
 * A thread's id is assigned the first time it invokes {@code ThreadId.get()}
 * and remains unchanged on subsequent calls.
 * <pre>
 * import java.util.concurrent.atomic.AtomicInteger;
 *
 * public class ThreadId {
 *     // Atomic integer containing the next thread ID to be assigned
 *     private static final AtomicInteger nextId = new AtomicInteger(0);
 *
 *     // Thread local variable containing each thread's ID
 *     private static final ThreadLocal<Integer> threadId =
 *         new ThreadLocal<Integer>() {
 *             @Override protected Integer initialValue() {
 *                 return nextId.getAndIncrement();
 *         }
 *     };
 *
 *     // Returns the current thread's unique ID, assigning it if necessary
 *     public static int get() {
 *         return threadId.get();
 *     }
 * }
 * </pre>
 * <p>Each thread holds an implicit reference to its copy of a thread-local
 * variable as long as the thread is alive and the {@code ThreadLocal}
 * instance is accessible; after a thread goes away, all of its copies of
 * thread-local instances are subject to garbage collection (unless other
 * references to these copies exist).
 *
 * @author  Josh Bloch and Doug Lea
 * @since   1.2
 */


2. 原理

之前源码中提及了set()和get()方法,看了下set()的源码,对ThreadLocal的实现原理有了比较深刻地了解。

2.1 从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);
    }

这里其实写的很棒。简单说来分为 3 步:

1.获取当前线程的标识

2.获取当前线程自己持有的ThreadLocalMap变量

3.如果该变量不是空,直接set一个entry下去,以threadLocal为key,以真正的值为值。


这里先解释下第2步,Thread有一个叫做threadLocals的成员变量,这是个ThreadLocal内部定义的一个map内部类。

    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal的ThreadLocalMap静态内部类。

这个内部类可以看成一个简单利用开放地址法解决hash冲突的map,特殊的地方在于是以ThreadLocal作为实例的。由于每个线程都持有自己独特的ThreadLocalMap变量,所以即使ThreadLocal相同,由于处于不同的map中,不会导致ThreadLocalMap存取的值受到其他线程的影响,这就实现了线程局部变量的目的。


2.2 从ThreadLocalMap看内存泄露

引用原文:

/*然后网上就传言,ThreadLocal会引发内存泄露,他们的理由是这样的:

如上图,ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用引用他,那么系统gc的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:

ThreadLocal Ref -> Thread -> ThreaLocalMap -> Entry -> value

永远无法回收,造成内存泄露。

我们来看看到底会不会出现这种情况。 其实,在JDK的ThreadLocalMap的设计中已经考虑到这种情况,也加上了一些防护措施。*/

首先ThreadLocalMap持有的静态内部类Entry是继承的WeakReference的,注释中提及了如果key变成了null,说明这个条目需要从table里移除,这些条目被当做过时的对待。

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

2.2.1 初始化

初始化为一个16的数组,这个和hashmap好像啊,然后第一个元素的key就是第一个被调用到的ThreadLocal,通过它以散列表的形式计算在数组中的位置,而对应的值一般是调用setInitialValue生成的值。

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


2.2.2 set()

set()方法可以理解为往table里以键值对的形式添加元素。这里没有像hashmap一样用链地址法解决hash冲突,而采用了开放定址法,即位置被占了就顺延下一个位置,直到一个位置是空。

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

            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();

                if (k == key) {
                    e.value = value;
                    return;
                }

                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

其中当key是null的时候会调用replaceStaleEntry这个方法会清空一下过时的entry,简单的了解下源码,它会以staleSlot为基准向前向后找key值同样是null的,然后以该key值为null的元素所在的index为起始参数,开始进行key为null的entry的清空操作。为什么不以staleSlot作为清空的参数呢,是由于该staleSlot所在的位置要么被替换要么被set。清空的过程当中也由于是用开放地址法决定的存储位置导致清空一个,后面的如果是顺延的就往前面挪一下,即所谓的rehash,保证hash计算的顺序。

/**
         * Replace a stale entry encountered during a set operation
         * with an entry for the specified key.  The value passed in
         * the value parameter is stored in the entry, whether or not
         * an entry already exists for the specified key.
         *
         * As a side effect, this method expunges all stale entries in the
         * "run" containing the stale entry.  (A run is a sequence of entries
         * between two null slots.)
         *
         * @param  key the key
         * @param  value the value to be associated with key
         * @param  staleSlot index of the first stale entry encountered while
         *         searching for key.
         */
        private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                       int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;
            Entry e;

            // Back up to check for prior stale entry in current run.
            // We clean out whole runs at a time to avoid continual
            // incremental rehashing due to garbage collector freeing
            // up refs in bunches (i.e., whenever the collector runs).
            //往前找到一个key值为空的元素,记录下它的位置。
            int slotToExpunge = staleSlot;
            for (int i = prevIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = prevIndex(i, len))
                if (e.get() == null)
                    slotToExpunge = i;

            // Find either the key or trailing null slot of run, whichever
            // occurs first
            for (int i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();

                // If we find key, then we need to swap it
                // with the stale entry to maintain hash table order.
                // The newly stale slot, or any other stale slot
                // encountered above it, can then be sent to expungeStaleEntry
                // to remove or rehash all of the other entries in run.
                if (k == key) {
                    e.value = value;

                    tab[i] = tab[staleSlot];
                    tab[staleSlot] = e;

                    // Start expunge at preceding stale entry if it exists
                    if (slotToExpunge == staleSlot)
                        slotToExpunge = i;
                    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                    return;
                }

                // If we didn't find stale entry on backward scan, the
                // first stale entry seen while scanning for key is the
                // first still present in the run.
                if (k == null && slotToExpunge == staleSlot)
                    slotToExpunge = i;
            }

            // If key not found, put new entry in stale slot
            tab[staleSlot].value = null;
            tab[staleSlot] = new Entry(key, value);

            // If there are any other stale entries in run, expunge them
            if (slotToExpunge != staleSlot)
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
        }


2.2.3 getEntry()

getEntry和set有些类似的地方在于当遇到e是空或者entry的key不相等时会作一些特殊操作。

/**
         * 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
                return getEntryAfterMiss(key, i, e);
        }

当k == null的时候调用expungeStaleEntry方法进行清理,清理操作看样子是一次性的,不像set的时候调用cleanSomeSlots额外执行log2(n)次检查。

 /**
         * 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)
                    expungeStaleEntry(i);
                else
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;
        }
 /**
         * 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;
        }

2.2.4 内存泄露和remove

引用原文:

/*在这个过程中遇到的key为null的Entry都会被擦除,那么Entry内的value也就没有强引用链,自然会被回收。仔细研究代码可以发现,set操作也有类似的思想,将key为null的这些Entry都删除,防止内存泄露。 但是光这样还是不够的,上面的设计思路依赖一个前提条件: 要调用ThreadLocalMap的genEntry 函数或者set 函数。这当然是不可能任何情况都成立的,所以很多情况下需要使用者手动调用ThreadLocal的remove函数,手动删除不再需要的ThreadLocal,防止内存泄露。所以JDK建议将ThreadLocal变量定义成private static的,这样的话ThreadLocal的生命周期就更长,由于一直存在ThreadLocal的强引用,所以ThreadLocal也就不会被回收,也就能保证任何时候都能根据ThreadLocal的弱引用访问到Entry的value值,然后remove它,防止内存泄露。
作者:winwill2012
链接:https://www.zhihu.com/question/23089780/answer/62097840
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。*/

看下所谓的remove方法会主动调用expungeStaleEntry方法清理过期entry,防止内存泄露。

/**
         * Remove the entry for key.
         */
        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;
                }
            }
        }

看了下Looper源码并没有显示调用move方法进行ThreadLocal的清空。


3.总结

ThreadLocal可以看做是Thread的局部变量,更深入地可以说是作为其成员变量map的以thread为key的值,每一个线程调用都会自己初始化一下(调用initialValue()),隔绝了其他线程染指的可能。

猜你喜欢

转载自blog.csdn.net/sinat_20059415/article/details/79821251