ThreadLocalMap里弱引用

java 线程 专栏收录该内容
1 篇文章 0 订阅

要回答ThreadLocalMap里弱引用,我们需要弄清者三个问题

第一个问题,我们先来看看引用相关的。其实Java中一直有争论关于值传递与引用传递(就我看到的百度是这样的)。我们先来看看代码。

public static void main(String[] args){
	//测试引用传递
	Map<String,String> a = new HashMap<>();
	a.put("1", "a");
	List<Map> b = new ArrayList<Map>();
	b.add(a);
	a = null;
	Map aa = b.get(0);
	System.out.println(aa.get("1"));
	//结论传递的是引用的副本
	//测试值传递
	int xx = 1;
	add(xx);
	System.out.print(xx);
}
	
public static void add(int temp){
	temp++;
}

最后结果是

a
1

从这里我们看到其实传递的都是副本,只不过基本类型传递的是值的副本,而引用传递的是引用的副本。

由此可见引用传递只是把自己指向某个对象的指向复制了一份给了形参。并且当操作完add()方法后,list中存留的也是复制后的引用,所以当 a = null 后list中的引用也能正确的指向到对象。

所以我们可以理解为有两份引用了,也就是导致当我们执行 a = null 后GC不会回收开始a 所指向的对象,因为还有一份存在了list中。

第二个问题,是关于强、软、弱、虚引用的,各个意思我就不解释了,具体的千万别百度。

我就简单解释其中需要用到的弱引用。

弱引用 WeakReference

如果一个对象只具有弱引用,那么垃圾回收器在扫描到该对象时,无论内存充足与否,都会回收该对象的内存。

第三个问题,最后的问题了,重头戏了。

Java里,每个线程都有自己的ThreadLocalMap,里边存着自己私有的对象。Map的Entry里,key为ThreadLocal对象,value即为私有对象T。在spring MVC中,常用ThreadLocal保存当前登陆用户信息,这样线程在任意地方都可以取到用户信息了。

每个Thread内部都维护一个ThreadLocalMap字典数据结构,字典的Key值是ThreadLocal,那么当某个ThreadLocal对象不再使用(没有其它地方再引用)时,每个已经关联了此ThreadLocal的线程怎么在其内部的ThreadLocalMap里做清除此资源呢?JDK中的ThreadLocalMap又做了一次精彩的表演,它没有继承java.util.Map类,而是自己实现了一套专门用来定时清理无效资源的字典结构。其内部存储实体结构Entry<ThreadLocal, T>继承自java.lan.ref.WeakReference,这样当ThreadLocal不再被引用时,因为弱引用机制原因,当jvm发现内存不足时,会自动回收弱引用指向的实例内存,即其线程内部的ThreadLocalMap会释放其对ThreadLocal的引用从而让jvm回收ThreadLocal对象。这里是重点强调下,是回收对ThreadLocal对象,而非整个Entry,所以线程变量中的值T对象还是在内存中存在的,所以内存泄漏的问题还没有完全解决。接着分析JDK的实现,会发现在调用ThreadLocal.get()或者ThreadLocal.set(T)时都会定期执行回收无效的Entry操作。


可以看个简单的例子

public class UserContext {
		private static final ThreadLocal<UserInfo> userInfoLocal = new ThreadLocal<UserInfo>();

		public static UserInfo getUserInfo() {
			return userInfoLocal.get();
		}

		public static void setUserInfo(UserInfo userInfo) {
			userInfoLocal.set(userInfo);
		}

		public static void clear() {
			userInfoLocal.remove();
		}
	}

我们可以看下源码,看它哪里用了弱引用

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

Entry中的key是弱引用,key 弱指向ThreadLocal<UserInfo> 对象,并且Key只是userInfoLocal强引用的副本(结合第一个问题),value是userInfo对象。

当我显示的把userInfoLocal = null 时就只剩下了key这一个弱引用,GC时也就会回收掉ThreadLocal<UserInfo> 对象。

但是我们最好避免threadLocal=null的操作,尽量用threadLocal.remove()来清除。因为前者中的userInfo对象还是存在强引用在当前线程中,只有当前thread结束以后, current thread就不会存在栈中,强引用断开, 会被GC回收。但是如果用的是线程池,那么的话线程就不会结束,只会放在线程池中等待下一个任务,但是这个线程的 map 还是没有被回收,它里面存在value的强引用,所以会导致内存溢出。



1、在ThreadLocal的生命周期中,都存在这些引用。看下图: 实线代表强引用,虚线代表弱引用。


2、ThreadLocal的实现是这样的:每个Thread 维护一个 ThreadLocalMap 映射表,这个映射表的 key 是 ThreadLocal实例本身,value 是真正需要存储的 Object。

3、也就是说 ThreadLocal 本身并不存储值,它只是作为一个 key 来让线程从 ThreadLocalMap 获取 value。值得注意的是图中的虚线,表示 ThreadLocalMap 是使用 ThreadLocal 的弱引用作为 Key 的,弱引用的对象在 GC 时会被回收。

4、ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,那么系统 GC 的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。

5、总的来说就是,ThreadLocal里面使用了一个存在弱引用的map, map的类型是ThreadLocal.ThreadLocalMap.Map中的key为一个threadlocal实例。这个Map的确使用了弱引用,不过弱引用只是针对key。每个key都弱引用指向threadlocal。 当把threadlocal实例置为null以后,没有任何强引用指向threadlocal实例,所以threadlocal将会被gc回收。

但是,我们的value却不能回收,而这块value永远不会被访问到了,所以存在着内存泄露。因为存在一条从current thread连接过来的强引用。只有当前thread结束以后,current thread就不会存在栈中,强引用断开,Current Thread、Map value将全部被GC回收。最好的做法是将调用threadlocal的remove方法,这也是等会后边要说的。

6、其实,ThreadLocalMap的设计中已经考虑到这种情况,也加上了一些防护措施:在ThreadLocal的get(),set(),remove()的时候都会清除线程ThreadLocalMap里所有key为null的value。这一点在上一节中也讲到过!

7、但是这些被动的预防措施并不能保证不会内存泄漏:

(1)使用static的ThreadLocal,延长了ThreadLocal的生命周期,可能导致内存泄漏。
(2)分配使用了ThreadLocal又不再调用get(),set(),remove()方法,那么就会导致内存泄漏,因为这块内存一直存在。

                
©️2021 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值