本文是自己的学习笔记,主要参考资料如下 https://www.cnblogs.com/dolphin0520/p/3920407.html JavaSE文档 https://blog.csdn.net/ThinkWon/article/details/102508721
ThreadLocal是用来管理多个线程的『公共变量』。要注意这里的公共变量不是只多个线程共享的同一实例,而是指同一类型,同一用途(最好是这样)的类有多个实例,多个线程都拥有自己专有的那一个实例。这多个实例都可以统一交给ThreadLocal来管理
如同ThreadLocal中注释所说,ThreadLocal通常是作为一个类的private static成员存在。private是防止被外部任意修改,static是因为ThreadLocal只需要有一个实例就能管理多个线程的『共有变量』。
下面用一个例子来展示ThreadLocal是定义,如何存储线程变量等基本用法。
如果多个线程需要有一个Integer类型的共有变量,我们可以先声明一个private static的ThreadLocal的变量,用来管理『公共变量』。这里我为了方便外界读取ThreadLocal,就将变量设为public。
public class ThreadLocalHolder { public static ThreadLocal<Integer> integerThreadLocal; public ThreadLocalHolder() { integerThreadLocal = new ThreadLocal<>(); } }当线程需要使用到Integer类型的『公共变量』时,可以直接通过integerThreadLocal直接读写属于自己的那一份Integer变量。
Integer i = ThreadLocalHolder.integerThreadLocal.get(); Integer i = ThreadLocalHolder.integerThreadLocal.set(1);任何线程都可以使用上面的代码来读取属于自己的变量。并不需要在参数中传当前是需要取哪个线程的变量,ThreadLocal会自动判断。
源码解析主要是为了了解ThreadLocal是如何存储多个线程拥有的变量副本,之后的源码解说点到为止,不会太深地挖掘。下面我们从get()方法入手,来解刨它的存储结构,
public T get() { // 获取当前的线程 Thread t = Thread.currentThread(); // 以当前线程参数,获取该线程对应的ThreadLocalMap ThreadLocal.ThreadLocalMap map = getMap(t); if (map != null) { // 还需要从map中获取到Entry ThreadLocal.ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") // 上面的注解只是一个提示,可以不用在意。 // 下面我们可以看到,程序会返回e.value, // 这就是线程想要的变量值 T result = (T)e.value; return result; } } return setInitialValue(); }从上面的代码可以看出,当运行threadLocal.get()方法时,程序先找到当前线程对应的ThreadLocal,然后在这个ThreadLocal中获取到对应的Entry,之后Entry.value就是我们想要得到的属于当前线程的变量。
那么接下来的目标,就应该是了解ThreadLocalMap和Entry到底是什么东西。
ThreadLocalMap是ThreadLocal中的一个静态类。虽然其定义在ThreadLocal中,但是实例却是存在与Thread之中。
Thread中的ThreadLocalMap变量在上面的get()方法中,获取ThreadLocalMap也是直接返回当前线程t的threadLocalMap。
ThreadLocal.ThreadLocalMap map = getMap(t);中的getMap(t)方法接下来我们来看看ThreadLocalMap的实现。这里内容较多,我省略了很多代码。接下来的源码只是为了解释ThreadLocalMap和Entry的关系。
在看源代码之前要先剧透一下,ThreadLocalMap并不直接存储线程的专有变量,Entry才是。ThreadLocalMap只是会存储着多个Entry,而Entry是一个基础了WeakReference的存储结构。请带着这个知识点看接下来的源码。
关于WeakReference可以看这篇文章https://editor.csdn.net/md/?articleId=108385006 static class ThreadLocalMap { //ThreadLocalMap内部有这Entry[]类型的数组 private Entry[] table; //通过ThreadLocalMap获取对应的Entry,下面这个方法只是为了展示ThreadLocalMap和Entry的关系 private ThreadLocal.ThreadLocalMap.Entry getEntry(ThreadLocal<?> key) { int i = key.threadLocalHashCode & (table.length - 1); ThreadLocal.ThreadLocalMap.Entry e = table[i]; if (e != null && e.get() == key) return e; else //这个方法不用关注 return getEntryAfterMiss(key, i, e); } }从上面的代码可以看到,ThreadLocalMap中的Entry是一个数组结构(其实是HashMap)。
getEntry这个方法中,我们看到程序会使用hashCode来计算table的坐标,通过坐标获取到Entry。这就是为什么会说ThreadLocalMap其实是个HashMap,这查找值的方式完全就是HashMap的方式。
到这里,ThreadLocalMap和Entry的关系就很明显了,ThreadLocalMap是个HashMap,其中存储着多个Entry。
另外还需要再多说一点,虽然不怎么重要,getEntry中的key,基本上就是ThreadLocal本身。
我们先来看看Entry的定义,其定义在ThreadLocalMap中。
static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }这就是全部的内容了,其结构非常简单,如果已经对WeakReference有了解了,那就很容易理解Entry的存储结构。
关于WeakReference可以看这篇文章https://blog.csdn.net/sinat_38393872/article/details/108385006Entry是一个键值对的结构,<WeakReference<ThreadLocalMap>, Object>。这意为着,通过一个ThreadLocalMap可以获取到一个对应的值。这个值就是线程专有的变量。
每个线程都有一个ThreadLocalMap,这个ThreadLocalMap的存储结构是一个Entry类型的数组,但是从数据的读取和写入方式来看,ThreadLocalMap更应该被看成是一个是<TheadLocal, Entry>的HashMap。
Entry本质上也是一个Map,它的key类型是ThreadLocal,value则是线程存储的专有变量。
当我们想要获取线程A存储在ThreadLocal threadLocal中的专用变量时,会使用ThreadLocal.get()方法。该方法先获取当前线程,之后获取到当前线程中存储的ThreadLocalMap threadLocalMap(<TheadLocal, Entry>的HashMap存储结构)。
之后以threadLocal为key,在threadLocalMap中找到对应的value。这个value就是线程A在threadLocal中存储的专有变量。
下图是存储结构。
最根本的原因就是,ThreadLocal本身不存储各个线程的专有变量,这些专有变量是存储在线程自己的空间中。但是想要获取到这些变量却又需要ThreadLocal作为key才能找到。所以当ThreadLocal的实例被回收后,那些名义上是由这个实例管理的变量空间仍然有引用指向这些变量,系统可达性分析会判断这些变量不能被回收。如果我们一直通过线程池复用线程,那线程中的这些需要被回收的变量会越来越多,就有可能造成内存泄漏。
下图是Thread和ThreadLocal的引用指针关系图。
从上图可以看到,一个在使用中的ThreadLocal实例是被两个引用指向,一个是声明这个实例时的引用,另一个是线程内部中的Entry引用。
真正的数据存储在线程的ThreadLocalMap里面,要想找到真正的数据就必须以ThreadLocal实例为参数。
这时候有这么一个情况,ThreadLocal ref被销毁,只剩下一个弱引用指向ThreadLocal实例,随后这个实例被JVM回收。于是线程中ThreadLocalMap里对应的数据value就永远访问不到,同时value有强引用指向,不会自动被垃圾回收,于是就一直占用着内存空间。
一般情况下,线程执行完之后会销毁所有占用的空间,也包括ThreadLocalMap中的数据,那value即使访问不到,最终也会被销毁,但是如果我们使用的是线程池,那一个线程会不断地重复使用,那最终多余的value会越来越多,最终内存泄漏。
Java本身也意识到了这个问题,也做出了相应的处理。
在get和set方法中,程序hash计算出元素在Entry[]数组中的下标后,如果发现元素的key == null,那就会执行expungeStaleEntry和replaceStaleEntry方法,这两个方法会清楚当前位置元素占用的多余空间,同时还可能会清楚其它key == null的元素占用的空间(具体删除那些元素和多少个元素是未知的,因为这两个方法会使用类似于解决hash冲突的逐步探测法来删除元素)。
不过以上处理也只是缓解,最终还是有可能内存泄露。
当某个线程不需要再使用某个ThreadLocal threadLocal维护的数据时,我们就执行threadLocal.remove()方法,该方法可以清除当前线程在threadLocal中维护的数据。