关于ThreadLocal你一定要知道的

2,815 阅读18分钟

为什么要用ThreadLocal

官方一点的说就是实现线程封闭的一种方式,比如说多线程访问共享变量,一个类里面的成员变量,在单线程的环境下是安全的,只有一个线程去使用。但是在多线程的情况下就会被多个线程使用,而ThreadLocal就是为了保证一个线程只是用他自己的成员变量。

简单使用

在ThreadLocal的应用中,最多的一个场景就是数据库连接的获取,我们不妨思考一下,如果我们这里维护是一个共享的全局连接,那么并发的情况下,就会导致上一个线程用完了关闭连接。但是当前现在仍在查询的情况。


    private static final String DB_URL  = "localhost:3306";

    private static  ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>(){
        /**
         * Returns the current thread's "initial value" for this
         * thread-local variable.  This method will be invoked the first
         * time a thread accesses the variable with the {@link #get}
         * method, unless the thread previously invoked the {@link #set}
         * method, in which case the {@code initialValue} method will not
         * be invoked for the thread.  Normally, this method is invoked at
         * most once per thread, but it may be invoked again in case of
         * subsequent invocations of {@link #remove} followed by {@link #get}.
         *
         * <p>This implementation simply returns {@code null}; if the
         * programmer desires thread-local variables to have an initial
         * value other than {@code null}, {@code ThreadLocal} must be
         * subclassed, and this method overridden.  Typically, an
         * anonymous inner class will be used.
         *
         * @return the initial value for this thread-local
         */
        @Override
        protected Connection initialValue() {
            return DriverManager.getConnection(DB_URL);
        }
    };

    private static Connection getConnection(){
        return connectionHolder.get();
    }


源码解析

我一直觉得看源码最重要的是把注释看懂,所以我这里不会直接把注释删除,会连着注释一起分析一下。

类注释

/**
 * 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).
 *
 * 这段话其实就说明了ThreadLocal的作用。线程自己的,独立初始化的变量副本
 *
 * <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>
 * 这里说明了,用id去标识线程。包括产生ID的方法。
 * 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&lt;Integer&gt; threadId =
 *         new ThreadLocal&lt;Integer&gt;() {
 *             &#64;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
 */

我们打开ThreadLocal的源码,在它的class上面就有这段注释。我们看的时候应该抓住重点,其实这一段就说了两件事,ThreadLocal是做什么的,ThreadId怎么生成的。

ThreadLocal结构

ThreadLocal-5.png 需要注意的点是:

  1. ThreadLocal实例本身是不存储值,它只是提供了一个在当前线程中找到副本值得key。
  2. 是ThreadLocal包含在Thread中,而不是Thread包含在ThreadLocal中
  3. 每个线程有一个自己的ThreadLocalMap。
  4. 每个线程在往ThreadLocal里放值的时候,都会往自己的ThreadLocalMap里存,读也是以ThreadLocal作为引用,在自己的map里找对应的key,从而实现了线程隔离

Entry结构

Entry 是一个k-v结构,其中key是ThreadLocal,并且是弱引用。 这个WeakReference(弱引用) 就是导致ThreadLocal内存泄漏原因。

        /**
         * 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.
         *
         * entry.get() == null的时候是脏key
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

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

ThreadLocal.set()

接下来我们首先看常用方法set(value)和get()


    /**
     * 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.
     * 将此线程局部变量的当前线程副本设置为指定值。 大多数子类将不需要重写此方法,而仅依靠initialValue方法来设置线程initialValue的值。
     */
    public void set(T value) {
        //获取当前线程
        Thread t = Thread.currentThread();
        //获取当前线程的ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        if (map != null)
        //如果map不为空就放在ThreadLocalMap里面 ThreadLocal为key
            map.set(this, value);
        else
        //否则创建一个ThreadLocalMap
            createMap(t, value);
    }


这里看到其实在ThreadLocal里面的实现很简单,并且大部分是ThreadLocalMap的操作,所以其实ThreadLocal的操作就是操作ThreadLocalMap。

getMap()

这里的代码实现只有一行,就是拿出来当前线程的threadLocals


    /**
     * Get the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     *
     * @param  t the current thread
     * @return the map
     */
    ThreadLocalMap getMap(Thread t) {
    //拿出来当前线程的threadLocals
        return t.threadLocals;
    }

ThreadLocalMap.set()

这里的代码主要就是对value进行set。其中涉及到,k-v存储的结构,解决冲突的算法,以及对回收了的对象的处理。

        /**
         * Set the value associated with key.
         * key为ThreadLocal value 就是实际设定的值
         * @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来实现k-v存储的,稍后会讲Entry的实现
            Entry[] tab = table;
            //获取tab的长度
            int len = tab.length;
            //通过threadLocalHashCode去计算存储的位置
            int i = key.threadLocalHashCode & (len-1);
            //这里是一个开放定址法解决hash碰撞
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                 //获取tab[i]
                ThreadLocal<?> k = e.get();
                //如果里面已经有ThreadLocal并且相等就直接覆盖
                if (k == key) {
                    e.value = value;
                    return;
                }
                //如果当前位置为null(ThreadLocal被回收了) 就进行替换
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
            //如果是没有相同ThreadLocal元素,并且当前位置没有被回收的ThreadLocal就直接new Entry(key,value)
            tab[i] = new Entry(key, value);
            //增加size
            int sz = ++size;
            //清除被回收的元素,并且如果sz大于等于threshold(阈值)进行rehash
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

ThreadLocalHashCode

ThreadLocalHashCode 其实就是一个自定义哈希码,底层的实现就是AtomicInteger,用unsafe做原子操作


    /**
     * ThreadLocals rely on per-thread linear-probe hash maps attached
     * to each thread (Thread.threadLocals and
     * inheritableThreadLocals).  The ThreadLocal objects act as keys,
     * searched via threadLocalHashCode.  This is a custom hash code
     * (useful only within ThreadLocalMaps) that eliminates collisions
     * in the common case where consecutively constructed ThreadLocals
     * are used by the same threads, while remaining well-behaved in
     * less common cases.
     * 说明了threadLocalHashCode的作用通过threadLocalHashCode搜索ThreadLocal对象
     */
    private final int threadLocalHashCode = nextHashCode();

    /**
     * The next hash code to be given out. Updated atomically. Starts at
     * zero.
     * threadLocalHashCode是一个AtomicInteger,他的操作时原子的,并且从0开始
     */
    private static AtomicInteger nextHashCode =
        new AtomicInteger();

    /**
     * The difference between successively generated hash codes - turns
     * implicit sequential thread-local IDs into near-optimally spread
     * multiplicative hash values for power-of-two-sized tables.
     * 这里可以看作一个自增的步长,把线性的id转换为用于2的幂次方表的近似最佳分布的乘法哈希值。
     */
    private static final int HASH_INCREMENT = 0x61c88647;

    /**
     * Returns the next hash code.
     * 返回下一个code
     */
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

replaceStaleEntry

这个方法比较复杂,这里需要注意 整个方法的流程是尽力地去擦除一些找到的staleSlot。可能看到这里会有这样一个疑问,既然key已经为空了,就应该会被GC回收,为什么还要特地的来进行这样一个过程。因为这里是key 被回收,value还没被回收,entry更加没回收,所以需要让他们回收。这些先不展开,后面讲。


        /**
         * 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(e.get() == null)
            //这个操作是为了把前面的脏Entry在一次过程中一起释放出来
            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
            //向后遍历,找到后边第一个脏key和上面的循环顺序相反
            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.
                //如果发现这个key已经存在
                if (k == key) {
                    //覆盖value
                    e.value = value;
                    //与之前的脏key交换
                    tab[i] = tab[staleSlot];
                    tab[staleSlot] = e;

                    // Start expunge at preceding stale entry if it exists
                    //如果在往前查找的过程中没发现脏key(slotToExpunge== staleSlot)
                    if (slotToExpunge == staleSlot)
                    //设置slotToExpunge = 当前位置
                        slotToExpunge = i;
                        //进行清理
                    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                    return;
                }


            // 如果在往前查找的过程中没发现脏key
            // 那么我们需要把slotToExpunge 设置为当前位置

                if (k == null && slotToExpunge == staleSlot)
                //设置slotToExpunge = 当前位置
                    slotToExpunge = i;
            }

            // If key not found, put new entry in stale slot
            
            // 如果key 在数组中没有存在,那么直接新建一个新的放进去就可以
            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);
        }


通过源码可以知道,整个过程可以总共分成两个部分。向前查找向后查找。因为如果一个位置出现脏key那么很可能相邻位置也出现了脏key,所以为了提升效率,这个方法会把相邻的脏key一起处理。这里我们看一下这几种情况。

注意:本文中脏key的判断是(e = tab[i]) != null && e.get() == null

  1. 向前找到脏key,并且向后找到可以覆盖的key(k == key)

threadLocal-1(1).png 2. 向前找到脏key,但是向后查找未找到可以覆盖的key(k != key)

threadLocal-2(1).png 3.向前没有找到脏key,向后找到可以覆盖的key(k == key) threadLocal-3(1).png 这里的逻辑稍微复杂一点,只要记住两点,1.如果向后查找找到脏key, cleanSomeSlots就是脏key的位置。2.如果向后查找没有找到脏key, cleanSomeSlots就是当前位置i。

  1. 向前没有找到脏key,向后没有找到可以覆盖的key(k != key) threadLocal-4(1).png

这里把覆盖的key交换的作用是防止出现两个相同的key

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
         * 清除脏key
         * @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
            //清除当前脏key
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            //减少tab size
            size--;

            // Rehash until we encounter null
            //重新hash直到遇到null
            Entry e;
            int i;
            for (i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
                //遇到脏key 清理
                if (k == null) {
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
                //rehash
                    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;
        }

所以,这个方法执行完毕,从staleSlot~i这个位置之间都没有脏key。

cleanSomeSlots

这里只需要注意两个入参即可:

  1. i是调用了expungeStaleEntry的返回值,i位置一定不会是脏key,所以从i的下一个位置开始查找。
  2. n是扫描控制参数,对n进行位运算来控制循环。

        /**
         * Heuristically scan some cells looking for stale entries.
         * This is invoked when either a new element is added, or
         * another stale one has been expunged. It performs a
         * logarithmic number of scans, as a balance between no
         * scanning (fast but retains garbage) and a number of scans
         * proportional to number of elements, that would find all
         * garbage but would cause some insertions to take O(n) time.
         * 
         * 此方法在添加和删除的时候调用
         * 此方法有可能会导致插入的时间复杂度变成O(n)
         *
         * @param i a position known NOT to hold a stale entry. The
         * scan starts at the element after i.
         * 从i位置之后开始
         * @param n scan control: {@code log2(n)} cells are scanned,
         * unless a stale entry is found, in which case
         * 扫描控制
         * {@code log2(table.length)-1} additional cells are scanned.
         * When called from insertions, this parameter is the number
         * of elements, but when from replaceStaleEntry, it is the
         * table length. (Note: all this could be changed to be either
         * more or less aggressive by weighting n instead of just
         * using straight log n. But this version is simple, fast, and
         * seems to work well.)
         * 如果没有找到脏key 就会继续扫描log2(n)次也就是log2(table.length)-1个元素
         * @return true if any stale entries have been removed.
         * 如果删除了脏key 就会返回 true
         */
        private boolean cleanSomeSlots(int i, int n) {
            //结果标志
            boolean removed = false;
            Entry[] tab = table;
            int len = tab.length;
            do {
                i = nextIndex(i, len);
                Entry e = tab[i];
                //判断是不是脏key
                if (e != null && e.get() == null) {
                    n = len;
                    removed = true
                    //调用处理脏key的方法
                    i = expungeStaleEntry(i);
                }
            } while ( (n >>>= 1) != 0); // 向右位移一位相当于相当于n除以2
            return removed;
        }

ThreadLocal.get()

接下来我们来看ThreadLocal.get()方法。get相比于set方法要简单易懂很多。我们只要关注setInitialValue方法就行

    /**
     * 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
     * 返回当前线程中的值,如果不存在就调用setInitialValue方法
     */
    public T get() {
        //拿取当前线程
        Thread t = Thread.currentThread();
        //获取当前线程的map
        ThreadLocalMap map = getMap(t);
        //map不为空
        if (map != null) {
            //获取当前线程的Entry
            ThreadLocalMap.Entry e = map.getEntry(this);
            //Entry不为空
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        //调用初始化方法
        return setInitialValue();
    }

setInitialValue

这个方法也很简单,可以发现这个和我们之前说的set()方法差不多的。这里需要注意initialValue()这个方法

    /**
     * Variant of set() to establish initialValue. Used instead
     * of set() in case user has overridden the set() method.
     *
     * @return the initial value
     */
    private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }

initialValue

可以看见这个方法的注释非常长,主要说明了几点:

  1. 在没有调用set()的前提下,第一次调用get()时会调用这个方法。
  2. 通常,每个线程最多调用一次此方法,但是在随后依次调用remove和get情况下,可以再次调用此方法。
  3. 如果需求的初始值非空,则必须将ThreadLocal子类化,并重写此方法。

    /**
     * Returns the current thread's "initial value" for this
     * thread-local variable.  This method will be invoked the first
     * time a thread accesses the variable with the {@link #get}
     * method, unless the thread previously invoked the {@link #set}
     * method, in which case the {@code initialValue} method will not
     * be invoked for the thread.  Normally, this method is invoked at
     * most once per thread, but it may be invoked again in case of
     * subsequent invocations of {@link #remove} followed by {@link #get}.
     *
     * <p>This implementation simply returns {@code null}; if the
     * programmer desires thread-local variables to have an initial
     * value other than {@code null}, {@code ThreadLocal} must be
     * subclassed, and this method overridden.  Typically, an
     * anonymous inner class will be used.
     *
     * @return the initial value for this thread-local
     */
    protected T initialValue() {
        return null;
    }

ThreadLocal.remove()

我们只要关注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.
     *
     * 如果删除这个当前线程元素之后马上调用get()会导致initialValue多次调用
     *
     * @since 1.5
     */
     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

remove

要注意,在删除后调用expungeStaleEntry进行脏key的清除

        /**
         * Remove the entry for key.
         */
        private void remove(ThreadLocal<?> key) {
            Entry[] tab = table;
            int len = tab.length;
            //获取当前线程的hash
            int i = key.threadLocalHashCode & (len-1);
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                 //找到目标Entry
                if (e.get() == key) {
                    //删除Entry
                    e.clear();
                    //删除脏key
                    expungeStaleEntry(i);
                    return;
                }
            }
        }

rehash

我们都知道HashMap,ArrayList都有自己的扩容机制,ThreadLocalMap实际上也有自己的扩容机制,他的代码实现就在rehash方法中。 在set()的代码中我们知道当(sz >= threshold)的时候就进行rehash。

        /**
         * Re-pack and/or re-size the table. First scan the entire
         * table removing stale entries. If this doesn't sufficiently
         * shrink the size of the table, double the table size.
         * 首先执行expungeStaleEntries扫描整个表的脏key,清空脏key,在判断是否需要扩容
         */
        private void rehash() {
            //扫表,清除所有脏key
            expungeStaleEntries();

            // Use lower threshold for doubling to avoid hysteresis
            //用一个较低的阈值去判断是否要扩容,这里是 size >= 3/4 threshold
            if (size >= threshold - threshold / 4)
               //扩容方法
                resize();
        }

expungeStaleEntries

这里就是对整个表进行清理工作

        /**
         * Expunge all stale entries in the table.
         */
        private void expungeStaleEntries() {
            Entry[] tab = table;
            int len = tab.length;
            //遍历表
            for (int j = 0; j < len; j++) {
                Entry e = tab[j];
                //判断是否为脏key
                if (e != null && e.get() == null)
                    //调用清理过程
                    expungeStaleEntry(j);
            }
        }

resize

这只关注是两倍扩容即可。

        /**
         * Double the capacity of the table.
         * 说明了是两倍扩容
         */
        private void resize() {
               
            Entry[] oldTab = table;
            int oldLen = oldTab.length;
            int newLen = oldLen * 2;
            //创建新的Entry[]大小为原来的两倍
            Entry[] newTab = new Entry[newLen];
            int count = 0;
            //遍历老Entry[]
            for (int j = 0; j < oldLen; ++j) {
                Entry e = oldTab[j];
                if (e != null) {
                    ThreadLocal<?> k = e.get();
                    //如果key为空
                    if (k == null) {
                        //把value也设为空,防止内存泄漏
                        e.value = null; // Help the GC
                    } else {
                        //计算位置
                        int h = k.threadLocalHashCode & (newLen - 1);
                        //如果位置被占用 开放定址法
                        while (newTab[h] != null)
                            h = nextIndex(h, newLen);
                        //设置位置
                        newTab[h] = e;
                        count++;
                    }
                }
            }
            //设置阈值
            setThreshold(newLen);
            size = count;
            table = newTab;
        }


开放定址法

所谓的开放定址法就是一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入。这里我们就简单的说一下开放定址法中的线性探测,也是我们ThreadLocal中用到的方法

线性探测法

线性探测法顾名思义,就是解决冲突的函数是一个线性函数,最直接的就是在TreadLocal的代码中也用的是这样一个解决冲突的函数。

 f(x)= x+1

但是要注意的是TreadLocal中,是一个环状的探测,如果到达边界就会直接跨越边界到另一头去。

        /**
         * Increment i modulo len.
         */
        private static int nextIndex(int i, int len) {
            return ((i + 1 < len) ? i + 1 : 0);
        }

        /**
         * Decrement i modulo len.
         */
        private static int prevIndex(int i, int len) {
            return ((i - 1 >= 0) ? i - 1 : len - 1);
        }

线性探测法的优点:

  1. 不用额外的空间(对比拉链法,需要额外链表)
  2. 探测序列具有局部性,可以利用系统缓存,减少IO(连续的内存地址)

缺点:

  1. 耗费时间>O(1)(最差O(n))
  2. 冲突增多——以往的冲突会导致后续的连环冲突(时间复杂度趋近O(n))

为什么要用弱引用

上面我们说到ThreadLocal中的Entry用了弱引用,这里我们不妨来看一下为什么要用弱引用。

ThreadLocal-6(1).png 从引用关系途中我们可以看到,Entry.key指向ThreadLocal这个引用是虚线,也就是弱引用,当我们的ThreadLocal为null时,由于没有强引用关系了,就会正确的被GC回收。

比如说这样一段代码:

public class test {
    private void threadLocalTest(){
        ThreadLocal<String> threadLocal = new ThreadLocal<>();
        System.out.println(threadLocal.get());
    }
}

当我的threadLocalTest方法执行结束时,threadLocal对象会被回收,但是如果ThreadLocal和Entry.key不是弱引用而是强引用关系,就会导致threadLocal对象不会被GC回收,造成内存泄漏。只有当线程死亡的时候才会被回收。

当然实际上使用弱引用也不能完全防止内存泄漏,因为value却依然存在内存泄漏的问题。所有当我们set(),get()的时候都会进行脏key的清理。

ThreadLocal的建议使用方法:

  1. 设计为static的,被class对象给强引用,线程存活期间就不会被回收
  2. 设计为非static的,长对象(比如被spring管理的对象)的内部,也不会被回收
  3. 最好不要在方法中创建ThreadLocal对象

最后,其实弱引用也可以提升JVM内存使用效率,假如我们不引入Redis这种第三方缓存,而是自己做JDK的缓存时,就很有用。

关于0x61c88647

在之前代码中我们可以看到,在代码中Hash算法的增长值为0x61c88647,这是一个很特殊的值。


    /**
     * The difference between successively generated hash codes - turns
     * implicit sequential thread-local IDs into near-optimally spread
     * multiplicative hash values for power-of-two-sized tables.
     * 这里可以看作一个自增的步长,把线性的id转换为用于2的幂次方表的近似最佳分布的乘法哈希值。
     */
    private static final int HASH_INCREMENT = 0x61c88647;

之前我们说过,线性探测法有个问题是,一旦发生碰撞,很可能之后每次都会产生碰撞,导致连环撞车。而使用0x61c88647这个值做一个hash的增长值就可以从一定程度上解决这个问题让生成出来的值较为均匀地分布在2的幂大小的数组中。也就是说当我们用0x61c88647作为步长累加为每个ThreadLocal分配各自的ID也就是threadLocalHashCode再与2的幂取模,得到的结果分布很均匀。

0x61c88647选取其实是与斐波那契散列有关,这个就是数学知识了,这里不展开。

TODO

  1. ThreadLocal实战

个人博客

西西弗的石头

作者水平有限,若有错误遗漏,请指出。

参考文章

1.被大厂面试官连环炮轰炸的ThreadLocal (吃透源码的每一个细节和设计原理)

2.一篇文章,从源码深入详解ThreadLocal内存泄漏问题

3.为什么使用0x61c88647

4.开放定址法——线性探测(Linear Probing)

参考书籍

  1. 《Java并发编程实战》