以前觉得鲁迅先生的“我家门口有两棵树一棵是枣树,另一棵也是枣树”完完全全是在凑字数。不过最近把这个句式一带入到“我的账户里有两只股票,一只是绿的,另一只也是绿的”,马上就懂了,不愧是大文豪,悲悯的氛围感一下就出来了。
上文介绍了security的用户信息保存在ThreadLocal中,那我们这篇文章聊下ThreadLocal
上节回顾
上篇文章主要是从具体实现层面来分析,基于源码和调用逻辑的分析聚焦于‘术’,我们再从‘法’的层面,换个视角看下为什么要这么做,加深理解;
我们部署的web应用,从tomcat——>security——>Ctrl——>Serveice——>Dao,我们需要用到用户信息的地方很多,如果换做你自己想法实现
1、最简单的就是透传,从头到尾的方法都加上User这个对象,需要时获取,这样做的缺点显而易见:
- 你不知道哪些方法要使用用户信息,为了后面万一需要使用,得把所有方法的入参一股脑的加上
- 写业务代码的人还要关注这个方法需不需要获取用户信息,导致所有接口入参冗余
- 后期修改变更极为痛苦
2、统一存储,然后提供工具类获取,这种方法比上一个优雅,但是存在一个问题是多个线程访问同一个资源,存在竟态条件,会有线程安全问题,那就得加锁,上锁的话性能就不行了
3、如果可以将用户信息进行本地化存储,让每个线程都有属于自己的本地资源,避免多线程之间的共享。这样ThreadLocal就应运而生
ThreadLocal初介绍
我们先看Thread类
public class Thread implements Runnable {
···
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */(开始学英语了,与此线程相关的线程本地值。这map 是由ThreadLocal 类进行维护)
ThreadLocal.ThreadLocalMap threadLocals = null;
/*
* InheritableThreadLocal values pertaining to this thread. This map is
* maintained by the InheritableThreadLocal class.(与此线程相关的InheritableThreadLocal值。这map 是由InheritableThreadLocal 类进行维护)
*/
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
···
可以看到ThreadLocalMap 是Thread类的一个属性;但是这些属性ThreadLocal有啥关系,怎么使用,我们先搁置,把后面看了再来解决
可以看到ThreadLocalMap是ThreadLocal的内部类,然后该内部类里面又存在一个内部类Entry,这个Entry理念和HashMap的Entry类似;可以看到ThreadLocalMap里面把我们需要存储的东西放到Entry[]数组table里面,即我们的用户信息是放到一个对象数组里面的,最终存储在Object value这个属性中;
我把上面这段话再捋一捋,把整个过程翻译下:
我们保存用户信息的时候,在security里面是运行的代码是 contextHolder.set(context); contextHolder就是一个threadLocal 对象,这个然后contextHolder调用的set方法如下:public void set(T value) { //获取到当前线程 Thread t = Thread.currentThread(); //根据线程获取当前线程的ThreadLocalMap对象 ThreadLocalMap map = getMap(t); if (map != null) //设置进去的key为ThreadLocal; value为我们想要保存的用户数据 map.set(this, value); else createMap(t, value); } ThreadLocalMap getMap(Thread t) { return t.threadLocals; }
public T get() { //获取到当前线程 Thread t = Thread.currentThread(); //根据线程获取当前线程的ThreadLocalMap对象 ThreadLocalMap map = getMap(t); if (map != null) { //this就是指代当前的这个threadlocal对象 ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } //找不到的情况下,调用初始化方法,返回初始值 return setInitialValue(); } ThreadLocalMap getMap(Thread t) { return t.threadLocals; }
ThreadLocal深入
弱引用&内存泄露
从图上可知,Entry是继承了弱引用的类,但是Entry本身不是弱引用,里面那个key才是,将key(即当前threadlocal对象)存在当前父类的referent属性中,value存放我们想要存储的值;
- 什么情况下会存在内存泄露?
拿线程池举例:线程池中的线程生命周期比较长,线程引用到线程对象的强引用链会一直存在,但是图片上面那条threadlocal的引用随着方法的出栈后,该条强引用链就没有了,这一时刻分析下threadlocal的
的引用链路,只存在一条了,就是放入到Entry-key里面存在一条弱引用链,然后发生gc,由于key是弱引用,所以
threadLocal 对象会被回收,这个时候Entry里面的key没了,没有key,那value肯定访问不到,这个时候Entry对象占用的内存区域无法被释放,造成内存泄露。 - 那为啥要设计为弱引用而不是强引用呢?
这个是没有明确答案的,只能自己揣摩设计思路,我的理解是threadlocal放入到Entry里面的key如果是强引用,使用完了,开发人员没有释放,会造成更大的内存泄露,两权相害取其轻,既然都会泄露,所以稍微选择影响小点的,所以改为弱引用;另外代码里面多处:set,扩容等都涉及清除掉Entry key为空的情况,反向印证了作者是知道这一点,且为之做了弥补措施
扩容
@TODO
Q & A
Q:为什么threadlocalMap要设计到Thread线程里面,而不是直接将threadlocal设计为一个公用的map,map-key为线程对象,map-value为我们想存储的值,岂不美哉?
A:这样多个线程访问threadlocalMap,就又回到集合下Map的思路了,会产生线程安全问题,同时加锁会导致性能损耗;
Q:为什么value不为弱引用?
A:Entry中的value如果弱引用,那么只要GC,value就会被清理掉,这时如果通过threadlocal对象查对应的值,是null;
Q:threadlocal有什么好处?
A:一是可以避免竞争,各自有一份副本;二是可以方便线程传参,比如登录用户信息;
Q:threadlocal使用场景?
A:保存数据库链接;如果一个请求中涉及多个 DAO 操作,而如果这些DAO中的Connection都是独立的话,就没有办法完成一个事务。但是如果DAO 中的 Connection 是从 ThreadLocal 中获得的(意味着都是同一个对象), 那么这些 DAO 就会被纳入到同一个 Connection 之下。