ThreadLocal 实现原理

简介: ThreadLocal 实现原理
ThreadLocal 实现原理


ThreadLocalget(),set()的时候都会清除线程ThreadLocalMap里所有keynullvalue。而ThreadLocal的remove()方法会先将Entry中对key的弱引用断开,设置为null,然后再清除对应的key为null的value。

每一个Thread维护一个ThreadLocalMap映射表,映射表的keyThreadLocal实例,并且使用的是ThreadLocal的弱引用 ,value是具体需要存储的Object。下面用一张图展示这些对象之间的引用关系,实心箭头表示强引用,空心箭头表示弱引用。

image.png

ThreadLocal local = new ThreadLocal();
local.set("当前线程名称:"+Thread.currentThread().getName());//将ThreadLocal作为key放入threadLocals.Entry中
Thread t = Thread.currentThread();//注意断点看此时的threadLocals.Entry数组刚设置的referent是指向Local的,referent就是Entry中的key只是被WeakReference包装了一下
local = null;//断开强引用,即断开local与referent的关联,但Entry中此时的referent还是指向Local的,为什么会这样,当引用传递设置为null时无法影响传递内的结果
System.gc();//执行GC
t = Thread.currentThread();//这时Entry中referent是null了,被GC掉了,因为Entry和key的关系是WeakReference,并且在没有其他强引用的情况下就被回收掉了
//如果这里不采用WeakReference,即使local=null,那么也不会回收Entry的key,因为Entry和key是强关联
//但是这里仅能做到回收key不能回收value,如果这个线程运行时间非常长,即使referent GC了,value持续不清空,就有内存溢出的风险
//彻底回收最好调用remove
//即:local.remove();//remove相当于把ThreadLocalMap里的这个元素干掉了,并没有把自己干掉
System.out.println(local);


使用InheritableThreadLocal,ThreadLocal threadLocal = new InheritableThreadLocal(),这样在子线程中就可以通过get方法获取到主线程set方法设置的值了。

ThreadLocalMap 结构


static class ThreadLocalMap {
 /**
  * 键值对实体的存储结构
  */
 static class Entry extends WeakReference<ThreadLocal<?>> {
  // 当前线程关联的 value,这个 value 并没有用弱引用追踪
  Object value;
  /**
   * 构造键值对
   *
   * @param k k 作 key,作为 key 的 ThreadLocal 会被包装为一个弱引用
   * @param v v 作 value
   */
  Entry(ThreadLocal<?> k, Object v) {
   super(k);
   value = v;
  }
 }
 // 初始容量,必须为 2 的幂
 private static final int INITIAL_CAPACITY = 16;
 // 存储 ThreadLocal 的键值对实体数组,长度必须为 2 的幂
 private Entry[] table;
 // ThreadLocalMap 元素数量
 private int size = 0;
 // 扩容的阈值,默认是数组大小的三分之二
 private int threshold;
}

Java 不同的是,采用的是线性探测法。


private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;
    // expunge entry at staleSlot
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;     // 以上代码,将entry的value赋值为null,这样方便GC时将真正value占用的内存给释放出来;将entry赋值为null,size减1,这样这个slot就又可以重新存放新的entry了
    // Rehash until we encounter null
    Entry e;
    int i;
    for (i = nextIndex(staleSlot, len); // 从staleSlot后一个index开始向后遍历,直到遇到为null的entry
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
        if (k == null) {    // 如果entry的key为null,则清除掉该entry
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            int h = k.threadLocalHashCode & (len - 1);
            if (h != i) {   // key的hash值不等于目前的index,说明该entry是因为有哈希冲突导致向后移动到当前index位置的
                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)      // 对该entry,重新进行hash并解决冲突
                    h = nextIndex(h, len);
                tab[h] = e;
            }
        }
    }
    return i;   // 返回经过整理后的,位于staleSlot位置后的第一个为null的entry的index值
}

在从第i个entry向后遍历的过程中,找到对应的key的entry就直接返回,如果遇到key为null的entry,则调用expungeStaleEntry方法进行清理。

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

ThreadLocalMap remove 方法


找到准确的key对应的entry之后,调用Entry的clear方法,紧接着调用expungeStaleEntry,对key为null的entry进行清理。

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) {       // 考虑到可能的哈希冲突,一定要准确找到此key对应的entry
            e.clear();  // 调用Entry的clear方法,见代码2
            expungeStaleEntry(i);       // 又是这个清除key为null的entry的方法,见代码3
            return;
        }
    }
}

平时怎么使用

看个测试代码

package thread;
public class ThreadLocalDemo {
    /**
     * ThreadLocal变量,每个线程都有一个副本,互不干扰
     */
    public static final ThreadLocal<String> THREAD_LOCAL = new ThreadLocal<>();
    public static void main(String[] args) throws Exception {
        new ThreadLocalDemo().threadLocalTest();
    }
    public void threadLocalTest() throws Exception {
        // 主线程设置值
        THREAD_LOCAL.set("mainthreadvalue");// 设置的key 是主线程
        String v = THREAD_LOCAL.get();
        System.out.println("Thread-0线程执行之前," + Thread.currentThread().getName() + "线程取到的值:" + v);
        new Thread(new Runnable() {
            @Override
            public void run() {
                String v = THREAD_LOCAL.get(); // 获取的是key 是当前线程 渠道的是 null
                System.out.println(Thread.currentThread().getName() + "线程取到的值:" + v);
                // 设置 threadLocal
                THREAD_LOCAL.set("inThreadValue"); // 设置的 key 是当前咸亨
                v = THREAD_LOCAL.get();
                System.out.println("重新设置之后," + Thread.currentThread().getName() + "线程取到的值为:" + v);
                System.out.println(Thread.currentThread().getName() + "线程执行结束");
            }
        }).start();
        // 等待所有线程执行结束
        Thread.sleep(3000L);
        v = THREAD_LOCAL.get();
        System.out.println("Thread-0线程执行之后," + Thread.currentThread().getName() + "线程取到的值:" + v);
    }
}

执行结果:

Thread-0线程执行之前,main线程取到的值:mainthreadvalue
Thread-0线程取到的值:null
重新设置之后,Thread-0线程取到的值为:inThreadValue
Thread-0线程执行结束
Thread-0线程执行之后,main线程取到的值:mainthreadvalue

面试指南


面试官:那ThreadLocal中弱引用导致的内存泄漏是如何发生的?

小白:如果ThreadLocal没有外部强引用,当发生垃圾回收时,这个ThreadLocal一定会被回收(弱引用的特点是不管当前内存空间足够与否,GC时都会被回收),这样就会导致ThreadLocalMap中出现key为null的Entry,外部将不能获取这些key为null的Entry的value,并且如果当前线程一直存活,那么就会存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value,导致value对应的Object一直无法被回收,产生内存泄露。

面试官:如何解决?

小白:查看源码会发现,ThreadLocal的get、set和remove方法都实现了对所有key为null的value的清除,但仍可能会发生内存泄露,因为可能使用了ThreadLocal的get或set方法后发生GC,此后不调用get、set或remove方法,为null的value就不会被清除。解决办法是每次使用完ThreadLocal都调用它的remove()方法清除数据,或者按照JDK建议将ThreadLocal变量定义成private static,这样就一直存在ThreadLocal的强引用,也就能保证任何时候都能通过ThreadLocal的弱引用访问到Entry的value值,进而清除掉。

新网银行内推


base: 成都高新区, 新网银行,待遇从优,社招,校招均可,欢迎投递


相关文章
|
6月前
|
存储 安全 Java
面试题:用过ThreadLocal吗?ThreadLocal是在哪个包下的?看过ThreadLocal源码吗?讲一下ThreadLocal的get和put是怎么实现的?
字节面试题:用过ThreadLocal吗?ThreadLocal是在哪个包下的?看过ThreadLocal源码吗?讲一下ThreadLocal的get和put是怎么实现的?
79 0
|
3月前
|
存储 设计模式 安全
深入理解ThreadLocal原理
本文深入探讨了Java中的ThreadLocal及其内部数据结构ThreadLocalMap的工作原理和特性,帮助读者理解如何利用ThreadLocal实现线程局部变量的隔离和线程安全。
深入理解ThreadLocal原理
|
4月前
|
存储 算法 Java
ThreadLocal 源码浅析
【7月更文挑战第5天】`ThreadLocal` 是Java中用于创建线程局部变量的工具类,确保每个线程拥有独立的变量副本。源码中,每个`ThreadLocal`实例都有一个唯一的哈希码用于映射到`ThreadLocalMap`,这个内部静态类使用弱引用存储键(`ThreadLocal`实例)以防止内存泄漏。`ThreadLocalMap`使用 Entry 数组,Entry 是一个扩展了 WeakReference 的类,持有线程变量值。`ThreadLocal`的`get()`和`set()`方法通过哈希计算定位并访问或设置线程局部变量。
|
6月前
|
存储 安全 Java
ThreadLocal原理讲解
ThreadLocal原理讲解
56 0
|
存储 Java
大厂是怎么用ThreadLocal?ThreadLocal核心原理分析
ThreadLocal**是Java中的一个线程本地变量类。它可以让每个线程都有自己独立的变量副本,而不会相互影响。
119 1
|
存储 SpringCloudAlibaba Java
浅析ThreadLocal使用及实现原理
提供了线程局部 (thread-local) 变量。这些变量不同于它们的普通对应物,因为访问某个变量(通过其`get` 或 `set`方法)的每个线程都有自己的局部变量,它独立于变量的初始化副本。`ThreadLocal`实例通常是类中的 private static 字段,它们希望将状态与某一个线程(例如,用户 ID 或事务 ID)相关联 。所以ThreadLocal与线程同步机制不同,线程同步机制是多个线程共享同一个变量,而ThreadLocal是为每一个线程创建一个单独的变量副本,故而每个线程都可以独立地改变自己所拥有的变量副本,而不会影响其他线程所对应的副本。可以这么说Th
109 0
浅析ThreadLocal使用及实现原理
|
存储 Java 数据安全/隐私保护
ThreadLocal的实现原理&源码解析
ThreadLocal是Java中的一个线程封闭机制,它提供了一种线程局部变量的解决方案,可以使每个线程都拥有自己独立的变量副本,互不干扰。
94 0
|
存储 算法 安全
ThreadLocal原理剖析
ThreadLocal原理剖析
222 0
|
存储 安全 Java
ThreadLocal源码分析
ThreadLocal,即线程局部变量。主要用于线程间数据隔离。这些变量在多线程环境下访问(通过get或set方法访问)时能保证各个线程里的变量相对独立于其他线程内的变量,ThreadLocal实例通常来说都是private static类型。ThreadLocal不是为了解决多线程访问共享变量,而是为每个线程创建一个单独的变量副本,提供了保持对象的方法和避免参数传递的复杂性。
|
Java 定位技术
ThreadLocal原理
经典八股文之ThreadLocal原理
193 0