为什么需要在拦截器删除threadLocal中的数据
因为在实际中的使用的时候,我们使用的tomcat的线程池时候,如果使用完不删除threadLocal里面数据,会导致数据可能在下次请求时,被其他线程可见。
ThreadLocal<String> threadLocal = new ThreadLocal<String>();
@RequestMapping("/test")
public Map<String,String> test(){
Map<String,String> resultMap = new HashMap<>();
String currentThreadName = Thread.currentThread().getName();
String s = threadLocal.get();
if (ObjectUtils.isEmpty(s)){
threadLocal.set(currentThreadName);
resultMap.put("code","200");
resultMap.put("message","当前线程名: "+currentThreadName);
return resultMap;
}else{
resultMap.put("code","300");
resultMap.put("message","当前线程名: "+currentThreadName+" threadLocal存在的线程名 "+s);
return resultMap;
}
}
这段代码就可以看见,threadLocal中会有数据冲突
ThreadLocal底层结构
每个thread中都存在一个map, map的类型是ThreadLocal.ThreadLocalMap. Map中的key为一个threadlocal实例.
private void set(ThreadLocal<?> key, Object value) {} 这是ThreadLocalMap,key是ThreadLocal的
ThreadLocal里面有个原子类,但是是用static修饰的,也就是说在类加载时,被赋值。
public class ThreadLocalDemo {
public static AtomicInteger atomicInteger = new AtomicInteger(0);
public AtomicInteger getAtomicInteger() {
return atomicInteger;
}
public static void main(String[] args) {
ThreadLocalDemo t1 = new ThreadLocalDemo();
ThreadLocalDemo t2 = new ThreadLocalDemo();
System.out.println(t1.getAtomicInteger() == t2.getAtomicInteger());
}
}
// 返回true
// 多个实例共用同一个原子类。他这边的用法可能和其他使用类加载不一样,其他的类可能是初始化一些数据
导致在创建ThreadLocal的时候
public class ThreadLocal<T> {
private final int threadLocalHashCode = nextHashCode();
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
}
也就保证了用户不会出现几乎相同的nextHashCode
魔数0x61c88647与碰撞解决#
- 机智的读者肯定发现ThreadLocalMap并没有使用链表或红黑树去解决hash冲突的问题,而仅仅只是使用了数组来维护整个哈希表,那么重中之重的散列性要如何保证就是一个很大的考验
- ThreadLocalMap通过结合三个巧妙的设计去解决这个问题:
- 1.Entry的key设计成弱引用,因此key随时可能被GC(也就是失效快),尽量多的面对空槽
- 2.(单个ThreadLocal时)当遇到碰撞时,通过线性探测的开放地址法解决冲突问题
- 3.(多个ThreadLocal时)引入了神奇的0x61c88647,增强其的散列性,大大减少碰撞几率
- 之所以不用累加而用该值,笔者认为可能跟其找最近的空槽有关(跳跃查找比自增1查找用来找空槽可能更有效一些,因为有了更多可选择的空间spreading out),同时也跟其良好的散列性有关
- 0x61c88647与黄金比例、Fibonacci 数有关,读者可参见What is the meaning of 0x61C88647 constant in ThreadLocal.java
0x61c88647 = 1640531527 ≈ 2 ^ 32 * (1 - 1 / φ), φ = (√5 + 1) ÷ 2, it is an another Golden ratio Num of 32 bits.
真正set时
放在当前线程的ThreadLocalMap里面,根据nextHashCode值,通过hash函数映射到一个数组里面
int i = key.threadLocalHashCode & (len-1);
会通过遍历Entry[] 数组,找到合适的位置放下
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;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
// ThreadLocal引用相同,则覆盖
if (k == key) {
e.value = value;
return;
}
// 引用已经过期(我不确定是不是就是已经被垃圾回收),清楚和占用
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
// 找不到合适的槽,就放在一个新的数组上
tab[i] = new Entry(key, value);
int sz = ++size;
// 达到阈值,扩容
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
我来解释一下ThreadLocal的内存泄漏
单个线程里面如果不断创建ThreadLocal,且不remove.而只是将ThreadLocal==null,垃圾回收的时候,可能会出现ThreadLocal被回收了,但是Entry数组里面对象的value,且没有被清除,就会导致value的内存泄漏。
但是吧,代理里面有一些避免的方法,就是遍历的时候,会进行看是否key是否过期(==null)
如果过期,就会替换。
如果线程exit的时候,内存就不会泄漏了,也就会清除Thread里面的ThreadLocalMap
ThreadLocal导致内存泄露的错误行为#
- 1.使用static的ThreadLocal,延长了ThreadLocal的生命周期,可能导致内存泄漏
- 2.分配使用了ThreadLocal又不再调用get(),set(),remove()方法 就会导致内存泄漏
- 3.当使用线程池时,即当前线程不一定会退出(比如固定大小的线程池),这样将一些大对象设置到ThreadLocal中,可能会导致系统出现内存泄露(当对象不再使用时,因为引用存在,无法被回收)
ThreadLocal导致内存泄露的根源#
- 首先需要明确一点:ThreadLocal本身的设计是不会导致内存泄露的,原因更多是使用不当导致的!
- ThreadLocalMap对象被Thread对象所持有,当线程退出时,Thread类执行清理操作,比如清理ThreadLocalMap;否则该ThreadLocalMap对象的引用并不会被回收。
/**
Thread里面的exit方法
* This method is called by the system to give a Thread
* a chance to clean up before it actually exits.
*/
private void exit() {
if (group != null) {
group.threadTerminated(this);
group = null;
}
/* Aggressively null out all reference fields: see bug 4006245 */
target = null;
/* Speed the release of some of these resources */
threadLocals = null;
inheritableThreadLocals = null;
inheritedAccessControlContext = null;
blocker = null;
uncaughtExceptionHandler = null;
}
- 根源:由于Entry的key弱引用特性(见注意),当每次GC时JVM会主动将无用的弱引用回收掉,因此当ThreadLocal外部没有强引用依赖时,就会被自动回收,这样就可能造成当ThreadLocal被回收时,相当于将Map中的key设置为null,但问题是该key对应的entry和value并不会主动被GC回收,
- 当Entry和value未被主动回收时,除非当前线程死亡,否则线程对于Entry的强引用会一直存在,从而导致内存泄露
- 建议: 当希望回收对象,最好使用ThreadLocal.remove()方法将该变量主动移除,告知JVM执行GC回收
- 注意: ThreadLocal本身不是弱引用的,Entry继承了WeakReference,同时Entry又将自身的key封装成弱引用,所有真正的弱引用是Entry的key,只不过恰好Entry的key是ThreadLocal!!
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
//这里才是真正的弱引用!!
super(k);//将key变成了弱引用!而key恰好又是ThreadLocal!
value = v;
}
}
public class WeakReference<T> extends Reference<T> {
public WeakReference(T referent) {
super(referent);
}
public WeakReference(T referent, ReferenceQueue<? super T> q) {
super(referent, q);
}
}
应用
主要解决的问题,就是同一个线程中信息传递的问题。其实还是同一个线程进行方法的调用,出栈入站,但是都还是一个线程里面的。
public class SiteContext {
// 类加载器会执行这个,且加载的数据只有一份
private static ThreadLocal<Map<String, Object>> CONTEXT = new ThreadLocal<Map<String, Object>>() {
@Override
protected Map<String, Object> initialValue() {
return new HashMap<>();
}
};
public static void set(String key, Object obj) {
CONTEXT.get().put(key, obj);
}
public static Object get(String key) {
return CONTEXT.get().get(key);
}
public static void clear() {
CONTEXT.remove();
}
public static String getSite() {
return (String) get("site");
}
}
@Slf4j
public class SiteContextInterceptor extends HandlerInterceptorAdapter {
/**
* 执行方法处理前
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
SiteContext.clear();
// 业务逻辑判断,举个简单的栗子,请求头token判断,如果验证通过,将用户信息放入threadLocal,不需要再Controller成再获取一遍
log.info("interceptor current thread name {}", Thread.currentThread().getName());
SiteContext.set("name", Thread.currentThread().getName());
return true;
}
/**
* 方法结束,清空threadLocal
* @param request
* @param response
* @param handler
* @param ex
* @throws Exception
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
SiteContext.clear();
}
}
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
// 托管
@Bean
HandlerInterceptor siteContextInterceptor(){
return new SiteContextInterceptor();
}
// 注册拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(siteContextInterceptor());
}
}
@RestController
@RequestMapping("/threadLocal")
@Slf4j
public class ThreadLocalController {
@RequestMapping("/get")
public String get(){
log.info("controller thread name :{}",(String)SiteContext.get("name"));
return (String)SiteContext.get("name");
}
}
测试
2020-07-22 14:22:29.174 INFO 10716 --- [nio-8080-exec-1] c.h.t.i.SiteContextInterceptor : interceptor current thread name http-nio-8080-exec-1
2020-07-22 14:22:29.181 INFO 10716 --- [nio-8080-exec-1] c.h.t.ThreadLocalController : controller thread name :http-nio-8080-exec-1
2020-07-22 14:22:34.931 INFO 10716 --- [nio-8080-exec-4] c.h.t.i.SiteContextInterceptor : interceptor current thread name http-nio-8080-exec-4
2020-07-22 14:22:34.933 INFO 10716 --- [nio-8080-exec-4] c.h.t.ThreadLocalController : controller thread name :http-nio-8080-exec-4
2020-07-22 14:22:39.203 INFO 10716 --- [nio-8080-exec-5] c.h.t.i.SiteContextInterceptor : interceptor current thread name http-nio-8080-exec-5
2020-07-22 14:22:39.203 INFO 10716 --- [nio-8080-exec-5] c.h.t.ThreadLocalController : controller thread name :http-nio-8080-exec-5
2020-07-22 14:22:39.410 INFO 10716 --- [nio-8080-exec-6] c.h.t.i.SiteContextInterceptor : interceptor current thread name http-nio-8080-exec-6
2020-07-22 14:22:39.410 INFO 10716 --- [nio-8080-exec-6] c.h.t.ThreadLocalController : controller thread name :http-nio-8080-exec-6
2020-07-22 14:22:39.612 INFO 10716 --- [nio-8080-exec-7] c.h.t.i.SiteContextInterceptor : interceptor current thread name http-nio-8080-exec-7
2020-07-22 14:22:39.612 INFO 10716 --- [nio-8080-exec-7] c.h.t.ThreadLocalController : controller thread name :http-nio-8080-exec-7
参考
https://www.cnblogs.com/hongdada/p/12108611.html