学习一个东西首先要知道为什么要引入它,就是我们能用它来干什么。所以我们先来看看ThreadLocal对我们到底有什么用,然后再来看看它的实现原理。
ThreadLocal如果单纯从名字上来看像是“本地线程"这么个意思,只能说这个名字起的确实不太好,很容易让人产生误解,ThreadLocalVariable(线程本地变量)应该是个更好的名字。我们先看一下官方对ThreadLocal的描述:
该类提供了线程局部 (thread-local) 变量。这些变量不同于它们的普通对应物,因为访问某个变量(通过其 get 或 set 方法)的每个线程都有自己的局部变量,它独立于变量的初始化副本。ThreadLocal 实例通常是类中的 private static 字段,它们希望将状态与某一个线程(例如,用户 ID 或事务 ID)相关联。
1、每个线程都有自己的局部变量
每个线程都有一个独立于其他线程的上下文来保存这个变量,一个线程的本地变量对其他线程是不可见的(有前提,后面解释)
2、独立于变量的初始化副本
ThreadLocal可以给一个初始值,而每个线程都会获得这个初始化值的一个副本,这样才能保证不同的线程都有一份拷贝。
3、状态与某一个线程相关联
ThreadLocal 不是用于解决共享变量的问题的,不是为了协调线程同步而存在,而是为了方便每个线程处理自己的状态而引入的一个机制,理解这点对正确使用ThreadLocal至关重要
什么时候用用到:
举几个例子说明一下:
1、比如线程中处理一个非常复杂的业务,可能方法有很多,那么,使用 ThreadLocal 可以代替一些参数的显式传递;
2、比如用来存储用户 Session。Session 的特性很适合 ThreadLocal ,因为 Session 之前当前会话周期内有效,会话结束便销毁。我们先笼统但不正确的分析一次 web 请求的过程:
- 用户在浏览器中访问 web 页面;
- 浏览器向服务器发起请求;
- 服务器上的服务处理程序(例如tomcat)接收请求,并开启一个线程处理请求,期间会使用到 Session ;
- 最后服务器将请求结果返回给客户端浏览器。
从这个简单的访问过程我们看到正好这个 Session 是在处理一个用户会话过程中产生并使用的,如果单纯的理解一个用户的一次会话对应服务端一个独立的处理线程,那用 ThreadLocal 在存储 Session ,简直是再合适不过了。但是例如 tomcat 这类的服务器软件都是采用了线程池技术的,并不是严格意义上的一个会话对应一个线程。并不是说这种情况就不适合 ThreadLocal 了,而是要在每次请求进来时先清理掉之前的 Session ,一般可以用拦截器、过滤器来实现。
3、在一些多线程的情况下,如果用线程同步的方式,当并发比较高的时候会影响性能,可以改为 ThreadLocal 的方式,例如高性能序列化框架 Kyro 就要用 ThreadLocal 来保证高性能和线程安全;
4、还有像线程内上线文管理器、数据库连接等可以用到 ThreadLocal;
现在我们先来看一段代码:
运行结果:
这个例子告诉我们 每一个线程之间的变量是互相之间不影响。
接着我们再来看一个例子:
输出结果:
咦,为什么这个每一个数值不一样呢。不是说好的 互不影响吗?
这时候就要拿出我多久不动的画笔,来给你们解析下为什么会出现这个情况。
我们先来看下一下 这个Demo1和Demo2的区别。
Demo1:
Demo2:
问题来了,Demo1每一次返回的都是0一个基本类型。但是indexnum是一个对象。所以每一次指向的还是同一个对象,为了加深理解 我们画一幅图来表示下。
所以ThreadLocal只保存了对象的地址副本,我们初始化的对象都是指向同一个地址,所以就会有这样子的 问题。那我们怎么解决这个问题呢。其实很简单
只需要每一次new 一个新的对象就好了,这样子就不会指向同一个地址。
再来看输出结果:
接下来我们看一下内部的源码 小戴带你读 ThreadLocal源码
ThreadLocal里面有几个方法 最主要的就是
public T get() { }
public void set(T value) { }
public void remove() { }
protected T initialValue() { }
get()方法是用来获取ThreadLocal在当前线程中保存的变量副本,set()用来设置当前线程中变量的副本,remove()用来移除当前线程中变量的副本,initialValue()是一个protected方法,一般是用来在使用时进行重写的,它是一个延迟加载方法
1:get方法:
- 先获取到当先的线程
- 判断当前线程中是否包含了ThreadLocalMap
- map 是 null 或者 map中的本地变量是空的话 就去创建初始值
创建初始值的时候又再去判断map是否存在,
- 不存在的话初始化map并setmap的key和value
- 存在的话直接set map的key和value
这就是ThreadLocal的get操作。
2:set方法:
也很类似 先获取当前线程,然后从当前线程的ThreadLocalMap中set值 存在则直接set不存在则初始化并set值
3:remove方法:
获取当前线程,当前线程实例就是该map的key 获取并remove掉。
这个就是ThreadLocal常用的三个方法。
接下去我们再来了解下ThreadLocal中的ThreadLocalMap
通过之前的分析已经知道,当使用ThreadLocal保存一个value时,会在ThreadLocalMap中的数组插入一个Entry对象,按理说key-value都应该以强引用保存在Entry对象中,但在ThreadLocalMap的实现中,key被保存到了WeakReference对象中。
这就导致了一个问题,ThreadLocal在没有外部强引用时,发生GC时会被回收,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。
那有人会问为什么要使用弱引用,其实这跟java 的设计思想有关系,java一直提倡的是弱化指针管理。所以就采用了key是若引用。那我们来对比下两种情况
- key 使用强引用:引用的ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。
- key 使用弱引用:引用的ThreadLocal的对象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。
比较两种情况,我们可以发现:由于ThreadLocalMap的生命周期跟Thread一样长,如果都没有手动删除对应key,都会导致内存泄漏,但是使用弱引用可以多一层保障:弱引用ThreadLocal不会内存泄漏,对应的value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。
因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用引起的。
综上所述
我们再使用ThredLocal的时候,使用完毕都要调用下remove方法。清除数据。
血泪教训!!!
p3事故
还记得那时3月26号的那一天,我不会忘记。那天中午睡醒之后,小伙伴带着我去luckin coffee buy a bottle of coffee,美滋滋。又是一个喝着咖啡敲着代码的日志。这时候内部技术群突然反馈,商家登录信息串号了。透,脑瓜子嗡嗡的~这个时候我在想我们也没有改动代码为什么会出现这样的问题呢。
这个时候,我意识到问题的严重性,马上飞奔回办公室,此时故障时间已经超过5分钟。我打开ci界面,马上回滚,还好merchant发布的快,5分钟内都回滚完毕。通知运营群让商家刷新页面,故障得以恢复。
其实在回滚的时候我已经意识到问题出在哪里(ThreadLocal使用后没有清除),随即出现在我脑海里的是,为啥以前没有出现问题现在就导致了这个问题的发生。后定位到 spring包版本的升级导致了aop的执行顺序:
我们代码里做了一个兜底方案,就是通过aop都去删除当前使用的ThreadLocal信息。但是spring包版本升级之后,aop先执行了这个兜底方案,然后又去执行了其他使用ThreadLocal的场景。
导致清除完之后,又去执行了Get操作,导致其他用户访问,拿到了其他线程池里面的用户信息(tomcat线程池复用)
后续针对这种情况做了兜底方案的改正。通过filter去实现兜底方案 因为执行顺序是
before:
aop 没有指定order所以没有办法处理。
after:
最外设置一个filter 里面设置和移除ThreadLocal
在最外层配置filter,ThreadLocal设置只放在Filter里面进行,方法执行完毕后,在filter中清除ThreadLocal。
ps:不管有没有这层兜底,使用了ThreadLocal之后 都在在final代码块中revome掉。