当前位置: 代码迷 >> 综合 >> JUC-2.1-ThreadLocal-原理解析
  详细解决方案

JUC-2.1-ThreadLocal-原理解析

热度:55   发布时间:2023-12-05 18:30:48.0

上文中了解了 ThreadLocal 的常用场景和最基本的用法之后再来看一下它的原理以及一些要注意的事项

Thread,ThreadLocal,ThreadLocalMap

首先需要了解一下 Thread ,ThreadLocal ,ThreadLocalMap 这三个类之间的关系,如图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aHaZZBms-1587203252906)(http://r.photo.store.qq.com/psc?/V10eEnSd0rz4Am/2aGbA7qLSN6GeC6g0ZsuRdjkL3IzX7.otgZsdQYxV37snLwKeveC*AxmmepXmsdf3Fpg20aroYGqmn5DwkryGXicKfvuFgMeAkmLyOo.GV0!/r)]

就是一个线程 (Thread) 对应有一个 ThreadLocalMap , ThreadLocalMap 中以键值对的形式存放着一堆的 ThreadLocal

我们可以看一下具体对应的代码

Thread类:
在这里插入图片描述
然后再点进去 ThreadLocalMap 类里
在这里插入图片描述
可以看到 ThreadLocalMap 这里存储的是一个键值对 Entry 类型的

重要方法解析

initialValue ,设置初始化值的方法 。
1.该方法会返回当前线程的“初始值”,这是一个延迟加载的方法,之后第一次调用get()的时候,才触发 。
2.当线程第一次使用get方法访问变量的时候,将调用 initialValue方法,除非线程先前调用了set方法,这样的话,不会在调用initialValue方法。
get方法是先取出当前线程的ThreadLocalMap,然后调用map.getEntry方法,把本ThreadLocal的引用作为参数传入,取出map中属于本ThreadLocal的value

首先看一下源码

public T get() {//获取当前线程Thread t = Thread.currentThread();//获取当前线程的 ThreadLocalMapThreadLocalMap map = getMap(t);if (map != null) {ThreadLocalMap.Entry e = map.getEntry(this);if (e != null) {@SuppressWarnings("unchecked")//获取ThreadLocal的值T result = (T)e.value;return result;}}//调用初始化方法return setInitialValue();}
private T setInitialValue() {//调用初始化方法,获取返回值T value = initialValue();//获取当前线程Thread t = Thread.currentThread();//根据线程 获取对应 ThreadLocalMap ThreadLocalMap map = getMap(t);// 如果map不为空,那么将初始化后的值也就是ThreadLocal放入mapif (map != null)map.set(this, value);elsecreateMap(t, value);return value;}

总体来说这个方法就是给 ThreadLocal 去设置值的, 这里首先会调用 initialValue 这个方法,这个是不是很熟悉, 就是我们上文中的案例中初始化 ThreadLocal 时,重写的方法,默认是返回空的
在这里插入图片描述
先去Map中找,没有的话再去初始化,从这里也可以看出,我们设置的初始化值是懒加载的

还有两个常用的方法就是 set()remove()
set()方法

public void set(T value) {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null)//this 是当前的ThreadLocalmap.set(this, value);elsecreateMap(t, value);}

注意事项

内存泄露

使用 ThreadLocal 应该注意内存泄露的风险

内存泄露是对象已经没用了, 但是占用的内存不能被GC回收 ,导致占用越来越多,内存不够用了

为什么会出现内存泄露我们来看一下
在这里插入图片描述
这里看一下存放键值对的这个 Entry ,它是继承了一个 WeakReference 用来存放key,所以key是弱引用类型的,value是强引用类型的。

弱引用的特点是如果这个对象只被弱引用关联,那么这个对象就会被回收,所以这里的key是可以被回收掉的,但是 value 是个强引用,它不会被回收掉

所以 ThreadLocalMap 的每个 Entry 都包含了一个key的弱引用,和一个value的强引用, 正常情况下,当线程终止保存在 ThreadLocal 中的 value 就会被回收, 但是如果一个线程要持续很久,那它的value就不会被回收掉,即使这个value已经没用了 。比如说线程池,其实是一个线程反复使用,如果说线程停止,那么其中ThreadLocal的value是有可能不被回收的。

所以因为 value 和 Thread 之间存在强引用,并且不会被回收,就可能导致内存泄露

解决方案

那么针对这种情况,我们应该如何去解决呢?

其实jdk已经帮我们做了一些处理, 在调用 set() remove() rehash() 这些方法的时候,系统会扫描所有的key为null的 Entry,把这些对应的 value 都置空,这样value对象就可以被回收,具体是调用的 resize() 这个方法,代码如下
在这里插入图片描述
虽然jdk帮我们处理掉一些情况,但是如果 ThreadLocal 用不到了,那基本也不会去调用这几个方法, resize() 也就不会被触发,那么value还是不能被回收的

这种情况下,我们只好在使用完毕之后手动去调用 remove() 方法,避免内存泄露,这也是阿里巴巴开发规范中所规定的

共享对象

第二个要注意的就是 ThreadLocal 中不能存放共享变量,如果放进去的是一个共享变量,比如用 static 修饰的对象,那么 ThreadLocal.get() 获取到的还是这个对象本身,就还是会有线程安全的问题

总结

  • 了解 Thread,ThreadLocal,ThreadLocalMap 之间的关系

了解常用的几个方法

  • setInitialValue()
  • get()
  • set()
  • remove()

最后还有使用 ThreadLocal 造成的内存泄漏的原因和解决方法

  相关解决方案