写点什么

ThreadLocal 底层源码解析

  • 2024-01-02
    福建
  • 本文字数:8037 字

    阅读完需:约 26 分钟

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


获取当前线程,并获取当前线程的散列映射,也就是存储 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计算哈希值确认索引位置


  1. 获取索引位置对应的Entry数组的对象位置,依次向下nextIndex循环,每次循环拿到对应Entry对象,如果有相同的哈希值的Entry对象,则将Entry对象的值赋值为 value,如果找不到相同哈希值的Entry对象,那么调用replaceStaleEntry去替换旧的Entry对象


  1. 最后 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 置空


  1. 第二步就是rehash去进行清除运算


  1. 所谓的清除就是对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个插槽。


  1. 循环扫描:方法使用一个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 的情况下,为什么会调用这个方法


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



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


2.Thread 与 ThreadLocal 与 ThreadLocalMap 的关系


废话不多说,上图:



示例:


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 方法进行垃圾回收.


文章转载自:不会上猪的树

原文链接:https://www.cnblogs.com/blissful/p/17929221.html

体验地址:http://www.jnpfsoft.com/?from=001

用户头像

还未添加个人签名 2023-06-19 加入

还未添加个人简介

评论

发布
暂无评论
ThreadLocal底层源码解析_Thread_快乐非自愿限量之名_InfoQ写作社区