Java ThreadLocal 实现原理 与 如何使用弱引用解决内存泄漏问题

一、ThreadLocal 有什么用

      ThreadLocal 诞生于 JDK 1.2,用于解决多线程间的数据隔离问题。也就是说 ThreadLocal 会为每一个线程创建一个单独的变量副本,在 Servlet 中就会将Request 和 Response对象存入ThreadLocal,我们在当前线程任意方法中都能通过RequestContextHolder 获取到当前请求的Request 和 Response对象 。
HttpServletRequest request =((ServletRequestAttributes)RequestContextHolder.getRequestAttributes()).getRequest();

二、ThreadLocal 使用示例

public static ThreadLocal<String> tlName = new ThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
    tlName.set("张三");
    new Thread(()->{
        tlName.set("李四");
        System.out.println(Thread.currentThread().getName()+"-" + tlName.get()); // Thread-0-李四
    }).start();
    Thread.sleep(10);
    System.out.println(Thread.currentThread().getName()+"-"+tlName.get()); // main-张三
}

在这里插入图片描述
这里可以看到同一个ThreadLocal对象tlName 在不同线程中可以独立设置自己的值,线程之间不会互相影响,下面会分析实现原理。

三、ThreadLocal 实现原理

分析ThreadLocal实现原理可以从它的set方法入手,set方法的源码如下:

// ThreadLocal 的set方法传入一个value
public void set(T value) {
    Thread t = Thread.currentThread(); // 获取当前线程对象
    // 调用ThreadLocal的getMap方法传入当前线程对象
    // 通过当前线程对象获取到当前线程的ThreadLocalMap对象,每个线程对象中都存储了自己的ThreadLocalMap对象
    ThreadLocal.ThreadLocalMap map = getMap(t); 
    // 这里会判断当前线程对象是否已经创建ThreadLocalMap,如果没有创建会先执行创建并且set
    // 如果已经创建则将当前的ThreadLocal对象为key,传入的value为值存储到当前线程的ThreadLocalMap中。
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}
// 这个方法就是通过线程对象点属性获取ThreadLocalMap
ThreadLocal.ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

可以看到,ThreadLocal为每个线程创建了一个ThreadLocalMap,在set的时候key就是当前的ThreadLocal对象,value就是set里的值,以上测试代码执行时逻辑内存空间如下图:
在这里插入图片描述

四、ThreadLocal 如何是使用弱引用解决内存泄漏问题

4.1、强引用内存泄漏分析

假设ThreadLocal没有使用弱引用而是使用的强引用,则会出现内存泄漏问题,这里对原理进行分析:

现在我们只考虑tlName这个对象,它通过new ThreadLocal<>()开辟了一个内存空间,当某线程进行set时,又在内存中开辟了一个空间存放ThreadLocalMap,线程对象的threadLocals对象指向这个ThreadLocalMapThreadLocalMapkeytlName这个对象,valueset的值,如下:

在这里插入图片描述

现在如果我们在线程中执行tlName = null,从逻辑上讲这个强引用就断开了,通过new ThreadLocal<>()开辟的内存空间就没用了,应该属于垃圾被GC回收,但问题是线程对象并没释放,其属性threadLocals还指向该内存空间,根据垃圾回收可达性算法,这两部分内存空间是不能被清除掉的,在使用线程池的业务中很容易出现这种问题,因为线程对象会出现复用的情况。
在这里插入图片描述

当然我们也可以手动将ThreadLocalMap中对tlName的引用删除,手动调用remove清除即可,但是在实际工作做并不一定记得手动清除,或者因为一些异常导致没有清除,这个时候还是会存在内存泄漏,ThreadLocal会使用弱引用来解决这个问题。

4.1、弱引用解决内存泄漏问题

      要想探究这个问题要先分析一下每个线程对象的threadLocals属性,这个属性是ThreadLocal.ThreadLocalMap 类型的,在ThreadLocal 第一次调用set方法时创建,通过上述的ThreadLocal实现原理可以知道,ThreadLocalMap就是每个线程真实用来存储ThreadLocalvalue的,其中key就是ThreadLocal对象,下面来分析一下ThreadLocalMap结构。

public class ThreadLocal<T> {
	// ... ...
    static class ThreadLocalMap {
    	// ThreadLocalMap 的Entry 对象实现了弱引用
        static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;
            // Entry 的构造方法 传入ThreadLocal 和value
            Entry(ThreadLocal<?> k, Object v) {
            	// 调用父类弱引用WeakReference的构造方法将ThreadLocal传入托管给弱引用管理
                super(k);
                value = v;
            }
        }
        // 多个ThreadLocal与value会存储在这个Entry数组中
        private ThreadLocal.ThreadLocalMap.Entry[] table;
		// 将ThreadLocal与value存储到Entry数组table中,并且会进行清理槽位判断
		private void set(ThreadLocal<?> key, Object value) {
            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);
            for (ThreadLocalMap.Entry e = tab[i];
                // ... ...
            }
            tab[i] = new Entry(key, value);
            int sz = ++size;
            // 当table中有Entry对象中的key为null时需要进行清理
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }
        // 清理槽位
        private void rehash() {
            expungeStaleEntries();
            if (size >= threshold - threshold / 4)
                resize();
        }
        // ... ...
    }
}

通过上述代码可以看到其中Entry就是一个弱引用,会通过弱引用WeakReference的构造方法将ThreadLocal传入托管给弱引用管理,也就谁说ThreadLocal中的key是通过弱引用指向new ThreadLocal<>()开辟的内存空间,所以当tlName = null时,这段内存空间由于只有弱引用指向它,经过一次GC直接就被清除了,key自动变为null,达到了预期的效果。
在这里插入图片描述

通过上图可以看到new ThreadLocal<>()开辟的内存空间被回收了,ThreadLocalMapkey也变为null,但这个Entry对象还在table数组中,value张三也没有被回收,如果张三是个大对象,没用了又占据着内存空间,那么ThreadLocal还是存在内存泄漏问题,不过上述代码中有一个调用rehash清理槽位的逻辑,在每次set时都会判断是否有keynull,如果有的话会将value置为null

要想彻底解决ThreadLocal内存泄漏问题需要手动调用ThreadLocal提供remove方法,或者set(null)也行,其实我们平时写代码感觉很少主动去写tlName = null这样的操作,但是如果tlName 声明周期只在某个方法里,方法出栈,线程还在的情况下,tlName 就不再属于GC Roots引用了,和tlName = null效果是一样的,可能不经意就造成内存泄漏。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值