从获取当前用户到Security再到ThreadLocal(二)

以前觉得鲁迅先生的“我家门口有两棵树一棵是枣树,另一棵也是枣树”完完全全是在凑字数。不过最近把这个句式一带入到“我的账户里有两只股票,一只是绿的,另一只也是绿的”,马上就懂了,不愧是大文豪,悲悯的氛围感一下就出来了。

上文介绍了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有啥关系,怎么使用,我们先搁置,把后面看了再来解决


image.png

  可以看到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深入

弱引用&内存泄露

ThreadLocalMap源码图.png

  从图上可知,Entry是继承了弱引用的类,但是Entry本身不是弱引用,里面那个key才是,将key(即当前threadlocal对象)存在当前父类的referent属性中,value存放我们想要存储的值;


threadlocal的引用关系图.png(网图,侵删)
  • 什么情况下会存在内存泄露?
      拿线程池举例:线程池中的线程生命周期比较长,线程引用到线程对象的强引用链会一直存在,但是图片上面那条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 之下。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容