文章目录
- 一、概述
- 二、get()方法
- 三、set()方法
- 四、可能导致的内存泄漏问题
- 五、remove
- 六、思考:为什么要将ThreadLocalMap的value设置为强引用?
一、概述
ThreadLocal
是线程私有的,独立初始化的变量副本。存放在和线程进行绑定的ThreadLocalMap
中。ThreadLocalMap
的内部类Entry
,是一个键值对的结构,key是ThreadLocal
对象,value是某个变量在某个时刻的副本。并且Entry
继承了WeakReference
类,使其key(ThreadLocal
对象)成为弱引用
,如果未正确使用remove方法,可能会导致内存泄漏问题。
构造entry对象时,key使用父类的方法,被包装成弱引用。
ThreadLocalMap
是ThreadLocal
的一个静态内部类:
但是其初始化的操作,是绑定在每个线程中的,作为线程对象的属性
,随着线程对象的加载而初始化。
并且ThreadLocalMap
的内部,是一个entry键值对
数组的形式。也有初始容量和扩容机制。这一点和HashMap类似,区别在于,处理Hash冲突时,HashMap使用的是拉链法
,也就是对于Hash值相同的key,会形成一条链表乃至树化。而ThreadLocalMap使用的是开放定址法中的线性探测再散列
,即某一个key计算出的Hash值,该位置已经有了元素,则会沿着数组下标依次向后寻找空位。
线性探测再散列
ThreadLocal通常会作为类的属性
,并且用static
关键字修饰。原因在于ThreadLocal需要从属于某个类,而不是具体的实例。
二、get()方法
在get方法中,主要做了几件事:
- 获取ThreadLocalMap。
- ThreadLocalMap不为空,则获取ThreadLocalMap的Entry 对象,并且对象不为空,就返回该Entry 对象的value。
- ThreadLocalMap为空,就执行初始化操作。
java"> public T get() {//获取当前线程对象(调用的是本地方法)Thread t = Thread.currentThread();//根据线程对象,获取到与该线程一一对应的ThreadLocalMap(ThreadLocalMap 是线程对象的属性) ThreadLocalMap map = getMap(t);//map第一次是为空的if (map != null) {ThreadLocalMap.Entry e = map.getEntry(this);if (e != null) {@SuppressWarnings("unchecked")T result = (T)e.value;return result;}}//执行初始化操作return setInitialValue();}
执行初始化操作:
java"> private T setInitialValue() {//一、初始化value为nullT value = initialValue();//获取当前线程对象Thread t = Thread.currentThread();//二、用当前线程对象,获取ThreadLocalMap ThreadLocalMap map = getMap(t);//map不为空,就将当前的ThreadLocal对象作为key,null作为value,构造Entry对象。if (map != null)map.set(this, value);else//三、否则初始化mapcreateMap(t, value);//返回nullreturn value;}//一protected T initialValue() {return null;}//二ThreadLocalMap getMap(Thread t) {//ThreadLocal.ThreadLocalMap threadLocals = null;return t.threadLocals;}//三void createMap(Thread t, T firstValue) {t.threadLocals = new ThreadLocalMap(this, firstValue);}
三、set()方法
set方法和get方法大同小异,也是根据当前线程获取ThreadLocalMap ,然后判空,执行set操作还是初始化操作。
java"> public void set(T value) {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null)//一、向ThreadLocalMap 的entry中插入元素的操作。map.set(this, value);elsecreateMap(t, value);}
四、可能导致的内存泄漏问题
先看一下这段代码,声明了一个线程池,以及LocalVariable
静态内部类,其中的成员变量是5M大的数组。并且还有一个ThreadLocal
属性,将LocalVariable
作为key包装成了弱引用。
java">public class ThreadLocalMemoryLeak {private static final int TASK_LOOP_SIZE = 500;/*线程池*/final static ThreadPoolExecutor poolExecutor= new ThreadPoolExecutor(5, 5, 1,TimeUnit.MINUTES,new LinkedBlockingQueue<>());static class LocalVariable {private byte[] a = new byte[1024 * 1024 * 5];/*5M大小的数组*/}ThreadLocal<LocalVariable> threadLocalLV;public static void main(String[] args) throws InterruptedException {SleepTools.ms(4000);for (int i = 0; i < TASK_LOOP_SIZE; ++i) {poolExecutor.execute(new Runnable() {public void run() {SleepTools.ms(500);
//
// LocalVariable localVariable = new LocalVariable();
//
//
// ThreadLocalMemoryLeak oom = new ThreadLocalMemoryLeak();
// oom.threadLocalLV = new ThreadLocal<>();
// oom.threadLocalLV.set(new LocalVariable());
//
// oom.threadLocalLV.remove();System.out.println("use local varaible");}});SleepTools.ms(100);}System.out.println("pool execute over");}}
如果没有加入ThreadLocal,而是仅仅在空跑,使用jvisualvm进行观察,看到的内存使用情况,是相对比较平稳的:
接着打开注释:
java"> ThreadLocalMemoryLeak oom = new ThreadLocalMemoryLeak();oom.threadLocalLV = new ThreadLocal<>();oom.threadLocalLV.set(new LocalVariable());
发现在运行过程中,内存使用情况起伏明显,多次触发gc。
并且最终程序执行完成后,内存还处于较高的水平,也就是说明堆中还存在很多没有被回收的垃圾对象。
为什么和没有使用ThreadLocal之前,会有如此大的差距?原因在于,每次垃圾回收时,作为弱引用的Entry的key:ThreadLocal对象会被回收,但是其value没有被回收。(在JVM停止时统一销毁)。
而每个线程中都存在一个5M的强引用对象没有被回收。
解决方式是,在ThreadLocal使用完成后,手动调用remove方法进行清除:
java"> public void remove() {ThreadLocalMap m = getMap(Thread.currentThread());if (m != null)m.remove(this);}
五、remove
在remove方法中,主要完成了三件事:
- 获取哈希表。
- 计算索引,根据 key(即 ThreadLocal 对象)的哈希码,计算它在哈希表中的索引。
- 遍历表格中的链表查找匹配的条目。
而expungeStaleEntry方法中,除了将value和entry的引用全部置空以外,还会继续向后扫描,将ThreadLocalMap中弱引用的key已经被回收的entry的value置空。
(get和set方法中也有该实现)
java"> 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) {//将key的引用置空e.clear();//一expungeStaleEntry(i);return;}}}//一private int expungeStaleEntry(int staleSlot) {Entry[] tab = table;int len = tab.length;// expunge entry at staleSlot//将指定下标的value的引用置空tab[staleSlot].value = null;//将指定下标的entry置空tab[staleSlot] = null;//table的长度减少size--;//这部分代码会继续扫描从 staleSlot 后的条目。如果遇到 ThreadLocal 对象已经被回收(k == null),则清除该条目。//否则,重新计算哈希位置并尝试将条目移动到新的位置。//这里使用了一个 while 循环来处理条目的重新定位,确保哈希表在移除过期条目后依然保持正确。Entry e;int i;for (i = nextIndex(staleSlot, len);(e = tab[i]) != null;i = nextIndex(i, len)) {ThreadLocal<?> k = e.get();if (k == null) {e.value = null;tab[i] = null;size--;} else {int h = k.threadLocalHashCode & (len - 1);if (h != i) {tab[i] = null;// Unlike Knuth 6.4 Algorithm R, we must scan until// null because multiple entries could have been stale.while (tab[h] != null)h = nextIndex(h, len);tab[h] = e;}}}return i;}
六、思考:为什么要将ThreadLocalMap的value设置为强引用?
线程本地存储的一个核心需求是,数据必须与线程的生命周期
绑定,直到该线程结束或者显式移除该值。如果线程本地存储的 value 被弱引用,就无法保证它在使用时的可用性,可能会导致意外的回收和不可预期的行为。如果 ThreadLocalMap 的 value 被设置为弱引用,那么 ThreadLocalMap 中的条目就可能会在垃圾回收时被回收,因为 value 被弱引用(即没有强引用指向它)。这就可能导致在需要访问该值时,数据已经被清理掉。
最主要的点在于,ThreadLocalMap 的生命周期跟 Thread 一样长。