ThreadLocal种种问题

为什么需要在拦截器删除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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值