[Depth analysis] ThreadLocal source and principle

Brief introduction

ThreadLocal each thread its own data structures maintained a storage object, independently of each other to achieve closure thread between threads. When used, examples of ThreadLocal object, call set / get method to obtain an object.

Source code analysis

get / set is how to get hold objects? How acquired objects? What kind of structure again to save? We start from the set / get method analysis

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t); // 根据当前线程获得ThreadLocalMap对象
    if (map != null)
        map.set(this, value); // 如果有则set
    else
        createMap(t, value); // 否则创建ThreadLocalMap对象
}
    
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}
    
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}
复制代码

By getMap method, visible return of our property threadLocals map actually Thread object. And this is ThreadLocalMap structure for storing data.

ThreadLocalMap Profile

ThreadLocalMap is the core of the ThreadLocal, ThreadLocal class is defined in the inner class, he maintained a Enrty array, we call entry. We ThreadLocal load / store operation is initiated by an array Enrty achieved. Enrty arrays are actually a Hash table, we save the package object using the open hashing method to address this array. HashMap with different, linked list method HashMap is hashed into an object array. Opening address method is hashed into an array current location if there is a conflict, then to find some kind of rule can hash the next position in the array, and then use the linear mode detection in ThreadLocalMap backwards in order to find possible hash position.

Enery Introduction

Enery Here we call entry, the hash table is maintained in the unit. Enery object is not null null single key entry called old entries here.

// 哈希映射表中的条目使用其引用字段作为键(它始终是ThreadLocal对象)继承WeakReference。
// 注意,null键(即entry.get()== null)表示不再引用该键,因此可以从表中删除该条目。这些条目在下面的代码中称为“旧条目”。
// 这些“旧条目”就是脏对象,因为存在引用不会被GC,为避免内存泄露需要代码里清理,将引用置为null,那么这些对象之后就会被GC清理。
// 实际上后面的代码很大程度上都是在描述如何清理“旧条目”的引用
static class Entry extends WeakReference<ThreadLocal<?>> {
    Object value;
    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}
复制代码

There may be two questions here

1. Since the content to be stored is unique thread object, why not set up a property directly Thread stored directly in the object? Or why should maintain memory contents to a hash table Entry to ThreadLocal object as a key?

A: A ThreadLocal objects belong to only one thread but a thread can be instantiated ThreadLocal objects. The array is stored ThreadLocalMap to maintain Entry ThreadLocal instance as a key target.

2, ThreadLocalMap of Enery why should inherit WeakReference, and to ThreadLocal object as if references?

A: That is a weak reference objects ThreadLocal strong reference case does not exist, a weak reference object will be cleared at the next GC. ThreadLocal object as the object is to prevent weak reference memory leaks. If the key is not Enery weak reference threadLocal references even in our code has expired, threadLocal will not be GC. Because of the current thread holds a reference ThreadLocalMap, threadLocal target for the current thread up and will not be GC. But even as a weak reference threadLocal been cleared GC, Entry [] entry objects still exist, but key is null, vlue objects still exist, these are dirty objects. Weak references not only to clean up the threadLocal object, it's another layer of meaning that can identify Enery [] array in which one element of this is GC (here referred to as the old entry), then the program was to identify and clean up these entry.

ThreadLocalMap.set方法

Back to the previous set method, when the map is not null ThreadLocalMap will call the set method, it describes how to set the hash value to the hash table. Is the open address detecting method in a linear way hash. After the set value, try to clean up some old entries, if the judge did not find the old entry threshold to determine whether the hash table is large enough, the need for expansion. If the hash table is too crowded, get / set the value of frequent conflict, which is an undesirable situation. Method set as codes and detailed annotation of ThreadLocalMap

private void set(ThreadLocal<?> key, Object value) {
    // We do not 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.
    // 我们不像get()那样先使用快速路径(直接散列)判断
    // 因为使用set()创建新条目至少与替换现有条目一样频繁,在这种情况下,快速路径会更频繁地失败。
    // 所以直接先线性探测
    Entry[] tab = table;
    int len = tab.length;

    // 根据hashcode散列到数组位置
    int i = key.threadLocalHashCode & (len-1);
    // 开放地址法处理散列冲突,线性探测找到可以存放位置
    // 遍历数组找到下一个可以存放条目的位置,这种位置包含三种情况
    // 1.条目的key已存在,直接赋值value
    // 2.条目的key位null,说明k作为弱引用被GC清理,该位置为旧数据,需要被替换
    // 3.遍历到一个数组位置为null的位置赋值
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();

        if (k == key) {//key已存在则直接更新
            e.value = value;
            return;
        }
        if (k == null) { //e不为null但k为null说明k作为弱引用被GC,是旧数据需要被清理
            // i为旧数据位置,清理该位置并依据key/value合理地散列或替换到数组中,重新散列i后面的元素,并顺便清理i位置附近的其他旧条目
            replaceStaleEntry(key, value, i);
            return;
        }
    }
    // 遍历到一个数组位置为null的位置赋值
    tab[i] = new Entry(key, value);
    int sz = ++size;
    // 调用cleanSomeSlots尝试性发现并清理旧条目,如果没有发现且旧条目当前容量超过阈值,则调用rehash
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        // 此时认为表空间不足,全量遍历清理旧条目,清理后判断容量若大于阈值的3/4,若是则扩容并从新散列
        rehash();
}
复制代码

replaceStaleEntry method

replaceStaleEntry method when we linear probing, if you encounter the old entries on the implementation. The method to do more, can be summarized as we find the old entry in the staleSlot position, will cover the new value to staleSlot position and clean up old entries near staleSlot. Code and the following detailed notes

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).
    // 向前检查是否存在旧条目,一次性彻底清理由于GC清除的弱引用key导致的旧数据,避免多次执行
    int slotToExpunge = staleSlot;
    // 向前遍历找到entry不为空且key为null的位置赋值给slotToExpunge
    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
    // staleSlot位置向后遍历如果位置不为空,判断key是否已经存在
    // 回想前面我们是set实例的时候,碰到旧条目的情况下调用该方法,所以很可能在staleSlot后面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.
        // 如果我们找到键,那么我们需要将它与旧条目交换以维护哈希表顺序。
        // 然后可以将交换后得到的旧索引位置或其上方遇到的任何其他旧索引位置传给expungeStaleEntry清理旧条
        // 如果碰到key相同的值则覆盖value
        if (k == key) {
            e.value = value;
            // i位置与staleSlot旧数据位置做交换,将数组条目位置规范化,维护哈希表顺序
            // 这里维护哈希表顺序是必要的,举例来说,回想前面threadLocal.set实例的判断,是线性探测找到可以赋值的位置
            // 如果哈希顺序不维护,可能造成同一个实例被赋值多次的情况
            // 包括后面清理旧条目的地方都要重新维护哈希表顺序
            tab[i] = tab[staleSlot];
            tab[staleSlot] = e;
            // Start expunge at preceding stale entry if it exists
            // 开始清理前面的旧条目
            // 如果前面向前或向后查找的旧条目不存在,也就是slotToExpunge == staleSlot
            //此时slotToExpunge = i,此时位置i的条目是旧条目,需要被清理
            // slotToExpunge用来存储第一个需要被清理的旧条目位置
            if (slotToExpunge == staleSlot)
                slotToExpunge = i;
            // 清理完slotToExpunge位置及其后面非空连续位置后,通过调用cleanSomeSlots尝试性清理一些其他位置的旧条目
            // cleanSomeSlots不保证清理全部旧条目,它的时间复杂度O(log2n),他只是全量清理旧条目或不清理的折中
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
            return;
        }

        // If we do not find stale entry on backward scan, the
        // first stale entry seen while scanning for key is the
        // first still present in the run.
        // 如果前面向前查找的旧条目不存在,也就是slotToExpunge == staleSlot,而此时位置i为旧条目,所以将i赋值给slotToExpunge
        // slotToExpunge用来存储第一个需要被清理的旧条目位置
        if (k == null && slotToExpunge == staleSlot)
            slotToExpunge = i;
    }

    // If key not found, put new entry in stale slot
    // 如果向后遍历非空entry都没有找到key,则直接赋值给当前staleSlot旧条目位置
    tab[staleSlot].value = null;
    tab[staleSlot] = new Entry(key, value);

    // If there are any other stale entries in run, expunge them
    // 通过前面根据staleSlot向前/向后遍历,如果发现有旧条目则清理
    if (slotToExpunge != staleSlot)
        // 清理完slotToExpunge位置及其后面非空连续位置后,通过调用cleanSomeSlots尝试性清理一些其他位置的旧条目
        // cleanSomeSlots不保证清理全部旧条目,它的时间复杂度O(log2n),他只是全量清理旧条目或不清理的折中
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
复制代码

expungeStaleEntry method

Find the old entry will be executed expungeStaleEntry method. expungeStaleEntry frequently used, it is the cell method to clean up old entries. Things to do which is: clean-up position in a row, including back staleSlot empty entries all rehashed old entries and return back staleSlot first empty position. Code and the following detailed notes

private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;

    // expunge entry at staleSlot
    // 清空staleSlot位置的条目
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;

    // Rehash until we encounter null
    // 旧位置清理后,后面的条目需要重新散列到数组里,直到遇到数组位置为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) { // k == null说明此位置也是旧数据,需要清理
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            int h = k.threadLocalHashCode & (len - 1);
            // 将staleSlot后面不为空位置重新散列,如果与当前位置不同,则向前移动到h位置后面(包括h)的首个空位置
            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;
}
复制代码

cleanSomeSlots方法

cleanSomeSlots is a more clever way. As his name suggests "some". This method is only tentatively looking for some old entries. This method will be called upon to add new elements or delete old entries. Its implementation complexity log2 (n), he is "not cleaned up" and "full amount of clean-up" of compromise. If found old entries returns true. Code and the following detailed notes

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);// 无符号右移,即执行次数以n的二进制最高位的1的位置为基准,所以时间复杂度log2(n)
    return removed;
}
复制代码

rehash/expungeStaleEntries/resize方法

After we set the value by threshold determination, if the program is considered insufficient table space will be called rehash method. rehash do two things, first of all traverse the full amount of clean up old entries and then clean up after judging capacity is adequate, if the establishment is two times the expansion and re-hash expungeStaleEntries is the total amount of clean up old entries, resize is twice the expansion.

// rehash全量地遍历清理旧条目,然后判断容量若大于阈值的3/4,则扩容并从新散列
// 程序认为表空间不足时会调用该方法
private void rehash() {
    // 全量遍历清理旧条目
    expungeStaleEntries();
    // Use lower threshold for doubling to avoid hysteresis
    // 适当的扩容,以避免hash散列到数组时过多的位置冲突
    if (size >= threshold - threshold / 4)
        // 2倍扩容并重新散列
        resize();
}

// 全量遍历清理旧条目
private void expungeStaleEntries() {
    Entry[] tab = table;
    int len = tab.length;
    for (int j = 0; j < len; j++) {
        Entry e = tab[j];
        if (e != null && e.get() == null)
            expungeStaleEntry(j);
    }
}

// 二倍扩容
private void resize() {
    Entry[] oldTab = table;
    int oldLen = oldTab.length;
    int newLen = oldLen * 2;
    Entry[] newTab = new Entry[newLen];
    int count = 0;

    for (int j = 0; j < oldLen; ++j) {
        Entry e = oldTab[j];
        if (e != null) {
            ThreadLocal<?> k = e.get();
            if (k == null) {
                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 get methods

ThreadLocal's get logical comparison set is much simpler. He simply hashes threadLocal objects into the array, matches the value found by way of the linear detection. Code and the following detailed notes

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;
        }
    }
    // 如果map不为null初始化一个key为当前threadLocal值为null的ThreadLocalMap对象
    return setInitialValue();
}

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 // 直接散列找不到的情况,调用getEntryAfterMiss线性探测查找期望条目
        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;
}
复制代码

remove method

remove references to empty and about to call to clean up old entry method. So it will not remove an entry, when we confirm the priority when using the remove method to clean up what needs to be removed.

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

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;
        }
    }
}
复制代码

to sum up

ThreadLocal biggest complexity is how to deal with the old entry, the purpose is to avoid memory leaks. In order to find that little time as possible in the case of old have set an entry, call cleanSomeSlots clean up old entries after several attempts to clean up some of the older entries, the efficiency of which is not to clean up and clean up the whole amount of a balance between mind. expungeStaleEntry cleaning up old entries on their own position but also clean up old entries neighborhood, was found to reduce both the situation on the entry. Even so, when the hash table full amount of excess capacity will clean up again the old entry and expansion.

Guess you like

Origin juejin.im/post/5cf75a995188254628166745