对ThreadLocal的理解

1、为什么需要ThreadLocal?

我们在做web应用时,会用到一些跟线程绑定的变量,举个例子:
1、每个用户登录后拿到属于自己的令牌token,这个token标识了用户的身份,这个是要在一个线程中贯穿始终的,比如在Spring MVC中在controller,Service或者其他对象中都可能需要获取登录用户的信息,这个时候就要根据这个token去取;

2、再比如日志信息,我们在打印日志的时候往往希望日志信息要带有一个标识,让开发者能知道这些日志是由同一个线程打印的,不然在分析日志的时候就很混乱,开发人员完全不知道日志的相关性;

怎么样在一个线程中都可以取到一些全局变量呢?上述这些,都是ThreadLocal 的应用,如果没有ThreadLocal,那么A用户拿到的token,就并不是线程安全的,会被后来登录的用户冲掉;

2、ThreadLocal原理

为什么Thread是线程安全的?我们可以从源码大致分析一下,首先看Thread类:

ThreadLocal.ThreadLocalMap threadLocals = null;

Thread类中有一个ThreadLocalMap 类型属性,看下这个内部类:

   static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
        private static final int INITIAL_CAPACITY = 16;
        private Entry[] table;

摘抄了一部分源码,我们大致分析一下, 首先这个Map中有一个Entry数组,然后看下Enrty类,这个类继承了WeakReference类(想了解弱引用的可以具体百度一下,这里不展开讲述),这里只讲出一个结论,仅被弱引用引用的对象在每次GC的时候都会(大概率会)JVM回收掉,为什么这里要设计成弱引用呢,我们暂时不回答这个问题,接着往下看,这里关注一下ThreadLocal的set方法:

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

从源码看出,其实ThreadLocal的set方法就是获取到当线程,然后往这个线程对象中的ThreadLocalMap中插入了一个键值对,这个key就是代表ThreadLocal对象(这里注意代表的是ThreadLocal对象,而不是你set进去的对象),而value才是我们真正的对象,举个例子:

public void setThreadLocal() {
  ThreadLocal<String> threadLocal = new ThreadLocal<>();
  threadLocal.set("123");
  }

上述代码大致相当于在ThreadLocalMap中插入了一个这样的数据结构:

{
	new ThreadLocal<>():"123"
}

如果说这个上面的setThreadLocal()方法执行完之后,threadLocal 在方法中的引用就结束了。
如果说Entry对key的引用为强引用,根据JVM内存回收算法我们知道,只要当前线程对象没有消亡,那么这个ThreadLocal对象就始终没办法被回收掉(因为始终存在一条从Thread->ThreadLocalMap->Entry的强引用链)
所以Entry对象对key的引用才被设置成了弱引用,这个情况下,如果外界对ThreadLocal对象失去了引用,JVM就可以在下次GC的时候将这个ThreadLocal对象给清除掉,这样这些map对应的key就变成了null。但是这里还是有个问题,虽然Entry对key(ThreadLocal对象)是弱引用,但是对value却是强引用,那这么时候对应的value怎么办呢?显然,ThreadLocal的设计非常巧妙,他在set,get,和remove方法执行的时候都会把map中值为null的value给清理掉。

3、内存泄露

这么看起来,ThreadLocal已经防止的内存泄露,为什么我们还要在用完每个ThreadLocal 之后把他给remove掉呢:我们看一个例子

 @GetMapping("/testThreadLocal")
    public void testThreadLocal() {
        ThreadLocal<byte[]> threadLocal = new ThreadLocal<>();
        threadLocal.set(new byte[1024*1024*512]);
    }

这个代码执行后,ThreadLocal的引用失效了,我们的堆内存却始终居高不下,哪怕我手动执行了GC
无论怎么GC内存都居高不下
再看一个例子:

 @GetMapping("/testThreadLocal2")
    public void testThreadLocal2() {
        new Thread(() -> {
            ThreadLocal<byte[]> threadLocal = new ThreadLocal<>();
            threadLocal.set(new byte[1024*1024*512]);
        }).start();

    }

在这里插入图片描述
从这里例子我们可以看出来,这个时候我手动后GC后竟然又回收了,这到底是为什么呢?
原因就是,Tomcat中处理每个请求其实底层维护的也是一个线程池,这个请求结束后,线程数如果没有超过核心线程数,那么线程是不会消亡的,如果不使用remove方法,那么这个线程中ThreadLocal存的对象就始终不会被回收,而这时Map中的key已经被置位null,基本上是永远也无法访问到这个对象的,所以就会造成内存泄露。
我们将第一个例子改成如下:

 @GetMapping("/testThreadLocal")
    public void testThreadLocal() {
        ThreadLocal<byte[]> threadLocal = new ThreadLocal<>();
        threadLocal.set(new byte[1024*1024*512]);
        threadLocal.remove();
    }

在这里插入图片描述
可以看到,手动GC后明显释放了内存,。
结论:所以,在使用ThreadLocal对象后,如果确定接下来不用了,一定要及时的remove掉。尽管你认为自己没有用到线程池,但是实际上,大多数情况下其实都是显示或者间接的用到了线程池。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值