ThreadLocal 是怎么实现的?

ThreadLocal 是怎么实现的?

ThreadLocal 大家都很熟悉了,那么他是如何工作的呢?
下面按照我们平时的使用顺序,来扒一扒他的实现机制(注意源码只提供了必要内容)

代码版本 jdk8

  • ThreadLocal()
  • set() 划重点 大家要认真看帖
  • get()
  • remove()

ThreadLocal

    /**
     * Creates a thread local variable.
     * @see #withInitial(java.util.function.Supplier)
     */
    public ThreadLocal() {
    }

诶? 什么都没有!
惊喜不惊喜? 意外不意外?

set()方法

核心方法ThreadLocal的主要逻辑都在这里了

    public void set(T value) {
        // 拿到当前线程
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            // 直觉告诉我这里不简单
            map.set(this, value);
        else
            // 直觉告诉我这里不简单
            createMap(t, value);
    }

    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

里面有几个个点内容可能比较多我们按顺序来

  • ThreadLocal.ThreadLocalMap
  • ThreadLocal.createMap()
  • ThreadLocal.ThreadLocalMap.set()

ThreadLocal.ThreadLocalMap

Thread.threadLocals 是线程内部成员变量,ThreadLocal的内部类ThreadLocalMap,是一个map容器。

    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
    
    static class ThreadLocalMap {
        
        // 内部数组初始长度
        private static final int INITIAL_CAPACITY = 16;
        
        // 又是一个内部类,用于存储具体数据
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;
    
            /**
            *
            *
            */
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
        
        // 数组用于存储ThreadLocal 和 value的对应关系
        private Entry[] table;
        // 数组table 扩容的 阈值
        private int threshold; // Default to 0
                
        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);
        }
        
        // 设置数组扩容阈值,初始化和扩容后都会调用这个方法
        private void setThreshold(int len) {
            threshold = len * 2 / 3;
        }
    }

ThreadLocalMap.Entry
这个类使用了 WeakReference(弱引用) 来存储 key(ThreadLocal)
因为弱引用不会阻止垃圾回,避免了因为线程类对ThreadLocal的引用导致线程存活的情况下GC无法回收ThreadLocal
这样就会对线程池这种线程复用机制造成困扰,不当的操作更容易造成内存溢出。
弱引用解决了垃圾回收的问题,但带来了另一个问题,jdk开发者们需要维护数组中的过期的key,我们后面会看到这部分代码

ThreadLocal.ThreadLocalMap.set()

// 注意这里只写了有关 ThreadLocal.ThreadLocalMap.set() 的源码 不是全部源码
static class ThreadLocalMap {
    
    // 看过hashmap等 map类实现的都知道,看map容器先看hash
    private final int threadLocalHashCode = nextHashCode();
    
    /**
     * BigDecimal goldenRatioResult = BigDecimal.valueOf(Math.pow(2, 32)).divide(new BigDecimal("1.618"), 0, ROUND_HALF_UP);
     * int hashIncrenment = ~goldenRatioResult.intValue() + 0b1; // 等效于 Math.abs() 结果是 1640531527 也就是十六进制的 0x61c88647
     * 1.618 是 1:0.618,是神奇的黄金分割数。
     * HASH_INCREMENT是根据黄金分隔数计算出来的一个值,使threadLocalHashCode的值之间被HASH_INCREMEN分隔
     * 旨在用这样的hash值生成更均匀的数组下标, 并减少冲突概率
     * 有一篇帖子是专门讲 为什么用0x61c88647这个数的,我就不献丑了文底贴链接
     *
     *
     * 看不懂?没关系只需要记住用这个数作为间隔,生成的hash值计算出来的数组下标更均, 并且冲突几率小
     * 后面称这个数为魔数
     */
    private static final int HASH_INCREMENT = 0x61c88647;

    /**
     * 以HASH_INCREMENT 递增生成hash
     */
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }
    
    private void set(ThreadLocal<?> key, Object value) {
            Entry[] tab = table;
            // len 数组的长度被控制为2的整数倍, len-1 的二进制为 1111111**11,这种有固定规律的格式,
            // 方便通过位运算生成数组下标时排除自身因素造成的冲突
            int len = tab.length;
            // 通过hash值位运算计算出 数组下标
            int i = key.threadLocalHashCode & (len-1);

            // 这里与hashmap不同, 因为ThreadLocal 的特性在合理的架构设计下是不会大规模使用的。
            // 又因为有魔数HASH_INCREMENT = 0x61c88647; 作为分隔。
            // 所以hash取下标的操作 发生冲突的可能性很小,且分布有一定间隔,所以这里干脆用循环查找可用节点的方式解决冲突
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                // 循环检查
                ThreadLocal<?> k = e.get();

                if (k == key) {
                    // 节点为当前 ThreadLocal 直接替换value
                    e.value = value;
                    return;
                }

                if (k == null) {
                    // 过期的数据, 因为key使用的弱引用gc回收之后就是空值, 需要维护清理
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
            // e 等于空直接使用

            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                // 没有成功删除任何过时的节点, 并且当前集合内有效数据长度达到扩容阈值 去扩容
                rehash();
        }
}

这里又出现两个函数

  • replaceStaleEntry 清理过期数据
  • cleanSomeSlots 也是清理过期数据, 并返回是否成功清理了一个以上空间出来
  • rehash 刷新加扩容

至此set函数有营养的部分已经结束,replaceStaleEntry cleanSomeSlots 等函数都是为了清理数据重排数组而封装出来的。
为什么要清理重排数组? 因为上面说了 数组的item是Entry它使用弱引用存储key

后面贴一下相关源码不在详细介绍

/**
 * 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).
    // 找到最靠前的过期数据一次性清理干净
    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) {
            // 发现了当前ThreadLocal 存储在了其他位置 下面进行校准替换
            // 一定是有过期数据没有清理造成的
            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);
}

/**
 * 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.
 *
 * @param i a position known NOT to hold a stale entry. The
 * scan starts at the element after 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.)
 *
 * @return true if any stale entries have been removed.
 */
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];
        if (e != null && e.get() == null) {
            n = len;
            removed = true;
            i = expungeStaleEntry(i);
        }
    } while ( (n >>>= 1) != 0);
    return removed;
}


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

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

get 方法比较简单

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


// ThreadLocalMap map 没初始化 就先进行初始化
// 初始化结束不过瘾, 就先value设置个 null. (真皮)
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;
}

protected T initialValue() {
    return null;
}


// map.getEntry 在这里
private Entry getEntry(ThreadLocal<?> key) {
    // hsah值计算下标 上面已经见识过了
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key)
        // 是我的 value 直接返回
        return e;
    else
        // 遇到了冲突 调用封装方法查找
        return getEntryAfterMiss(key, i, e);
}

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


好了 我们看到 get方法也会map的进行初始化,遇到hash碰撞就去循环递增知道遇到null(代表没找到value),或者找到存储着相同的key的item

remove 更简单

public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}

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

结尾

本文很多内容需要稍微了解map结构的原理才比较好理解,包括散列表 hash碰撞 位运算 二进制。迷糊的朋友们不要紧关注我本周内会出下一篇对hashMap 源码进行分析,其中会详细讲解hashmap的原理

( ⊙ o ⊙ )啊!

关于魔数

发布了17 篇原创文章 · 获赞 24 · 访问量 28万+

猜你喜欢

转载自blog.csdn.net/qq_22956867/article/details/99759473