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值,进而清除掉。

相关文章
|
存储 安全 Java
【ThreadLocal】
【ThreadLocal】
|
3月前
|
Java 测试技术 索引
ThreadLocal详解
文章详细讨论了Java中的`ThreadLocal`,包括它的基本使用、定义、内部数据结构`ThreadLocalMap`、主要方法(set、get、remove)的源码解析,以及内存泄漏问题和避免策略。`ThreadLocal`提供了线程局部变量,确保多线程环境下各线程变量的独立性,但不当使用可能导致内存泄漏,因此建议在不再需要`ThreadLocal`变量时调用其`remove`方法。
103 2
ThreadLocal详解
|
4月前
|
存储 Java
ThreadLocal应用及理解
ThreadLocal应用及理解
45 10
|
5月前
|
Java
ThreadLocal 场景题
ThreadLocal 场景题
35 1
|
6月前
|
存储 Java
ThreadLocal 有什么用
ThreadLocal 有什么用
52 0
|
存储 安全 Java
ThreadLocal介绍和应用
ThreadLocal介绍和应用
63 0
|
缓存 安全 Java
浅谈ThreadLocal
浅谈ThreadLocal
146 0
|
存储 分布式计算 安全
什么是ThreadLocal?
这篇文章是慕课网上一门免费课程《ThreadLocal》的观后总结。这门课将ThreadLocal讲得非常清晰易懂,又深入底层原理和设计思想,是我看过的最好的ThreadLocal的资料,现在把用自己的话,把它整理成文字版本。 总共预计产出四篇文章,这是第一篇。
266 3