导言

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;
}

ThreadLocalMapThreadLocal类的一个静态内部类,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目标的下标位置是经过ThreadLocalthreadLocalHashCode核算出来的。

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目标。ThreadThreadLocalThreadLocalMap以及Entry数组的联系如下图:

阿里二面:谈谈ThreadLocal的内存走漏问题?问麻了。。。。

ThreadLocal在哪些场景下不会呈现内存走漏?

当一个目标失掉一切强引证,或者它仅被弱引证、软引证、虚引证相关时,废物收集器(GC)一般都能辨认并收回这些目标,然后防止内存走漏的发生。当咱们在手动创立线程时,若将变量存储到ThreadLocal中,那么在Thread线程正常运转的过程中,它会坚持对内部ThreadLocalMap实例的引证。只需该Thread线程持续履行使命,这种引证联系将持续存在,保证ThreadLocalMap实例及其间存储的变量不会因无引证而被GC收回。

阿里二面:谈谈ThreadLocal的内存走漏问题?问麻了。。。。

当线程履行完使命并正常退出后,线程与内部ThreadLocalMap实例之间的强引证联系随之断开,这意味着线程不再持有ThreadLocalMap的引证。在这种情况下,失掉强引证的ThreadLocalMap目标将契合废物收集器(GC)的收回条件,然后被主动收回。与此同时,鉴于ThreadLocalMap内部的键(ThreadLocal目标)是弱引证,一旦ThreadLocalMap被收回,若此刻没有其他强引证指向这些ThreadLocal目标,它们也将被GC同时收回。因而,在线程结束其生命周期后,与之相关的ThreadLocalMap及其包含的ThreadLocal目标理论上都可以被正确整理,防止了内存走漏问题。

实际应用中还需重视ThreadLocalMap中存储的值(非键)是否为强引证类型,因为即使键(ThreadLocal目标)被收回,假如值是强引证且没有其他途径开释,仍或许导致内存走漏。

ThreadLocal在哪些场景下会呈现内存走漏?

在实际项目开发中,假如为每个使命都手动创立线程,这是一件很消耗资源的方式,而且在阿里巴巴的开发标准中也提到,不推荐运用手动创立线程,推荐运用线程池来履行相对应的使命。那么当咱们运用线程池时,线程池中的线程跟ThrealLocalMap的引证联系如下:

阿里二面:谈谈ThreadLocal的内存走漏问题?问麻了。。。。

在运用线程池处理使命时,每一个线程都会相关一个独立的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中的四种引证类型?

阿里二面:谈谈ThreadLocal的内存走漏问题?问麻了。。。。

Entry目标中的Key被GC主动收回后,对应的ThreadLocal被GC收回掉了,变成了null,可是ThreadLocal对应的value值仍然被Entry引证,不能被GC主动收回。这样就造成了内存走漏的危险。

阿里二面:谈谈ThreadLocal的内存走漏问题?问麻了。。。。

在线程池环境下运用ThreadLocal存储数据时,内存走漏的危险主要源自于线程生命周期办理及ThreadLocalMap内部结构的规划。因为线程池中的中心线程在完成使命后会复用,每个线程都会坚持对各自相关的ThreadLocalMap目标的强引证,这保证了只需线程持续存在,其对应的ThreadLocalMap就无法被废物收集器(GC)主动收回。

进一步剖析,ThreadLocalMap内部采用一个Entry数组来保存键值对,其间每个条目的Key是当时线程中对应ThreadLocal实例的弱引证,这意味着当外部不再持有该ThreadLocal实例的强引证时,Key部分可以被GC正常收回。然而,关键在于Entry的Value部分,它直接或间接地持有着强引证的目标,即使Key因为弱引证特性被收回,但Value所引证的数据却不会随之开释,除非明确移除或者整个ThreadLocalMap随着线程结束而失效。

所以,在线程池中,假如未正确整理不再运用的ThreadLocal变量,其所持有的强引证数据将在多个使命履行过程中逐渐积累并驻留在线程的ThreadLocalMap中,然后导致潜在的内存走漏危险。

ThreadLocal怎么防止内存走漏

经过上述ThreadLocal原理以及发生内存走漏的剖析,咱们知道防止内存走漏,咱们一定要在完成线程内的使命后,调用ThreadLocalremove()办法来铲除当时线程中ThreadLocal所对应的值。其remove办法源码如下:

 public void remove() {
	 ThreadLocalMap m = getMap(Thread.currentThread());
	 if (m != null) {
		 m.remove(this);
	 }
 }

remove()办法中,首先根据当时线程获取ThreadLocalMap类型的目标,假如不为空,则直接调用该目标的有参remove()办法移除value的值。ThreadLocalMapremove办法源码如下:

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()办法知道ThreadLocalEntry下标是经过核算ThreadLocalhashCode获得了,而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的内存走漏问题?问麻了。。。。

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、中间件、架构规划、面试题、程序员攻略等