导言
ThreadLocal
在Java多线程编程中扮演着重要的人物,它供给了一种线程部分存储机制,答应每个线程拥有独立的变量副本,然后有用地防止了线程间的数据同享抵触。ThreadLocal的主要用途在于,当需求为每个线程保护一个独立的上下文变量时,比如每个线程的业务ID、用户登录信息、数据库连接等,可以减少对同步机制如synchronized
关键字或Lock类的依靠,进步体系的履行功率和简化代码逻辑。
可是咱们在运用ThreadLocal
时,经常因为运用不当导致内存走漏。此刻就需求咱们去探求一下ThreadLocal
在哪些场景下会呈现内存走漏?哪些场景下不会呈现内存走漏?呈现内存走漏的根本原因又是什么呢?怎么防止内存走漏?
ThreadLocal原理
ThreadLocal
的实现根据每个线程内部保护的一个ThreadLocalMap
。
public class Thread implements Runnable {
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
}
ThreadLocalMap
是ThreadLocal
类的一个静态内部类,ThreadLocal
自身不能存储数据,它在作用上更像一个东西类,ThreadLocal
类供给了set(T value)
、get()
等办法来操作ThreadLocalMap
存储数据。
public class ThreadLocal<T> {
// ...
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
// ...
}
而ThreadLocalMap
内部保护了一个Entry
数据,用来存储数据,Entry
继承了WeakReference
,所以Entry
的key是一个弱引证,可以被GC收回。Entry
数组中的每一个元素都是一个Entry
目标。每个Entry
目标中存储着一个ThreadLocal
目标与其对应的value值。
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}
关于弱引证的知识点,请参考:美团一面:说一说Java中的四种引证类型?
而Entry
数组中Entry
目标的下标位置是经过ThreadLocal
的threadLocalHashCode
核算出来的。
private ThreadLocalMap(ThreadLocalMap parentMap) {
Entry[] parentTable = parentMap.table;
int len = parentTable.length;
setThreshold(len);
table = new Entry[len];
for (Entry e : parentTable) {
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);
// 经过key的threadLocalHashCode核算下标,这个key便是ThreadLocall目标
int h = key.threadLocalHashCode & (len - 1);
while (table[h] != null)
h = nextIndex(h, len);
table[h] = c;
size++;
}
}
}
}
而从Entry
数组中获取对应key即ThreadLocal
对应的value值时,也是经过key的threadLocalHashCode
核算下标,然后可以快速的回来对应的Entry
目标。
private Entry getEntry(ThreadLocal<?> key) {
// 经过key的threadLocalHashCode核算下标,这个key便是ThreadLocall目标
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);
}
在Thread
中,可以存储多个ThreadLocal
目标。Thread
、ThreadLocal
、ThreadLocalMap
以及Entry
数组的联系如下图:
ThreadLocal在哪些场景下不会呈现内存走漏?
当一个目标失掉一切强引证,或者它仅被弱引证、软引证、虚引证相关时,废物收集器(GC)一般都能辨认并收回这些目标,然后防止内存走漏的发生。当咱们在手动创立线程时,若将变量存储到ThreadLocal
中,那么在Thread
线程正常运转的过程中,它会坚持对内部ThreadLocalMap
实例的引证。只需该Thread
线程持续履行使命,这种引证联系将持续存在,保证ThreadLocalMap
实例及其间存储的变量不会因无引证而被GC收回。
当线程履行完使命并正常退出后,线程与内部ThreadLocalMap
实例之间的强引证联系随之断开,这意味着线程不再持有ThreadLocalMap
的引证。在这种情况下,失掉强引证的ThreadLocalMap
目标将契合废物收集器(GC)的收回条件,然后被主动收回。与此同时,鉴于ThreadLocalMap
内部的键(ThreadLocal
目标)是弱引证,一旦ThreadLocalMap
被收回,若此刻没有其他强引证指向这些ThreadLocal
目标,它们也将被GC同时收回。因而,在线程结束其生命周期后,与之相关的ThreadLocalMap
及其包含的ThreadLocal
目标理论上都可以被正确整理,防止了内存走漏问题。
实际应用中还需重视
ThreadLocalMap
中存储的值(非键)是否为强引证类型,因为即使键(ThreadLocal
目标)被收回,假如值是强引证且没有其他途径开释,仍或许导致内存走漏。
ThreadLocal在哪些场景下会呈现内存走漏?
在实际项目开发中,假如为每个使命都手动创立线程,这是一件很消耗资源的方式,而且在阿里巴巴的开发标准中也提到,不推荐运用手动创立线程,推荐运用线程池来履行相对应的使命。那么当咱们运用线程池时,线程池中的线程跟ThrealLocalMap
的引证联系如下:
在运用线程池处理使命时,每一个线程都会相关一个独立的ThreadLocalMap
目标,用于存储线程本地变量。因为线程池中的中心线程在完成使命后不会被销毁,而是坚持活动状态等待接收新的使命,这意味着中心线程与其内部持有的ThreadLocalMap
目标之间一直坚持着强引证联系。因而,只需中心线程存活,其所对应的ThreadLocal
目标和ThreadLocalMap
不会被废物收集器(GC)主动收回,此刻就会存在内存走漏的危险。
关于Java中的线程池参数以及原理,请参考:Java线程池最全讲解
呈现内存走漏的根本原因
由上述ThreadLocalMap
的结构图以及ThreadLocalMap
的源码中,咱们知道ThreadLocalMap
中包含一个Entry
数组,而Entry
数组中的每一个元素便是Entry
目标,Entry
目标中存储的Key便是ThreadLocal
目标,而value便是要存储的数据。其间,Entry
目标中的Key属于弱引证。
static class 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
,在引证的目标运用结束之后,即使内存足够,GC也会对其进行收回。
关于弱引证的知识点,请参考:美团一面:说一说Java中的四种引证类型?
当Entry
目标中的Key被GC主动收回后,对应的ThreadLocal
被GC收回掉了,变成了null,可是ThreadLocal
对应的value值仍然被Entry
引证,不能被GC主动收回。这样就造成了内存走漏的危险。
在线程池环境下运用ThreadLocal
存储数据时,内存走漏的危险主要源自于线程生命周期办理及ThreadLocalMap
内部结构的规划。因为线程池中的中心线程在完成使命后会复用,每个线程都会坚持对各自相关的ThreadLocalMap
目标的强引证,这保证了只需线程持续存在,其对应的ThreadLocalMap
就无法被废物收集器(GC)主动收回。
进一步剖析,ThreadLocalMap
内部采用一个Entry数组来保存键值对,其间每个条目的Key是当时线程中对应ThreadLocal
实例的弱引证,这意味着当外部不再持有该ThreadLocal
实例的强引证时,Key部分可以被GC正常收回。然而,关键在于Entry的Value部分,它直接或间接地持有着强引证的目标,即使Key因为弱引证特性被收回,但Value所引证的数据却不会随之开释,除非明确移除或者整个ThreadLocalMap
随着线程结束而失效。
所以,在线程池中,假如未正确整理不再运用的ThreadLocal
变量,其所持有的强引证数据将在多个使命履行过程中逐渐积累并驻留在线程的ThreadLocalMap
中,然后导致潜在的内存走漏危险。
ThreadLocal怎么防止内存走漏
经过上述ThreadLocal
原理以及发生内存走漏的剖析,咱们知道防止内存走漏,咱们一定要在完成线程内的使命后,调用ThreadLocal
的remove()
办法来铲除当时线程中ThreadLocal
所对应的值。其remove
办法源码如下:
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null) {
m.remove(this);
}
}
在remove()
办法中,首先根据当时线程获取ThreadLocalMap
类型的目标,假如不为空,则直接调用该目标的有参remove()
办法移除value的值。ThreadLocalMap
的remove
办法源码如下:
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;
}
}
}
由上述ThreadLocalMap
中的set()
办法知道ThreadLocal
中Entry
下标是经过核算ThreadLocal
的hashCode
获得了,而remove()
办法要找到需求移除value所在Entry
数组中的下标时,也时经过当时ThreadLocal
目标的hashCode
获的,然后找到它的下标之后,调用expungeStaleEntry
将其value也置为null。咱们持续看一下expungeStaleEntry
办法的源码:
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
tab[staleSlot].value = null;
tab[staleSlot] = 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) {
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;
}
在expungeStaleEntry()
办法中,会将ThreadLocal
为null对应的value
设置为null,同时会把对应的Entry
目标也设置为null,而且会将一切ThreadLocal
对应的value为null的Entry
目标设置为null,这样就去除了强引证,便于后续的GC进行主动废物收回,也就防止了内存走漏的问题。即调用完remove
办法之后,ThreadLocalMap
的结构图如下:
在
ThreadLocal
中,不仅仅是remove()
办法会调用expungeStaleEntry()
办法,在set()
办法和get()
办法中也或许会调用expungeStaleEntry()
办法来整理数据。这种规划保证了即使没有显式调用remove()
办法,体系也会在必要时主动整理不再运用的ThreadLocal
变量占用的内存资源。
需求咱们特别注意的是,尽管ThreadLocal
供给了remove
这种机制来防止内存走漏,但它并不会主动履行相关的整理操作。所以为了保证资源有用开释并防止潜在的内存走漏问题,咱们应当在完成对ThreadLocal
目标中数据的运用后,及时调用其remove()
办法。咱们最好(也是有必要)是在try-finally
代码块结构中,在finally
块中明确地履行remove()
办法,这样即使在处理过程中抛出反常,也能保证ThreadLocal
相关的数据被铲除,然后有利于GC收回不再运用的内存空间,防止内存走漏。
总结
本文探讨了ThreadLocal
的工作原理以及其内存走漏问题及处理策略。ThreadLocal
经过为每个线程供给独立的变量副本,实现多线程环境下的数据阻隔。其内部经过ThreadLocalMap
与当时线程绑定,利用弱引证办理键值对。可是,假如未及时整理不再运用的ThreadLocal
变量,或许导致内存走漏,尤其是在线程池场景下。处理办法包含在完成使命后调用remove办法移除无用数据。正确理解和运用ThreadLocal
可以有用提高并发编程功率,但必须重视潜在的内存走漏危险。
本文已收录于我的个人博客:码农Academy的博客,专注分享Java技能干货,包含Java基础、Spring Boot、Spring Cloud、Mysql、Redis、Elasticsearch、中间件、架构规划、面试题、程序员攻略等