什么,你的ThreadLocal内存泄漏了?


声明:本文转载自https://my.oschina.net/u/3790005/blog/3037839,转载目的在于传递更多信息,仅供学习交流之用。如有侵权行为,请联系我,我会及时删除。

微信公众号:IT一刻钟。大型现实非严肃主义现场,一刻钟与你分享优质技术架构与见闻,做一个有剧情的程序员。 关注可第一时间了解更多精彩内容,定期有福利相送哟。

又是一个风和日丽的早上。

这天小美遇到了一个难题。

原来小美在做用户服务鉴权的时候,需要根据每个请求获取token:

//获取认证信息
Authentication authentication = 
tokenProvider.getAuthentication(jwt);
//设置认证信息
SecurityContext.setAuthentication(authentication);

然后经过层层的调用,在业务代码里根据认证信息进行权限的判断,也就是鉴权。

小美心里琢磨着,如果每个方法参数中都传递SecurityContext信息,就显的太过冗余,而且看着也丑陋。

那么怎么才能隐式传递参数呢?

这个当然难不倒小美,她决定用ThreadLocal来传递这个变量:

class SecurityContextHolder {
	private static final ThreadLocal<SecurityContext> contextHolder 
	= new ThreadLocal<SecurityContext>();
	
	public SecurityContext getContext() {
		SecurityContext ctx = contextHolder.get();
		if (ctx == null) {
			contextHolder.set(createEmptyContext());
		}
		return ctx;
	}
}
......(省略不必要的)
SecurityContextHolder.getContext().setAuthentication(authentication);

整体思路上就是将SecurityContext放入ThreadLocal,这样当一个线程缘起生灭的时候,这个值会贯穿始终。

完美,小美喜滋滋的提交了代码,然后发布出去了。

结果第二天系统就出现异常了,明明是这个用户A的发起的请求,到了数据库中,却发现是操作人是用户B的信息,一时间权限大乱。

完蛋了。。。

这是为什么呢?

我们得先扯一扯ThreadLocal,Thread,ThreadLocalMap之间的爱恨情仇。

图片解说:

1.Thread即线程,内部有一个ThreadLocal.ThreadLocalMap,key值是ThreadLocal,value值是指定的变量值;

2.ThreadLocalMap内部有一个Entry数组,用来存储K-V值,之所以是数组,而不是一个Entry,是因为一个线程可能对应有多个ThreadLocal;

3.ThreadLocal对象在线程外生成,多线程共享一个ThreadLocal对象,生成时需指定数据类型<?>,每个ThreadLocal对象都自定义了不同的threadLocalHashCode;

4.ThreadLocal.set 首先根据当前线程Thread找到对应的ThreadLocalMap,然后将ThreadLocal的threadLocalHashCode转换为ThreadLocalMap里的Entry数组下标,并存放数据于Entry[]中;

5.ThreadLocal.get 首先根据当前线程Thread找到对应的ThreadLocalMap,然后将ThreadLocal的threadLocalHashCode转换为ThreadLocalMap里的Entry数组下标,根据下标从Entry[]中取出对应的数据;

6.由于Thread内部的ThreadLocal.ThreadLocalMap对象是每个线程私有的,所以做到了数据独立。

于是我们知道了ThreadLocal是如何实现线程私有变量的。 但是问题来了,如果线程数很多,一直往ThreadLocalMap中存值,那内存岂不是要撑死了?

当然不是,设计者使用了弱引用来解决这个问题:

static class Entry extends WeakReference<ThreadLocal<?>> {
    Object value;
    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

不过这里的弱引用只是针对key。每个key都弱引用指向ThreadLocal。当把ThreadLocal实例置为null以后,没有任何强引用指向ThreadLocal实例,所以ThreadLocal将会被GC回收。

然而,value不能被回收,因为当前线程存在对value的强引用。只有当前线程结束销毁后,强引用断开,所有值才将全部被GC回收,由此可推断出,只有这个线程被回收了,ThreadLocal以及value才会真正被回收。

听起来很正常?

那如果我们使用线程池呢?常驻线程不会被销毁。这就完蛋了,ThreadLocal和value永远无法被GC回收,造成内存泄漏那是必然的。

而我们的请求进入到系统时,并不是一个请求生成一个线程,而是请求先进入到线程池,再由线程池调配出一个线程进行执行,执行完毕后放回线程池,这样就会存在一个线程多次被复用的情况,这就产生了这个线程此次操作中获取到了上次操作的值。

怎么办呢?

 

解决办法就是每次使用完ThreadLocal对象后,都要调用其remove方法,清除ThreadLocal中的内容。 示例:

public class ThreadLocalTest {
    static ThreadLocal<AtomicInteger> sequencer = ThreadLocal.withInitial(() -> new AtomicInteger(0));
    static class Task implements Runnable {
        @Override
        public void run() {
            int initial = sequencer.get().getAndIncrement();
            // 期望初始为0
            System.out.println(initial);
        }
    }
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(2);
        executor.execute(new Task());
        executor.execute(new Task());
        executor.execute(new Task());
        executor.execute(new Task());
        executor.execute(new Task());
        executor.execute(new Task());
        executor.shutdown();
    }
}

输出:

0

1

0

2

3

1

这里就是错误的。 如果每次执行完调用remove:

@Override
public void run() {
    int initial = sequencer.get().getAndIncrement();
    // 期望初始为0
    System.out.println(initial);
    sequencer.remove();
}

输出:

0

0

0

0

0

0

输出则正常。

好了,本期就说到这里,转发加关注,是我分享的最大动力~

 

本文发表于2019年04月17日 00:00
(c)注:本文转载自https://my.oschina.net/u/3790005/blog/3037839,转载目的在于传递更多信息,并不代表本网赞同其观点和对其真实性负责。如有侵权行为,请联系我们,我们会及时删除.

阅读 1749 讨论 0 喜欢 0

抢先体验

扫码体验
趣味小程序
文字表情生成器

闪念胶囊

你要过得好哇,这样我才能恨你啊,你要是过得不好,我都不知道该恨你还是拥抱你啊。

直抵黄龙府,与诸君痛饮尔。

那时陪伴我的人啊,你们如今在何方。

不出意外的话,我们再也不会见了,祝你前程似锦。

这世界真好,吃野东西也要留出这条命来看看

快捷链接
网站地图
提交友链
Copyright © 2016 - 2021 Cion.
All Rights Reserved.
京ICP备2021004668号-1