本地线程之ThreadLocal

本文详细探讨了ThreadLocal的工作原理,解释了内存泄漏的原因,并提供了清除ThreadLocal内存泄漏的方法。重点介绍了为何使用弱引用作为ThreadLocalMap的key,以及如何避免和解决内存泄漏问题。

ThreadLocal允许将每个线程与持有数值的对象关联起来。

​ ThreadLocal提供了get和set访问器,为每个使用它的线程维护一份单独的拷贝,所以get总是返回当前执行线程通过set设置的最新的值。使用ThreadLocal 变量来实现线程隔离。

​ ThreadLocal持有事务的上下文,当框架代码需要获得当前正在运行的哪个事务时,只需要从ThreadLocal中get事务的上下文即可,避免了多线程竞争导致的锁等消耗。

//将数据库链接的key放到ThreadLocal本地线程中,那么每个线程就会拥有属于自己的DataSource connection
private static final ThreadLocal<DBTypeEnum> contextHolder = new ThreadLocal<>();
public static void set(DBTypeEnum dbType){
    contextHolder.set(dbType);
}
public static DBTypeEnum get(){
    return contextHolder.get();
}
public static void remove(){
    contextHolder.remove();
}
1、ThreadLocal实现原理

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

//当前线程
Thread t = Thread.currentThread();
//维护的ThreadLocalMap映射表
ThreadLocalMap map = getMap(t);
//ThreadLocalMap映射表: key 是 ThreadLocal实例本身;value 是真正需要存储的 Object
static class ThreadLocalMap {
    static class Entry extends WeakReference<ThreadLocal<?>> {
        Object value;
        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
}
2、ThreadLocal 内存泄漏的原因

​ 从上面的源代码可以看到,threadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal不存在外部强引用时,Key(ThreadLocal)势必会被GC回收,这样就会导致ThreadLocalMap中key为null, 而value还存在着强引用,只有thead线程退出以后,value的强引用链条才会断掉。

​ 但如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:

Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value

导致value永远无法回收,造成内存泄漏。

3、如何解决ThreadLocal内存泄漏?

​ 每次使用完ThreadLocal都调用它的remove()方法清除数据。

void demoTest {
 threadlocal.set(xxx);
 try {
  // do something
 } finally {
  threadlocal.remove();
 }
}

​ 将ThreadLocal变量定义成private static,这样就一直存在ThreadLocal的强引用,也就能保证任何时候都能通过ThreadLocal的弱引用访问到Entry的value值,进而清除掉 。

​ threadLocal的remove()方法源码:

private void remove(ThreadLocal<?> key) {
    //使用hash方式,计算当前ThreadLocal变量所在table数组位置
    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);
    //再次循环判断是否在为ThreadLocal变量所在table数组位置
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        if (e.get() == key) {
            //调用WeakReference的clear方法清除对ThreadLocal的弱引用
            e.clear();
            //清理key为null的元素
            expungeStaleEntry(i);
            return;
        }
    }
}
//清理key为null的元素
private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;

    // 根据强引用的取消强引用关联规则,将value显式地设置成null,去除引用
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;

    // 重新hash,并对table中key为null进行处理
    Entry e;
    int i;
    for (i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
        //对table中key为null进行处理,将value设置为null,清除value的引用
        if (k == null) {
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            int h = k.threadLocalHashCode & (len - 1);
            if (h != i) {
                tab[i] = null;
                while (tab[h] != null)
                    h = nextIndex(h, len);
                tab[h] = e;
            }
        }
    }
    return i;
}
4、这里ThreadLocalMap的key为什么使用弱引用而不是强引用?
  • 当threadLocalMap的key为强引用回收ThreadLocal时,因为ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。

  • 当ThreadLocalMap的key为弱引用回收ThreadLocal时,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。当key为null,在下一次ThreadLocalMap调用set(),get()时候,其remove()方法的时候会被清除value值。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值