6

ThreadLocal底层源码解析 - 不会上猪的树

 8 months ago
source link: https://www.cnblogs.com/blissful/p/17929221.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

ThreadLocal底层源码解析

ThreadLocal:顾名思义的意思是本地线程或者局部线程的意思,其真正含义是希望多个线程之间拥有自己的局部变量,多个线程间拥有自己的私人变量,在多线程间不被共享,被线程单独享用,这就是ThreadLocal设计之初的原衷

因此,无论是操作系统级别还是编程语言中,我们都能看到ThreadLocal的设计实现.

1.ThreadLocal原理

ThreadLocal如何实现线程隔离?

具体来说,ThreadLocal在每个线程中维护了一个ThreadLocalMap对象。ThreadLocalMap是一个散列表,其中键是ThreadLocal变量的引用,值是ThreadLocal变量的值。

具体来看Get方法的实现:

1.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();
    }
  1. 获取当前线程,并获取当前线程的散列映射,也就是存储value的地方,根据当前散列映射判断键值对是否存在,不存在则说明未初始化,之后调用getEntry方法获取ThreadLocalMap的entry,也就是存放键和值的地方,至于这个键值是什么,后面再看,如果没有拿到,就同时去进行初始化setInitialvalue.

1.2.setInitialValue初始化方法

在一开始线程的局部变量没初始化设置好的情况下,这个方法是一定会被调用的,因此了解他的内部实现是有必要的

 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);
        }
        if (this instanceof TerminatingThreadLocal) {
            TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
        }
        return value;
    }
  1. 先初始化他内部的值,这个值默认情况下为null
  2. 然后获取当前线程,根据当前线程获取他的ThreadLocalMap
  3. 如果ThreadLocalMap存在则进行初始化赋值,如果不存在则创造
  4. 最后返回value.
  • :ThreadLocal并不是传统意义上的散列映射
  • set方法的实现类似

1.3.ThreadLocalMap的具体实现

static class ThreadLocalMap {

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

ThreadLocalMap是一个静态内部类,同样内部也包含一个静态内部类Entry,实现其真正value的存储方式,并继承弱引用,因此Entry的真正的实现是一个简单的Object的对象去存储的value,除此之外还包括几个重要成员对象

        /**
         * The initial capacity -- MUST be a power of two.
         */
        private static final int INITIAL_CAPACITY = 16;

        /**
         * The table, resized as necessary.
         * table.length MUST always be a power of two.
         */
        private Entry[] table;

        /**
         * The number of entries in the table.
         */
        private int size = 0;

        /**
         * The next size value at which to resize.
         */
        private int threshold; // Default to 0

用来修饰散列表的一些重要字段,散列表的真正实现是这个table数组,而每一个Entry中的value就是其Object对象,根据其内部构造实现,便可以明白这个K也就是键是其ThreadLocal对象本身,也就是这个引用

  • 接下来就是面对这个散列表时,value的具体存储实现了

1.4.ThreadLocalMap的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();
        }
  1. 简言之就是获取当前ThreadLocal对象的散列映射,然后根据当前ThreadLocal计算哈希值确认索引位置
  2. 获取索引位置对应的Entry数组的对象位置,依次向下nextIndex循环,每次循环拿到对应Entry对象,如果有相同的哈希值的Entry对象,则将Entry对象的值赋值为value,如果找不到相同哈希值的Entry对象,那么调用replaceStaleEntry去替换旧的Entry对象
  3. 最后size++(前提是e!=null,也就是说Entry对象不存在,就不会在循环里面走,立刻跳出,赋值一个新的Entry对象),同时判断size字段是否大于threshold字段,并且cleanSomeSlots方法返回false,则进行rehash方法重新哈希数组

为什么需要调用cleanSomeSlots方法:这个方法的目的是为了清除一些旧的value对象,也就是Entry对象,底层他会去部分遍历这个散列表,直到n的值为0,也就是sz的值为0,也就是为了避免内存的占用,至于内存泄露后面再将


1.5.ThreadLocalMap的remove方法

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

其思路很清晰,就是查找相同Entry对象,然后进行清除clear,这个方法实际上是将referent这个字段设置为null,是Reference中的一个字段,用来帮助我们进行GC回收的,expungeStaleEntry方法则是真正用来帮我们进行Entry对象和值的回收,设置为null.因此调用clear方法实际上就显式地回收了我们弱引用关联的对象,避免了内存泄漏的问题.而这个referent实际上也就是我们一开始对Entry对象进行初始化的ThreadLocal这个键.


1.6.ThreadLocalMap的getEntry方法

这个方法是真正去查找ThreadLocalMap中对饮Entry对象的方法,具体实现如下:

   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);
        }
  • 根据哈希运算得到对应的索引位置,查找对应的Entry对象,如果找到符合条件就返回,如果没有就调用getEntryAfterMiss方法
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;
        }
  • 这个方法其实也就是哈希表的开放地址法,没有类似HashMap去采用链地址开放去解决哈希冲突,因此会一次次向下去寻找,如果发现Entry对象的k为null,那么调用expungeStaleEntry方法,这个方法在此之前也出现过,简单的英文释义是:擦去稳定的Entry对象,具体实现后面在看.

1.7.ThreadLocalMap的expungeStaleEntry方法

这个方法的设计比较重要,主要是用于清除没有用的ThreadLocal,还有进行重新哈希的一个过程,具体实现如下:

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;
        }
  1. 先将索引位置的key,value置空
  2. 第二步就是rehash去进行清除运算
  3. 所谓的清除就是对Entry对象的key为null的Entry对象进行一个回收,所谓的运算就是因为在set方法中,解决哈希冲突的实现是通过开放地址法去解决的,因此在某些Entry对象进行清理之后,这些对象的索引位置重新进行安排

1.8.ThreadLocalMap的cleanSomeSlots部分清除方法

在set方法中,这个方法有使用到,他的真正含义就是去部分清除一些对象

 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;
        }
  1. 参数解释
    • int i:起始索引,指明从哪个位置开始检查。
    • int n:控制扫描范围的参数,方法会扫描大约n/2个插槽。
  2. 循环扫描
    • 方法使用一个do-while循环来遍历哈希表的一部分。
    • 在每次迭代中,它使用nextIndex(i, len)来移动到下一个索引。
    • 如果发现任何条目的ThreadLocal引用为null(意味着没有线程再使用它),则调用expungeStaleEntry(i)来清理这个条目,并重置扫描范围(n = len)。

可以看到这是一种均衡策略,在清除和时间效率上做出的一种决策,如果发现有引用为null的情况,就可能存在垃圾的问题,因此需要去重新调用expungeStaleEntry方法进行一个清除,因此这个方法的清理类似抽样调查

为什么在set方法最后,如果添加了一个新的Entry就需要去调用这个方法?

我的理解是这样的,如果没有定期去清除,就不能确保哈希表的健康和效率,只添加元素而不做任何监控,这对于任何一件事情来说都是一种不可控的风险.因此在时间上,对于我们整个Entry而言,也是局部抽样的方式去进行检查


1.9ThreadLocalMap的replaceStaleEntry方法

这个方法的实现相对于其他方法要复杂很多,其核心思想就是进行Entry的替换

具体实现如下:

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

rehash之前的几步都很明白,不过多讲解,直接从Rehash看起

  • 我们要去思考在key为null的情况下,为什么会调用这个方法
  1. 可以看到这个for循环是向前遍历的,这是一个大前提,在第一个for循环中向前查找第一个key为null的情况,因为一旦遇到Entry对象存在的情况,就会退出,因此回过头去看set方法时是向后遍历,与这里是反向,也就是调用这个方法的前提是此Entry对象前的Entry对象的key存在且不是我要替换的key(也就不是相同的哈希值),但是由于此时此Entry对象前存在的对象如果发生GC的情况下,此时我们存放此Entry对象的位置应该放在前面那一个位置

    ThreadLocal-17035769147052.png
  2. 因此第一遍扫描是解决了一个同一时间的问题,那下一次for循环又是解决什么问题,如果在我们对索引为2的位置(图中为null的第一个区域)此时进行了安置,然后前一项发生了GC进行了处理.现在来看循环退出条件是Entry对象不为null,也就是说如果遇到适合的位置,为null,则直接进行赋值,和我们的set方法有相似之处,如果没有说明此时的位置可能存在Entry对象了,后续操作就是如果找到了相同的Key,那么进行替换,同时if (slotToExpunge == staleSlot)如果满足,说明 staleSlot 之前没有需要清理的元素,那么就将 slotToExpunge 设置 i,意思是从当前元素开始进行清理,因为如果staleSlot之前的位置有需要清理的元素,两者就不会相等(参考第一个for循环).


2.Thread与ThreadLocal与ThreadLocalMap的关系

废话不多说,上图:

ThreadLocalMap2.png

示例:

public class ThreadLocalExample {

    // 创建三个线程局部变量
    private static final ThreadLocal<Integer> threadLocalVar1 = new ThreadLocal<>();
    private static final ThreadLocal<String> threadLocalVar2 = new ThreadLocal<>();
    private static final ThreadLocal<Boolean> threadLocalVar3 = new ThreadLocal<>();

    public static void main(String[] args) {
        // 线程1
        new Thread(() -> {
            threadLocalVar1.set(100);
            threadLocalVar2.set("Hello");
            threadLocalVar3.set(true);

            System.out.println("Thread 1: " + threadLocalVar1.get());
            System.out.println("Thread 1: " + threadLocalVar2.get());
            System.out.println("Thread 1: " + threadLocalVar3.get());
        }).start();

        // 线程2
        new Thread(() -> {
            threadLocalVar1.set(200);
            threadLocalVar2.set("World");
            threadLocalVar3.set(false);

            System.out.println("Thread 2: " + threadLocalVar1.get());
            System.out.println("Thread 2: " + threadLocalVar2.get());
            System.out.println("Thread 2: " + threadLocalVar3.get());
        }).start();
    }
}

因此我们实际上发现其实ThreadLocal可以在不同的线程之间进行复用,只不过这个具体存储的value只和每个线程独有的Entry有关.


3.ThreadLocal的内存泄露问题

这个问题看到这里其实就可以很容易的理解了,因为对于Entry对象而言,他的key作为ThreadLocal引用,是一个弱引用对象,也就是说当ThreadLocal对象没有在被强引用对象引用的时候,当触发GC就会进行垃圾回收,但Entry对象中的value对象也就是Object对象是未被回收的一个状态,就可能导致内存垃圾的存在,导致内存泄漏问题.

如何解决内存泄漏问题

在次之前我们看到了如果手动调用remove方法是可以避免内存泄漏的问题,因此最简单的方法就是手动调用remove方法进行垃圾回收.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK