ThreadLocal的内存泄露?

ThreadLocal设计的目的

按照jdk源码的注释,ThreadLocal用于保存线程本地变量。区别于普通变量是所有线程共享的,ThreadLocal保存的变量不是共享的,每个线程有自己独有的一份(语句不严格,不过大家应该了解其含义)。

ThreadLocal使用的场景

根据ThreadLocal的特性,一般ThreadLocal用于保存线程上下文。说通俗一点就是线程独有,而且代码全局可访问。

解释:

1.线程独有,如果不是线程独有,使用普通变量即可。

2.全局可访问,如果不是全局访问,那变量肯定是函数中的临时变量,函数执行过程中的临时变量是在本线程私有的栈上面存储的,不会被共享,使用普通变量也没有关系。

应用示例:

1. spring事务,事务的开启和提交不在一个函数,但是两个操作必须是同一个connection,不然会提交错误的事务。这样数据库连接可以保存到ThreadLocal中,整个线程都可以访问到唯一且一致的connection。

2. 比如大规模系统中,日志都会被收集到ES。为了查询单次请求所有日志,我可以创建一个Filter,进来的时候使用ThreadLocal保存一个UUID,后续打日志的时候,统一加上这个UUID。这样通过该关键字查询日志系统,就可以把单次请求的日志按顺序输出,方便查阅。

ThreadLocal源码解读

public static void main(String[] args) {
	ThreadLocal<String> a = new ThreadLocal<>();
	a.set("aa");
	Thread current = Thread.currentThread();
}

以该段代码为例,内存结构如下:

说明:

1.线程内部有个字段为threadLocals,类型为ThreadLocalMap;该类型类似Map,用于存储key-value结构。Key为ThreadLocal变量,如示例代码中的变量a,value为ThreadLocal.set()方法传递的值,如示例代码中的"aa";

2.ThreadLocal.set()方法实际上是将值存入了当前线程threadLocals中。代码如下:

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

ThreadLocalMap中的Entry是WeakReference类型

static class Entry extends WeakReference<ThreadLocal<?>> {
    Object value;

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

ThreadLocalMap设计解析

名词说明:

本地变量:ThreadLocal.set()方法设置的值,或者说ThreadLocal对象持有的值。

 

ThreadLocal主要用于保存线程本地变量,所以肯定有个Map存储三者(线程、ThreadLocal、本地变量值)中两者的关系。如果Map放在ThreadLocal中,则(key,value)为(线程、本地变量),如果Map放在线程中,(key,value)为(ThreadLocal、本地变量)。

Map中的数据不主动remove,很容易产生内存泄漏。现实中应该有很多这种事故,变量存入集合中,使用完后忘记清理集合导致内存泄漏。

为了解决这个问题,jdk设计的ThreadLocal体系,不用大家显式去释放。

1.   ThreadLocalMap中的Entry是WeakReference类型,其"弱引用"ThreadLocal对象。这样当其他地方对ThreadLocal对象的引用解除后,其内存会被回收,Entry中的key被设置成null。这样就有机制感知到能够被回收的本地变量。

2.  如果线程结束,线程里面的ThreadLocalMap字段会被回收。这样线程本地变量会被回收,不会影响其他线程正常访问ThreadLocal。

3.  当前线程新创建ThreadLocal变量并调用其set方法时,会进入到ThreadLocalMap的set方法中。该方法在存储完key-value后会对失效的ThreadLocal进行清理,防止内存泄漏。

        private void set(ThreadLocal<?> key, Object value) {
            Entry[] tab = table;
            int len = tab.length;
            # 计算hash索引
            int i = key.threadLocalHashCode & (len-1);

            for (Entry e = tab[i];
                 e != null;
                 # 使用开放地址法解决哈希碰撞,而不是常规的链表
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();
                # 因为没有创建新的Entry,不会增加内存,所以不会去清理无效的ThreadLocal
                if (k == key) {
                    e.value = value;
                    return;
                }
                # k == null,说明该Entry之前存储的ThreadLocal已经失效
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            tab[i] = new Entry(key, value);
            int sz = ++size;
            # 清理无效的ThreadLocal
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

从代码中可以看到以下设计:

1.使用的时开放地址法解决哈希碰撞,而不是常规的链表。开放地址法内存利用率更高,一般ThreadLocal变量不会大规模使用,所以使用的时开放地址法能够有效节省内存。

2.set方法也不是每次都会清理无效的ThreadLocal,而是由新增Entry时才会触发清理。这样兼顾了性能和内存占用。因为不新增Entry,不会造成内存增加,没必要实时去清理失效的ThreadLocal。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值