JUC-ThreadLocal-1-原理解析

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

Thread,ThreadLocal,ThreadLocalMap

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

image

就是一个线程 (Thread) 对应有一个 ThreadLocalMap , ThreadLocalMap 中存放这一堆的 ThreadLocal

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

Thread类:

image

然后再点进去 ThreadLocalMap 类里

image

可以看到 ThreadLocalMap 这里存储的是一个键值对 Entry 类型的

重要方法解析

第一个 ThreadLocal.setInitialValue() ,设置初始化值的方法

首先看一下源码

image

总体来说这个方法就是给 ThreadLocal 去设置值的, 这里首先会调用 initialValue 这个方法,这个是不是很熟悉, 就是我们上文中的案例中初始化 ThreadLocal 时,重写的方法,默认是返回空的

image

setInitialValue() 这个方法会在 get() 方法中调用,再来看一下 get() 方法的源码

image

先去Map中找,没有的话再去初始化,从这里也可以看出,我们设置的初始化值是懒加载的

还有两个常用的方法就是 set()remove() 这两个这里就不贴代码了

注意事项

内存泄露

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

内存泄露是指某个对象已经没用了, 但是占用的内存不能被会GC回收

为什么会出现内存泄露我们来看一下

image

这里看一下存放键值对的这个 Entry ,它是继承了一个 WeakReference 弱引用类型的,用来存放key

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

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

所以因为 value 和 Thread 之间存在强引用,就可能导致内存泄露

解决方案

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

其实jdk已经帮我们做了一些处理, 在调用 set() remove() rehash() 这些方法的时候,系统会扫描所有的key为null的 Entry,把这些对应的 value 都置空,这样value对象就可以被回收,具体是调用的 resize() 这个方法,代码如下

image

虽然jdk帮我们处理掉一些情况,但是如果 ThreadLocal 用不到了,那基本也不会去调用这几个方法, resize() 也就不会被触发,那么value还是不能被回收的

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

共享对象

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

总结

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

了解常用的几个方法

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

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