写点什么

深入浅出 ThreadLocal

  • 2023-03-25
    湖南
  • 本文字数:3350 字

    阅读完需:约 11 分钟

ThreadLocal 相信大家都有用过的,一般用作存取一些全局的信息。比如用户信息,流程信息,甚至在 Spring 框架里面通过事务注解 Transactional 去获取数据库连接的实现上,也有它的一份功劳。


ThreadLocal 作为一个进阶必会知识点,而且还是面试高频考点。网上博客对它的解读也必然不会少,但是网上博客解读水平良莠不齐,看多了难免会绕。不如自己亲自再梳理一遍,顺便记录下自己的解读。

ThreadLocal 的线程隔离性 Demo

先来看一个小的 demo

static ThreadLocal<Student> threadLocal = new ThreadLocal<Student>();
public static void main(String[] args) { threadLocalTest1();}
private static void threadLocalTest1() { new Thread(new Runnable() { public void run() { try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(threadLocal.get()); } }).start();

new Thread(new Runnable() { public void run() { try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } threadLocal.set(new Student("zhangsan")); } }).start();}
复制代码

如代码所示,开启两个线程。

  • 第一个线程 3 秒之后去取在静态变量 threadLocal 里的变量。

  • 第二个线程 1 秒之后去设置 threadLocal 里的变量。


这段代码运行的结果就是,第一个线程永远获取不到第二个线程给静态变量 threadLocal 里设置的变量。


  • 结论:不同的线程操作同一个 threadLocal 对象,可以实现线程信息之间的隔离。

  • 猜想:看到 set 方法和 get 方法,大胆猜想 threadLocal 对象里面有个 Map,key 为当前线程,value 为 ThreadLocal 泛型里的对象,这样就实现了在空间上的线程安全性。


但事实并不是这样,答案不在猜想中,而在源码中。

ThreadLocal.set()方法源码解读

public void set(T value) {    // 获取当前线程    Thread t = Thread.currentThread();    // 获取当前线程的ThreadLocalMap    ThreadLocalMap map = getMap(t);    if (map != null)        // 把当前ThreadLocal对象作为key,调用set方法的入参对象作为value,放入当前线程的ThreadLocalMap        map.set(this, value);    else        // 通过当前线程和调用set方法的入参对象去构造Map        createMap(t, value);}
复制代码

ThreadLocal.get()方法源码解读

public T get() {    // 获取当前线程    Thread t = Thread.currentThread();    // 获取当前线程的ThreadLocalMap    ThreadLocalMap map = getMap(t);    if (map != null) {	// 通过当前ThreadLocal对象去取Entry	ThreadLocalMap.Entry e = map.getEntry(this);	if (e != null) {	    @SuppressWarnings("unchecked")	    // 获取Entry的value返回	    T result = (T)e.value;	    return result;	}    }    return setInitialValue();}
复制代码

那么 ThreadLocalMap 又是什么呢?

再点进源码,你会发现,ThreadLocalMap 是 ThreadLocal 的一个静态内部类,同时在 Thread 类下有一个成员变量 ThreadLocals

ThreadLocal.ThreadLocalMap threadLocals = null
复制代码

那我们顺着逻辑再下去看看 ThreadLocalMap 的 set 方法用来干嘛的。

ThreadLocalMap.set()方法源码解读

private void set(ThreadLocal<?> key, Object value) {	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;		}	}	// 设置Entry	tab[i] = new Entry(key, value);	int sz = ++size;	if (!cleanSomeSlots(i, sz) && sz >= threshold)		rehash();}
复制代码

其实我们在知道了 HashMap 的底层原理实现的基础上去理解上述代码并不难,取 Entry 数组长度,哈希,与运算取模,设置 Entry 一气呵成。


但是这里需要注意的是,也是 ThreadLocal 最有特色的一点,是这个 Entry 并不是普通理解里的 Entry,而是 ThreadLocalMap 里面的一个静态内部类并且继承了 WeakReference


static class Entry extends WeakReference<ThreadLocal<?>> {	Object value;
Entry(ThreadLocal<?> k, Object v) { super(k); value = v; }}
复制代码

到此,是不是对几个带有 Thread 的名词弄的有点晕,休息一下,我们先来看张图梳理一下 ThreadLocal,ThreadLocalMap,与 Thread 的关系,如图:

层层深入,这时候问题又来了,弱引用是啥?啥是弱引用?弱什么?什么引用?

WeakReference 演示

public static void main(String[] args) {    wrTest();}
private static void wrTest() { WeakReference<Student> weakReference = new WeakReference<Student>(new Student("aaaa")); System.out.println(weakReference.get()); System.gc(); System.out.println(weakReference.get());}
复制代码

输出的结果为:

Student@1b6d3586null
复制代码

在这个 demo 里面,我们可以很清晰的看到弱引用的特性:当 JVM 进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。


当垃圾回收器执行一次之后,原来的弱引用关联的对象就为 null 了,它拥有这样的特质,又有什么用呢?

这就得引出内存泄漏的问题了。

内存泄漏

使用 ThreadLocal 哪些情况会产生内存泄漏?


调用 ThreadLocal 的 set 方法设置某个对象进去,后来这个对象回收不了。久而久之,影响程序运行速度,最终造成 OOM。


为什么会回收不了?是因为垃圾回收器执行之后,CurrentThread 依然运行的前提下,Entry[]一直存在,但是其中有些 key 由于是继承了 WeakReference,在 GC 之后其 get 方法返回值就是 null 了,导致取不到 Entry 里面 key.get()为 null 的 KEY 所对应的 value,而这块 value 永远也访问不到了。如图:

使用 ThreadLocal 如何避免内存泄漏?


把 ThreadLocal 定义为 static,保持单例,不被回收。


用完 ThreadLocal,需要手动擦除对应的 Entry 节点信息,记得调用 ThreadLocal 的 remove 方法。


特别是在实际项目的场景下,大多数情况下线程都是交给线程池在管理。一个线程任务跑完,通常不会立即销毁,而是放在线程池里面等待下次任务的来临(有种说法说是在把线程放回线程池的过程中会擦除 Thread 下的 ThreadLocal.ThreadLocalMap threadLocals 信息,当然,这是线程池帮我们做的)无论线程池是否帮我们擦除,我们用完 ThreadLocal 手动 remove 总是安全的。


附上 remove 的源码:

public void remove() {	// 获取当前线程的ThreadLocalMap	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)]) {         // 匹配key         if (e.get() == key) {    	 // 清除一个Entry元素    	 e.clear();    	 expungeStaleEntry(i);    	 return;        }    }}
复制代码

总结

解读完 ThreadLocal 的源码,再回归到它的命名,理解又深了一个层次:


Thread + Local = 线程 + 本地 = 线程本地变量 = 把某个对象放在了线程本地。


文理不分家,不妨借用文科的思维打比方去理解它:


ThreadLocal 对象就像一个具体的客观的对象,可以是某个话题,某部电影,某本书,甚至某个人。


而每个 Thread 就像一个人,读者,旁观者。


Thread 对 ThreadLocal 的 set 操作和 get 操作,就分别对应是一个人对某个客观的对象进行设置主观印象和获取主观印象。即便是同一个对象,不同的人会对其有不同的主观印象并记录在自己的脑海里,在每个人来看这些印象都是合理的,无论你处在哪个上下文,总能快速获取到这个印象信息,而不会错乱。


在空间上保留对象的副本,通过空间换时间的思想,也就实现了 ThreadLocal 的线程安全性。


作者:厨师沙拉

链接:https://juejin.cn/post/7208830813886283837

来源:稀土掘金

用户头像

还未添加个人签名 2021-07-28 加入

公众号:该用户快成仙了

评论

发布
暂无评论
深入浅出ThreadLocal_Java_做梦都在改BUG_InfoQ写作社区