参考:
java guide : https://javaguide.cn/java/concurrent/java-concurrent-questions-03.html
阿里云开发者社区:https://developer.aliyun.com/article/1180633
Dongguabai: https://blog.csdn.net/Dongguabai/article/details/105342001?utm_medium=distribute.pc_relevant.none-task-blog-2~default~baidujs_baidulandingword~default-0-105342001-blog-124063590.235^v38^pc_relevant_sort_base2&spm=1001.2101.3001.4242.1&utm_relevant_index=3
ThreadLocal 介绍
ThreadLocal
类主要解决的就是让每个线程绑定自己的值,可以存储每个线程的私有数据。
如果你创建了一个ThreadLocal
变量,那么访问这个变量的每个线程都会有这个变量的本地副本。
Thread 类
public class Thread implements Runnable { //...... //与此线程有关的ThreadLocal值。由ThreadLocal类维护 ThreadLocal.ThreadLocalMap threadLocals = null; //与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护 ThreadLocal.ThreadLocalMap inheritableThreadLocals = null; //...... }
ThreadLocal 类的 set() 方法
public void set(T value) { //获取当前请求的线程 Thread t = Thread.currentThread(); //取出 Thread 类内部的 threadLocals 变量(哈希表结构) ThreadLocalMap map = getMap(t); if (map != null) // 将需要存储的值放入到这个哈希表中 map.set(this, value); else createMap(t, value); } ThreadLocalMap getMap(Thread t) { return t.threadLocals; }
最终的变量是放在了当前线程的 ThreadLocalMap
中,并不是存在 ThreadLocal
上,ThreadLocal
可以理解为只是ThreadLocalMap
的封装,传递了变量值。 ThrealLocal
类中可以通过Thread.currentThread()
获取到当前线程对象后,直接通过getMap(Thread t)
可以访问到该线程的ThreadLocalMap
对象。
ThreadLocal 使用示例
ThreadLocalMap 类的定义在 ThreadLocal<T> 类中,是 ThreadLocal 类的一个内部类, key 是 ThreadLocal<T> 的一个对象,value 是这个 <T> 的实际值
Thread 线程类有一个字段,ThreadLocal.ThreadLocalMap threadLocals = null,是 ThreadLocal.ThreadLocalMap 类的一个实现对象,也就是每个线程对象都有一个 ThreadLocalMap 对象
ThreadLocal<T> 相当于一个维护所有线程 ThreadLocal.ThreadLocalMap threadLocals = null 这个属性,即所有线程 <T> 的取值 value 的一个工具类(所以一般声明为 static)
下面用一个例子说明这一点:
下面这段代码里,定义了两个 ThreadLocal<T> 对象
- private static final ThreadLocal<TreeNode> treeNodeThreadLocal = new ThreadLocal<>();
- private static final ThreadLocal<Integer> integerThreadLocal = new ThreadLocal<>().withInitial(() -> 0);
每个线程生成一个随机整数 curThreadInt
然后分别向这两个 ThreadLocal<T> 对象存放刚才生成的 随机整数 curThreadInt
import java.util.Random; public class testThreadLocal implements Runnable { private static final ThreadLocal<TreeNode> treeNodeThreadLocal = new ThreadLocal<>(); private static final ThreadLocal<Integer> integerThreadLocal = new ThreadLocal<>().withInitial(() -> 0); public static void main(String[] args) throws InterruptedException { testThreadLocal thisclass = new testThreadLocal(); for(int i=0 ; i<10; i++){ Thread t = new Thread(thisclass, ""+i); // 目的让各个线程打印初始值和 set,get 交错 Thread.sleep(new Random().nextInt(1000)); t.start(); } } @Override public void run() { System.out.println("Thread Name= "+Thread.currentThread().getName()+" integerThreadLocal = "+ integerThreadLocal.get()); Integer curThreadInt = new Random().nextInt(1000); // 目的让各个线程打印初始值和 set,get 交错 try { Thread.sleep(new Random().nextInt(1000)); } catch (InterruptedException e) { e.printStackTrace(); } treeNodeThreadLocal.set(new TreeNode(curThreadInt, Thread.currentThread().getName())); integerThreadLocal.set(curThreadInt); System.out.println("Thread Name= "+Thread.currentThread().getName() +" integerThreadLocal = "+integerThreadLocal.get() +" treeNodeThreadLocal = "+treeNodeThreadLocal.get()); } private class TreeNode { private Integer index; private String name; private TreeNode leftChild; private TreeNode rightChild; public TreeNode(Integer index, String name) { this.index = index; this.name = name; } @Override public String toString() { return "TreeNode{" + "index=" + index + ", name='" + name + '\'' + '}'; } } }
输出结果如下
D:\Program\Java\jdk1.8.0_321\bin\java.exe -javaagent:D:\Program\IDEA2022\lib\idea_rt.jar=61996:D:\Program\IDEA2022\bin -Dfile.encoding=UTF-8 -classpath D:\Program\Java\jdk1.8.0_321\jre\lib\charsets.jar;D:\Program\Java\jdk1.8.0_321\jre\lib\deploy.jar;D:\Program\Java\jdk1.8.0_321\jre\lib\ext\access-bridge-32.jar;D:\Program\Java\jdk1.8.0_321\jre\lib\ext\cldrdata.jar;D:\Program\Java\jdk1.8.0_321\jre\lib\ext\dnsns.jar;D:\Program\Java\jdk1.8.0_321\jre\lib\ext\jaccess.jar;D:\Program\Java\jdk1.8.0_321\jre\lib\ext\jfxrt.jar;D:\Program\Java\jdk1.8.0_321\jre\lib\ext\localedata.jar;D:\Program\Java\jdk1.8.0_321\jre\lib\ext\nashorn.jar;D:\Program\Java\jdk1.8.0_321\jre\lib\ext\sunec.jar;D:\Program\Java\jdk1.8.0_321\jre\lib\ext\sunjce_provider.jar;D:\Program\Java\jdk1.8.0_321\jre\lib\ext\sunmscapi.jar;D:\Program\Java\jdk1.8.0_321\jre\lib\ext\sunpkcs11.jar;D:\Program\Java\jdk1.8.0_321\jre\lib\ext\zipfs.jar;D:\Program\Java\jdk1.8.0_321\jre\lib\javaws.jar;D:\Program\Java\jdk1.8.0_321\jre\lib\jce.jar;D:\Program\Java\jdk1.8.0_321\jre\lib\jfr.jar;D:\Program\Java\jdk1.8.0_321\jre\lib\jfxswt.jar;D:\Program\Java\jdk1.8.0_321\jre\lib\jsse.jar;D:\Program\Java\jdk1.8.0_321\jre\lib\management-agent.jar;D:\Program\Java\jdk1.8.0_321\jre\lib\plugin.jar;D:\Program\Java\jdk1.8.0_321\jre\lib\resources.jar;D:\Program\Java\jdk1.8.0_321\jre\lib\rt.jar;D:\001Know\a_project\myTest\target\classes;C:\Users\Administrator\.m2\repository\com\alibaba\fastjson\1.2.83\fastjson-1.2.83.jar;C:\Users\Administrator\.m2\repository\org\freemarker\freemarker\2.3.20\freemarker-2.3.20.jar testThreadLocal Thread Name= 0 integerThreadLocal = 0 Thread Name= 0 integerThreadLocal = 302 treeNodeThreadLocal = TreeNode{index=302, name='0'} Thread Name= 1 integerThreadLocal = 0 Thread Name= 1 integerThreadLocal = 441 treeNodeThreadLocal = TreeNode{index=441, name='1'} Thread Name= 2 integerThreadLocal = 0 Thread Name= 3 integerThreadLocal = 0 Thread Name= 4 integerThreadLocal = 0 Thread Name= 2 integerThreadLocal = 390 treeNodeThreadLocal = TreeNode{index=390, name='2'} Thread Name= 3 integerThreadLocal = 941 treeNodeThreadLocal = TreeNode{index=941, name='3'} Thread Name= 5 integerThreadLocal = 0 Thread Name= 4 integerThreadLocal = 285 treeNodeThreadLocal = TreeNode{index=285, name='4'} Thread Name= 5 integerThreadLocal = 129 treeNodeThreadLocal = TreeNode{index=129, name='5'} Thread Name= 6 integerThreadLocal = 0 Thread Name= 6 integerThreadLocal = 926 treeNodeThreadLocal = TreeNode{index=926, name='6'} Thread Name= 7 integerThreadLocal = 0 Thread Name= 7 integerThreadLocal = 823 treeNodeThreadLocal = TreeNode{index=823, name='7'} Thread Name= 8 integerThreadLocal = 0 Thread Name= 9 integerThreadLocal = 0 Thread Name= 9 integerThreadLocal = 556 treeNodeThreadLocal = TreeNode{index=556, name='9'} Thread Name= 8 integerThreadLocal = 615 treeNodeThreadLocal = TreeNode{index=615, name='8'} Process finished with exit code 0
treeNodeThreadLocal 和 integerThreadLocal 这两个 ThreadLocal 类的对象与线程的关系如下所示
ThreadLocal. ThreadLocalMap 内部类定义
/** * ThreadLocalMap is a customized hash map suitable only for * maintaining thread local values. No operations are exported * outside of the ThreadLocal class. The class is package private to * allow declaration of fields in class Thread. To help deal with * very large and long-lived usages, the hash table entries use * WeakReferences for keys. However, since reference queues are not * used, stale entries are guaranteed to be removed only when * the table starts running out of space. */ static class ThreadLocalMap { /** * The entries in this hash map extend WeakReference, using * its main ref field as the key (which is always a * ThreadLocal object). Note that null keys (i.e. entry.get() * == null) mean that the key is no longer referenced, so the * entry can be expunged from table. Such entries are referred to * as "stale entries" in the code that follows. */ static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } /** * The initial capacity -- MUST be a power of two. */ private static final int INITIAL_CAPACITY = 16; /** * The table, resized as necessary. * table.length MUST always be a power of two. */ private Entry[] table; /** * The number of entries in the table. */ private int size = 0; /** * The next size value at which to resize. */ private int threshold; // Default to 0 /** * Set the resize threshold to maintain at worst a 2/3 load factor. */ private void setThreshold(int len) { threshold = len * 2 / 3; } /** * Increment i modulo len. */ private static int nextIndex(int i, int len) { return ((i + 1 < len) ? i + 1 : 0); } /** * Decrement i modulo len. */ private static int prevIndex(int i, int len) { return ((i - 1 >= 0) ? i - 1 : len - 1); } /** * Construct a new map initially containing (firstKey, firstValue). * ThreadLocalMaps are constructed lazily, so we only create * one when we have at least one entry to put in it. */ ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) { table = new Entry[INITIAL_CAPACITY]; int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); table[i] = new Entry(firstKey, firstValue); size = 1; setThreshold(INITIAL_CAPACITY); } /** * Construct a new map including all Inheritable ThreadLocals * from given parent map. Called only by createInheritedMap. * * @param parentMap the map associated with parent thread. */ private ThreadLocalMap(ThreadLocalMap parentMap) { Entry[] parentTable = parentMap.table; int len = parentTable.length; setThreshold(len); table = new Entry[len]; for (int j = 0; j < len; j++) { Entry e = parentTable[j]; if (e != null) { @SuppressWarnings("unchecked") ThreadLocal<Object> key = (ThreadLocal<Object>) e.get(); if (key != null) { Object value = key.childValue(e.value); Entry c = new Entry(key, value); int h = key.threadLocalHashCode & (len - 1); while (table[h] != null) h = nextIndex(h, len); table[h] = c; size++; } } } } /** * Get the entry associated with key. This method * itself handles only the fast path: a direct hit of existing * key. It otherwise relays to getEntryAfterMiss. This is * designed to maximize performance for direct hits, in part * by making this method readily inlinable. * * @param key the thread local object * @return the entry associated with key, or null if no such */ private Entry getEntry(ThreadLocal<?> key) { int i = key.threadLocalHashCode & (table.length - 1); Entry e = table[i]; if (e != null && e.get() == key) return e; else return getEntryAfterMiss(key, i, e); } // ...... 后面的就不贴过来了
ThreadLocal 的内存泄露问题
ThreadLocal 内存泄漏的场景
ThreadLocal关联两个引用,在ThreadLcocal对象上是强引用,而在Entry对象上是弱引用
如果gc想回收,必须ThreadLocal对象本身不存在强引用,在将 ThreadLocal 定义为局部变量时有两种情况:
- 不会发生内存泄漏:线程结束,ThreadLocal 和 线程中 的 ThreadLocalMap 包括 key 和 value 一起被回收
- 会发生内存泄漏:ThreadLocal 对象先于线程中 ThreadLocalMap 被垃圾回收
- 在 线程还没结束的时候,将ThreadLocal对象赋值为null(或者像下图这个例子一样作为内部方法的局部变量,后内部方法返回),这意味着对象已经死亡,可以进行垃圾回收,这时ThreadLocal就不再存在强引用,而 Entry 对象 key 引用的 ThreadLocal 由于是弱引用,也会被垃圾回收掉,而Value对象还存在Entry的强引用,value对象不会被垃圾回收,这就导致只要线程不结束,那Entry的key就为null,对应的value也就无法被访问,造成了内存泄漏,只有等线程结束才会被回收。
如果产生了内存泄漏,其实线程的存活时间比较短的话,影响程度一般不会很大,但是如果是在线程池中使用,就会产生较大的影响,因为线程池中的线程由于要重复利用,核心线程不会进入终止状态,这就意味着,线程会持续存货不会消亡,导致 value 对象一直不被回收。
不过现实中一般都不会把 ThreadLocal 定义为只有一个线程能访问到的局部变量,一个线程是不需要线程隔离的,所以这样是没有意义的。而是定义成 全局变量。
ThreadLocal 设置弱引用原因
从上面的分析,我们发现内存泄漏的一部分原因是由于,Entry对象的弱引用,但是为什么设置Entry引用的ThreadLocal对象为弱引用呢,其实将他设置成弱引用也是防止内存泄漏的,看到这里,一些读者可定会很困惑,内存泄漏的原因和他有关,但是为什么他却又是防止内存泄漏呢?
其实,ThreadLocal的内存泄漏可能分两种
- key内存泄漏(在 key 为对 ThreadLocal 的强引用时会发生)
这种内存泄漏我们是看不见的,为什么,就是因为将ThreadLocal使用弱引用才解决了这个问题,如果设计成强引用,这时我们在业务方法中将ThreadLocal设置为null,这时Entry对象中对ThreadLocal还是强引用,就意味着ThreadLocal不是进行垃圾回收(gc 回收的条件是不存在任何强引用),直接导致了key的内存泄漏。 - value内存泄漏(在 key 为对 ThreadLocal 的弱引用时会发生)
其实value的内存泄漏就是我们前面分析的内存泄漏,为了防止他发生,所以调用remove方法,除此之外,set和get方法也有将ThreadLocal为null的Enrty删除掉的逻辑,目的也就是处理这种情况
ThreadLocal 提供的解决方法
ThreadLocal也提供了remove方法来方式来防止内存泄漏。在使用完数据后,令 ThreadLocal = null 之前,调用 remove() 清理掉就行了。我们看一下remove方法的实现
public void remove() { 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)]) { if (e.get() == key) { e.clear(); expungeStaleEntry(i); return; } } }
弱引用介绍:
如果一个对象只具有弱引用,那就类似于可有可无的生活用品。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它 所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。
弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。
ThreadLocal 与 线程复用
因为是作为多线程时的线程隔离,所以现实应用中一般把 ThreadLocal 定义为全局的 static,只要不显式地令 ThreadLocal 为 null,就不会发生内存泄漏。
但是即使这样,如果使用不当,还是会导致一些业务问题,因为创建和销毁线程的代价比较大,所以现在的线程一般都是通过线程池复用的。
就 java 应用来说,处理 web 请求的 tomcat 线程,rpc 的 dubbo 线程,MQ 发送端消费端处理线程, 都是线程池中的。
也就是说,如果你处理一笔业务时,向 ThreadLocal 设置了一些变量,而在业务结束后没有清除这些变量。这个线程会被之后的业务复用,之后的业务就会取到错误的值。