ThreadLocal源码分析
一、概述
ThreadLocal应用场景很广,很多主流框架都使用到它。例如,Spring用它来管理数据库连接,每个线程获取的都是自己的数据库连接对象。
通常,我们使用ThreadLocal有两个目的:
1.用来隔离不同线程的变量,避免线程间互相干扰。
比如,我们系统每秒钟同时会有很多用户请求,每个请求都带有用户信息。通常我们都是一个线程处理一个用户请求,所以我们可以把用户信息放到Threadlocal里,让每个线程处理自己的用户信息,线程之间互不干扰。
2.使用ThreadLocal来传递数据。
比如,一个典型的用户请求需要经过拦截器-->controller-->service-->dao。如果想在这条请求链上传递数据,可以使用参数的方式一直传递下去,但这样做不太优雅。这时就可以使用ThreadLocal来存数据,由于一次请求只会对应一个线程,数据是与线程绑定的 。后续可以再通过ThreadLocal取出数据。
下面是jdk文档对ThreadLocal的描述。
ThreadLocal类提供了线程局部变量,这些变量不同于通常的变量,每个线程访问(通过get/set方法)的都是线程各自独有的变量副本。 ThreadLocal实例通常都是类中私有的静态的(private static)成员变量。这些类希望将状态与线程关联(例如,用户id或事物id)。 例如,下面的类将会为每个线程产生一个独有的标识符(id)。线程id在首次调用ThreadId.get()方法时分配,并且在随后的调用中保持不变。
官网也提供的一个ThreadLocal的例子。
当多个线程同时调用ThreadId.get()时,会轮流将共享的原子变量nextId加1后的值作为各个线程的线程id,例如线程A为0,则线程B为1,线程C初为2……。
public class ThreadId { // Atomic integer containing the next thread ID to be assigned private static final AtomicInteger nextId = new AtomicInteger(0); // Thread local variable containing each thread's ID private static final ThreadLocal<Integer> threadId = new ThreadLocal<Integer>() { @Override protected Integer initialValue() { return nextId.getAndIncrement(); } }; // Returns the current thread's unique ID, assigning it if necessary public static int get() { return threadId.get(); } }
二、源码分析
ThreadLocal内部使用ThreadLocalMap来持有元素,它是ThreadLocal的核心,我们可以把ThreadLocalMap看做是一个定制化的HashMap。它的数据结构如下图所示。
1.ThreadLocalMap
我们知道HashMap是由数组加链表组成的,但ThreadLocalMap只使用到了数组,并且数组是首尾相连的环形结构,后面会解释原因。
由源码可知,ThreadLocalMap的初始容量为16,负载因子为2/3。
/** * 初始容量。必须是2的次方。 */ private static final int INITIAL_CAPACITY = 16; /** * table数组,必要时会扩容。数组长度必须是2的次方。 */ private Entry[] 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. * 设置扩容的阈值,以维持负载因子至少为2/3. */ private void setThreshold(int len) { threshold = len * 2 / 3; }/** * 构造器。ThreadLocalMap使用的【懒初始化】,当有多个Entry要存放时,只会先创建一个Entry。 */ ThreadLocalMap(ThreadLocal firstKey, Object firstValue) { //初始化table数组 table = new Entry[INITIAL_CAPACITY]; //根据key的hash值定位其在数组中位置 int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); //在数组中创建首个节点 table[i] = new Entry(firstKey, firstValue); //设置初始化元素个数 size = 1; //设置初始阈值 setThreshold(INITIAL_CAPACITY); }
ThreadLocalMap使用的懒初始化,当有多个Entry节点要存储时,也只能先通过构造器来先初始化一个Entry节点。而ThreadLocalMap初始化的时机是在ThreadLocal中首次调用set或get方法时。
另外,节点存放的位置需要通过hash函数计算来定位。
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
说明一下,下面两种hash方式的结果都是一样的,做个测试就知道了。
- x % n
- x & (n-1)
2.set操作
ThreadLocal内部使用ThreadLocalMap来持有元素,它是ThreadLocal的核心,ThreadLocalMap可以看做是一个定制化的HashMap。查看Thread类的源码,可以发现Thread类有一个threaLocals属性,类型为ThreadLocalMap类型,所以可以理解为每个线程都会绑定一个ThreadLocalMap。
public class Thread implements Runnable { ThreadLocal.ThreadLocalMap threadLocals = null; //本地变量 }
存放数据时,我们会调用ThreadLocal.set(value)方法
public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); } ThreadLocalMap getMap(Thread t) { return t.threadLocals; }
set方法会先获取当前线程,然后通过getMap获取当前线程绑定的ThreadLocalMap。
- getMap结果非空,则进行保存
- getMap结果为空,说明是第一次调用set方法,需要先实例化ThreadLocalMap。
如果getMap方法返回为空,说明是第一次调用set方法,需要先实例化ThreadLocalMap,前面已经讲过,不再赘述。
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
如果getMap方法返回非空,将<ThreadLocal实例,value>作为键值对保存到ThreadLocalMap中。
保存的具体步骤是怎么样的呢,我们继续跟踪。set(value)-->set(key, value)
private void set(ThreadLocal<?> key, Object value) { // We don't use a fast path as with get() because it is at // least as common to use set() to create new entries as // it is to replace existing ones, in which case, a fast // path would fail more often than not. Entry[] tab = table; int len = tab.length; //通过hash定位 int i = key.threadLocalHashCode & (len-1); //从当前节点开始,往后线性探测 for (Entry e = tab[i];//当前节点 e != null; e = tab[i = nextIndex(i, len)]) {//下一个探测节点 //探测节点的key ThreadLocal<?> k = e.get(); //要存入的key与探测节点的key相等,直接覆盖 if (k == key) { e.value = value; return; } //探测节点key为空,说明被回收了【弱引用原因】 //说明可以使用该位置,用新k-v将其替换。 if (k == null) { replaceStaleEntry(key, value, i); return; } } //执行到这里,说明探测到了空位置,可以插入 tab[i] = new Entry(key, value);//创建节点插入 int sz = ++size; //插入后需要检测一下容量 //先尝试启发式清理,如果无法回收且容量又达到阈值,则需要扩容 if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }
1)hash定位
首先,使用hash算法来定位到数组的下标,使用的hash算法与上面讲解初始化时一样,不再赘述。
通过hash算法得到的位置,并不一定就是最终value将要存放的位置。ThreadLocalMap同HashMap一样也存在hash冲突问题。
2)使用线性探测,解决hash冲突
我们知道HashMap是使用链地址法来解决hash冲突的,而ThreadLocalMap则是使用的另一种解决hash冲突的方法:开放地址法。所谓的开放定址法就是一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入。
开放地址法可以用公式表示为 ( hash(key) + di ) % m,这里di是个变量,表示每次移动的步数。开放地址法在进行探测时,di有下列几种取法:
- 线性探测再散列:di=1,2,3,……m-1 。这种探测最简单。
- 二次探测再散列:di=21,-21,22,-22,……k2 (k<2/m)
- 随机探测再散列:di取伪随机数列
TheadLocalMap使用的是线性探测法,每次探测都是通过nextIndex()往后挪动一步。如果当前已经是最后一个节点,则探测第一个节点。这就说明了ThreadLocalMap中的数组是环形结构。但这里暂时还不能看出它是一个双向的,需要结合后面的prevIndex方法才能确定,后面再讲。
/** * Increment i modulo len. */ private static int nextIndex(int i, int len) { //下一个节点的位置。如果当前是最后一个节点,则下一个节点是第一个节点。 return ((i + 1 < len) ? i + 1 : 0); }
在探测的过程,对探测的节点进行判断
- ①如果探测节点的key与要存入的key相等,则直接覆盖。停止探测。
- ②如果探测节点的key为空,说明由于弱引用的原因被回收了。说明该位置可以重新利用,直接替换掉即可。停止探测。
替换失效节点的方法:replaceStaleEntry()
private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) { Entry[] tab = table; int len = tab.length; Entry e; // Back up to check for prior stale entry in current run. // We clean out whole runs at a time to avoid continual // incremental rehashing due to garbage collector freeing // up refs in bunches (i.e., whenever the collector runs). int slotToExpunge = staleSlot; for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len)) if (e.get() == null) slotToExpunge = i; // Find either the key or trailing null slot of run, whichever // occurs first for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) { ThreadLocal<?> k = e.get(); // If we find key, then we need to swap it // with the stale entry to maintain hash table order. // The newly stale slot, or any other stale slot // encountered above it, can then be sent to expungeStaleEntry // to remove or rehash all of the other entries in run. if (k == key) { e.value = value; tab[i] = tab[staleSlot]; tab[staleSlot] = e; // Start expunge at preceding stale entry if it exists if (slotToExpunge == staleSlot) slotToExpunge = i; cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); return; } // If we didn't find stale entry on backward scan, the // first stale entry seen while scanning for key is the // first still present in the run. if (k == null && slotToExpunge == staleSlot) slotToExpunge = i; } // If key not found, put new entry in stale slot tab[staleSlot].value = null; tab[staleSlot] = new Entry(key, value); // If there are any other stale entries in run, expunge them if (slotToExpunge != staleSlot) cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); }
3)启发式清理
如果上面的两种方式无法找到插入点,就只能找空节点来插入了。一旦遇到了空节点,就停止探测,准备在此处插入。插入前先做一次启发式清理操作。
原因就是,ThreadLocalMap同HashMap一样也是有负载因子的,当到达一定容量后就会进行rehash。rehash毕竟是一个耗性能的操作,应该尽量避免。所以,如果先通过启发式清理,能找到已经失效(key=null)的空间可以重复利用,这样就能避免rehash。
private boolean cleanSomeSlots(int i, int n) { boolean removed = false; Entry[] tab = table; int len = tab.length; do { i = nextIndex(i, len); Entry e = tab[i]; if (e != null && e.get() == null) { n = len; removed = true; i = expungeStaleEntry(i); } } while ( (n >>>= 1) != 0); return removed; } private int expungeStaleEntry(int staleSlot) { Entry[] tab = table; int len = tab.length; // expunge entry at staleSlot //清理失效的节点 tab[staleSlot].value = null; //value设为null tab[staleSlot] = null; //整个Entry设为null size--; // Rehash until we encounter null Entry e; int i; for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) { ThreadLocal<?> k = e.get(); if (k == null) {//k为null表示引用的对象已经被gc回收了 e.value = null; tab[i] = null; size--; } else { //hash不相等,说明这个元素之前发生过hash冲突(本应放在这却没放在这), //现在因为有元素被移除了,很有可能原来冲突的位置空出来了,重试一次 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; }
4)rehash
如果启发式清理没能清理出过期空间(key==null),而容量又达到了阈值,就只能rehash了。
private void rehash() { //全量清理 expungeStaleEntries(); // Use lower threshold for doubling to avoid hysteresis if (size >= threshold - threshold / 4) //扩容 resize(); } /** * 做一次全量清理失效节点的操作 */ private void expungeStaleEntries() { Entry[] tab = table; int len = tab.length; for (int j = 0; j < len; j++) { Entry e = tab[j]; if (e != null && e.get() == null) expungeStaleEntry(j); } } /** * Double the capacity of the table. */ private void resize() { Entry[] oldTab = table; int oldLen = oldTab.length; int newLen = oldLen * 2;//扩容后的容量为原来的2倍。因为要为2的次幂。 Entry[] newTab = new Entry[newLen]; int count = 0; for (int j = 0; j < oldLen; ++j) { Entry e = oldTab[j]; if (e != null) { ThreadLocal<?> k = e.get(); if (k == null) { e.value = null; // Help the GC } else { int h = k.threadLocalHashCode & (newLen - 1); while (newTab[h] != null) h = nextIndex(h, newLen); newTab[h] = e; count++; } } } setThreshold(newLen); size = count; table = newTab; }
3.get操作
取数据时,我们会调用ThreadLocal.get()方法
public T get() { // 获取当前线程 Thread t = Thread.currentThread(); // 获取当前线程绑定的ThreadLocalMap ThreadLocalMap map = getMap(t); if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) return (T)e.value; } // map为空,则自动为其设置一个初始值并返回。 return setInitialValue(); }
ThreadLocalMap getMap(Thread t) { return t.threadLocals; }
同样会先获取执行该方法的当前线程,然后再获取该线程绑定的ThreadLocalMap,将当前ThreadLocal实例作为key来获取Map中保存的value值。
get()-->getEntry()
/** * 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. * * 根据key获取Entry节点。 */ private Entry getEntry(ThreadLocal key) { //根据key的hash值定位在数组中位置i 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); } /** * getEntry未直接命中的时候调用此方法 */ private Entry getEntryAfterMiss(ThreadLocal key, int i, Entry e) { Entry[] tab = table; int len = tab.length; // 向后不断进行线性探测 while (e != null) { ThreadLocal k = e.get(); //找到目标 if (k == key) return e; //key为null,清除失效的Entry if (k == null) expungeStaleEntry(i); else i = nextIndex(i, len); //探测下一个 e = tab[i]; } return null; }
4.remove操作
public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) m.remove(this); } /** * Remove the entry for key. */ 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) { //直接将弱引用设为null,断开对对象的引用。 e.clear(); //清理无效节点 expungeStaleEntry(i); return; } } }
三、弱引用与内存泄露
ThreadLocalMap中Entry节点的定义如下。类上的注释信息已经解释的很清楚了,Entry类继承于WeakReference类,key是ThreadLocal类型。当key为null时,也就是entry.get() == null,意味着key将不再被引用,因此就可以将Entry从table数组中清除了。这样的Entry在后续的代码中被认为是过期失效的Entry。
/** * Entry类继承于WeakReference,使用其主引用域(中括号中的类类型)作为key(通常总是一个ThreadLocal对象)。 * 注意:key为null(也就是entry.get() == null)意味着key将不再被引用,因此可以将Entry从table数组中清除。 * 这样的Entry在后续的代码中被认为是过期失效的Entry。 */ static class Entry extends WeakReference<ThreadLocal> { /** * The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal k, Object v) { super(k); value = v; } }
需要说明的是,这里的弱引用指的既不是Entry也不是value,而是key,也就是key对ThreadLocal实例存在弱引用。
当调用ThreadLocal.set(value)时,引用关系如下图所示。
弱引用解决ThreadLocal实例内存泄露
那么问题来了,为什么要将ThreadLocalMap的key设计为弱引用类型呢?
我们先看看下面的代码。
public class ThreadLocalDemo { public static void main(String[] args) throws InterruptedException { print(); …… } private static void print() { ThreadLocalWrapper wrapper = new ThreadLocalWrapper(); wrapper.set("hello"); System.out.println(wrapper.get()); }//作用域结束,wrapper实例不再被使用会被gc,其所持有的local也会被gc。 } class ThreadLocalWrapper { //1.使用ThreadLocal时通常都会在某个类中。 private ThreadLocal<String> local = new ThreadLocal<>(); public String get() { return local.get(); } public void set(String str) { local.set(str); } }
上面代码print方法结束后,wrapper作用域结束会被gc,其持有的ThreadLocal实例也就是local也理应会被gc。
假设key不是弱引用,而是强引用,由于强引用的存在,local是不会被gc的。只有等待线程结束,Thread-->ThreadLocalMap-->Entry这条强引用链消失,entry不可达被gc,最终Threadlocal对象也会不可达才会被gc。如果是这样的话,岂不是ThreadLocal对象的回收要看线程的执行时间了,如果线程生命周期较长,比如线程池中的线程,ThreadLocal对象就发生内存泄露了。
ThreadLocal的设计者显然要考虑这个问题。线程的执行时间是由开发者的业务决定的,对于threadlocal引用出了作用域范围或者threadlocal=null后ThreadLocal对象的回收这个问题,肯定要在设计层面就解决掉,而不能依赖业务线程的终止,所以设计者就将key设计为弱引用类型。弱引用能保证Threadlocal对象一定活不过下次gc,一定会被回收掉。所以说,将ThreadLocalMap的key设计为弱引用,能在一定程度上防止内存泄露,这里的泄露指的是ThreadLocal对象的泄露。
remove()解决value内存泄露
那上面为什么说是一定程度上防止内存泄露,而不是说最终保证不会发生内存泄露?
别忘了还有value对象。由于ThreadLocalMap和线程的生命周期是一致的,当线程资源长期不释放,即使ThreadLocal本身由于弱引用机制已经被回收掉了,但还存在Thread-->ThreadLocalMap-->Entry-->value这样的强引用链,value在留存在线程的ThreadLocalMap的Entry中。即存在key为null而value却有值的无效Entry,导致内存泄漏。(由于value只能通过ThreadLocal的set/get/remove方法来访问,当ThreadLocal对象因为弱引用的原因被回收后,value自然也就无法再被访问到,成了无用资源了。)
所以,ThreadLocal采取了一定的措施来尽量避免内存泄露的发生。每次调用ThreadLocal的get/set/remove方法时,都会触发执行expungeStaleEntry方法,对失效(key为null)的Entry的做清理工作:擦除Entry(置为null),同时检测整个Entry数组将key为null的Entry一并擦除,然后重新调整索引。但是,只有在调用这三个方法才会触发清理,而实际上很可能我们在使用完ThreadLocal之后就不再做任何操作了,这样就不会触发ThreadLocal的清理工作。所以,当我们使用完ThreadLocal后,尽量手动调用一下remove方法,尽早地将value清理掉。
三、继承性
InheritableThreadLocal
在一些场景中,子线程需要可以获取父线程的本地变量,比如子线程要获取父线程中保存的用户的信息,或者使用一个统一的traceId来进行链路追踪。但是ThreadLocal不支持继承性,即子线程无法从父线程中获取父线程的本地变量。原因很简单,因为操作ThreadLocal时每次都是获取的当前线程。因此,JDK提供了InheritableThreadLocal来解决继承性问题。
InheritableThreadLocal 继承了ThreadLocal,并且重写了createMap等三个方法。当首次调用set方法时,创建的是当前线程的inheritableThreadLocals变量,而不再是threadLocals。同样在调用get方法时,获取当前线程的内部map时,获取的是inheritableThreadLocals而不再是threadLocals。总的来说,在InheritableThreadLocal中,变量inheritableThreadLocals替代了threadLocals。
当父线程创建子线程时,构造函数会将父线程中的inheritableThreadLocals变量里的本地变量赋值到子线程的inheritableThreadLocals里面。
// new Thread()-->init() init(){ …… //如果父线程inheritableThreadLocals变量不为null if (inheritThreadLocals && parent.inheritableThreadLocals != null) //设置子线程中的inheritableThreadLocals变量 this.inheritableThreadLocals =ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); …… } static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) { return new ThreadLocalMap(parentMap); }
TransmittableThreadLocal(淘宝开源)
但是InheritableThreadLocal提供的方案并不彻底。因为对于使用线程池等会池化复用线程的情况,线程由线程池创建好,并且线程是池化起来反复使用的;这时父子线程关系的ThreadLocal值传递已经没有意义,应用需要的实际上是把任务提交给线程池时的ThreadLocal值传递到任务执行时。
淘宝技术部哲良在github上开源了一个TransmittableThreadLocal,完美解决了线程变量继承问题,github地址为:https://github.com/alibaba/transmittable-thread-local。
总结
1.ThreadLocal的作用?
主要解决线程变量隔离问题。在实际使用中,我们有两种典型用法
①用来隔离线程变量。比如在数据库连接池中,可以将数据库连接Connection对象放在ThreadLocal中,每个线程获取各自的数据库连接,线程间不会互相干扰。
②用来在同一个线程的调用链中传递数据。
2.ThreadLocal的工作原理?(如何实现线程变量隔离的?)hash冲突解决方法?
工作原理略。每个线程都持有了一个线程本地变量,每个线程只操作自己的线程本地变量,线程间避免了相互干扰。
ThreadLocal使用开发地址法中的线性探测来解决hash冲突。
3.ThreadLocalMap为什么要定义在ThreadLocal中,而不是Thread中?
Thread类有个ThreadlocalMap类型的成员变量,但ThreadlocalMap的定义却在Threadlocal 中。
将ThreadLocalMap定义在Thread中似乎看起来更符合逻辑,但是实际上并不需要在Thread中操作ThreadLocalMap,定义在Thread类中只会增加一些不必要的开销。
定义在ThreadLocal类中的原因是ThreadLocal类负责ThreadLocalMap的创建,仅当线程中设置第一个ThreadLocal时,才为当前线程创建ThreadLocalMap,之后所有其他ThreadLocal变量将使用一个ThreadLocalMap。
总的来说就是,ThreadLocalMap不是必需品,定义在Thread中增加了成本,定义在ThreadLocal中按需创建。
4.既然是线程局部变量,那为什么不用线程(Thread)对象作为key,获取变量时通过线程作为key来获取,这样不是更清晰?
这样设计就存在数据覆盖的问题。如果用线程对象作为key,假设已经存入了用户信息,存入<线程,用户信息>。这时需要新增加用户地理位置信息,需要存入<线程,用户地理信息>。由于key都是同一个线程,不就覆盖了嘛。
5.那使用ThreadLocal新增信息应该怎么做呢?
为当前线程再绑定一个Threadlocal对象不就好了。比如已经存入了用户信息,要新增加用户的地理信息,直接Threadlocal<Geo> geo = new Threadlocal<> (); geo.set(地理信息);
这样线程的ThreadlocalMap里面就会有二个元素,一个是用户信息,一个是地理位置。(一个线程绑定一个ThreadLocalMap,一个ThreadLocalMap存多个ThreadLocal实例。)
6.如果有多个变量都要塞到ThreadlocalMap中,那岂不是要申明多个Threadlocal 对象?有没有好的解决办法。
可以再封装一下,把这些变量打包成一个Map不就好了,整个Map作为value存入,这样就只需要一个Threadlocal 对象。
7.为什么ThreadLocalMap中key被设计成弱引用类型?
key设计为弱引用是为了尽最大努力避免内存泄漏,解决的是ThreadLocal对象的内存泄露问题。
ThreadLocal的设计者考虑到了某些线程的生命周期较长,比如线程池中的线程。由于存在Thread -> ThreadLocalMap -> Entry这样一条强引用链,如果key不设计成弱引用类型,是强引用的话,key就一直不会被GC回收,一直不会是null,Entry就不会被清理。
(ThreadLocalMap根据key是否为null来判断是否清理Entry。因为key为null时,引用的ThreadLocal实例不可达会被回收。value又只能通过ThreadLocal的方法来访问,此时相当于value也没用处了。所以,可以根据key是否为null来判断是否清理Entry。)
8.ThreadLocal内存泄露的原因?要如何避免?
弱引用解决的是ThreadLocal对象的内存泄露问题,但value还存在内存泄露的风险。
内存泄露的原因:
由于ThreadLocalMap和线程的生命周期是一致的,当线程资源长期不释放,即使ThreadLocal本身由于弱引用机制已经被回收掉了,但value还是驻留在线程的ThreadLocalMap的Entry中。即存在key为null,但value却有值的无效Entry,导致内存泄漏。
ThreadLocal自身采取的措施:
但实际上,ThreadLocal内部已经为我们做了一定的防止内存泄漏的工作。ThreadLocalMap提供了一个expungeStaleEntry方法,该方法在每次调用ThreadLocal的get、set、remove方法时都会执行,即ThreadLocal内部已经帮我们做了对key为null的Entry的清理工作:擦除Entry(置为null),同时检测整个Entry数组将key为null的Entry一并擦除,然后重新调整索引。
但是必须需要调用这三个方法才会触发清理,很可能我们使用完之后就不再做任何操作了(set/get/remove),这样就不会触发内部的清理工作。
开发人员需要注意:
所以,通常建议每次使用完ThreadLocal后,立即调用remove方法。
9.为什么使用ThreadLocal时通常定义为static?
10.ThreadLocal继承性问题?如何解决?
ThreadLocal不支持子线程继承,可以使用JDK中的InheritableThreadLocal来解决继承性问题。对于线程池等场景,可以使用淘宝技术部哲良实现的TransmittableThreadLocal
参考: