当前位置: 首页 > 科技观察

InheritableThreadLocal异步数据传递原理

时间:2023-03-15 00:35:17 科技观察

由于上次主要分析的是如何解决异步获取不到Session的问题,分析留下的思考问题就不展开了:使用InheritableThreadLocal来传递Session,为什么用线程池不一定能拿到Session,而不是拿不到呢?在Java中,一个Java线程就是一个操作系统线程。创建线程需要通过newThread来创建,JVM将操作系统线程绑定到Thread。即使使用线程池,也需要通过newThread创建线程。Thread类有两个ThreadLocal字段:publicclassThreadimplementsRunnable{ThreadLocal.ThreadLocalMapthreadLocals=null;ThreadLocal.ThreadLocalMapinheritableThreadLocals=null;}InheritableThreadLocal是ThreadLocal的子类,本质上是一个ThreadLocal。在Thread类中,threadLocals和inheritableThreadLocals都是线程对象私有的,只能通过当前线程对象写入和获取数据,但是Thread会把写入inheritableThreadLocals的数据传递给子线程的inheritableThreadLocals。当我们向ThreadLocal或InheritableThreadLocal写入数据时,写入过程如下:1.ThreadLocal或InheritableThreadLocal首先调用Thread#currentThread静态方法获取当前线程的Thread对象;2、获取Thread对象的threadLocals或inheritableThreadLocals;InheritableThreadLocal对象作为键值写入当前Thread对象的threadLocals或inheritableThreadLocals字段。所以Thread的threadLocals和inheritableThreadLocals的key是ThreadLocal或者InheritableThreadLocal的实例,value是写入的数据。threadLocals我在之前的文章《反向理解ThreadLocal,或许这样更容易理解》已经详细介绍过了。本文重点介绍如何将inheritableThreadLocals传递给子线程。默认情况下,当我们使用newThread()创建线程时,会在Thread构造方法中通过Thread#currentThread获取到当前线程,并将当前线程作为新创建线程的父线程,所以有是父子线程关系。无论使用哪种重载的构造方法来创建Thread,构造方法中都会调用init方法完成初始化并对Thread字段赋值,而init方法中有这么一段代码:privatevoidinit(ThreadGroupg,Runnabletarget,Stringname,longstackSize,AccessControlContextacc,booleaninheritThreadLocals){......如果(inheritThreadLocals&&parent.inheritableThreadLocals!=null)this.inheritableThreadLocals=ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);......}在init方法,由于inheritThreadLocals参数默认为true,所以只要父线程的inheritableThreadLocals字段不为空,将父线程的inheritableThreadLocals复制到当前创建的线程对象中,就实现了数据的传递保存在父线程的inheritableThreadLocals中给子线程。使用InheritableThreadLocal我们要考虑的问题:内存泄漏。ThreadLocal.ThreadLocalMap使用数组来存储元素。与HashMap不同的是,它使用开放的寻址方式来解决哈希冲突。没有链表,数组可以通过动态扩充数组来无限存储元素。数组元素的类型是Entry。当我们向ThreadLocal.ThreadLocalMap写入一个key-value时,ThreadLocalMap会将key和value打包成一个Entry,通过key的hashcode值计算出索引值,并将Entry放入一个数组中。ThreadLocal.ThreadLocalMap.Entry类的源码如下:staticclassEntryextendsWeakReference>{Objectvalue;Entry(ThreadLocalk,Objectv){super(k);value=v;}}虽然key是一个弱引用的ThreadLocal,当ThreadLocal被释放的时候,Entry的key就变成了null,但是由于value还在,如果Thread没有被释放,这个Entry就不会被垃圾回收器回收。但是如果线程是临时创建的,在方法中创建,没有在其他地方引用,线程执行完成后会被JVM销毁,JVM会在线程执行前调用线程的exit方法清理线程对象线程实际退出。exit方法的部分源码如下。privatevoidexit(){...threadLocals=null;inheritableThreadLocals=null;...}因此,只要调用Thread对象的exit方法,就不会出现内存泄漏问题。线程只要用完就会被销毁,所以使用InheritableThreadLocal,不需要在子线程中调用InheritableThreadLocal的remove方法,也不会有内存泄露的可能。比如我们在项目中使用InheritableThreadLocal将Session传递给子线程:@GetMapping("/test")publicSsoUsertest(){//获取登录用户SsoUserssoUser=SsoUserManager.curLoggedUser();System.out.println(ssoUser.getUserCode());//支持子线程传递newThread(()->{try{Thread.sleep(100);SsoUserssoUser2=SsoUserManager.curLoggedUser();System.out.println(ssoUser2.getUserCode());}catch(InterruptedExceptione){}}).start();returnssoUser;}这种情况下,由于子线程只是临时创建的,我们不需要在子线程中调用InheritableThreadLocal的remove方法,只需要在父线程调用一次remove方法,因为tomcat的工作线程不会在一次请求结束后销毁。既然知道了InheritableThreadLocal是如何给子线程传递数据的,那么问题的答案就说了一半:由于InheritableThreadLocal只能将线程上下文传递给当前线程创建的子线程,所以只有线程池中的线程才会被当前线程创建的当前线程可以通过。但是要知道另一半的答案,我们还是需要从线程池中去寻找。不同参数构建的线程池是不一样的。常见的线程池有单线程线程池、只有固定数量的核心线程的线程池、固定数量的核心线程和非核心线程的线程池、只有非核心线程的线程池。线程池的几个构造参数解释如下:corePoolSize:核心线程数,不会释放的线程数(allowCoreThreadTimeOut设置为true时除外);maximumPoolSize:线程池的最大线程数,等于核心线程数与非核心线程数之和;keepAliveTime:一个非核心线程的最大空闲等待时间,如果超过指定的空闲时间没有任务,该线程将被释放;workQueue:任务队列,当核心线程数用完后,会将任务放入队列。1、线程池是一个临时的线程池。如果线程池是在当前线程中创建的,任务是当前线程提交的,那么线程池用完就会销毁,所以无论是哪种线程池,池中的线程都会be是由当前线程创建的。在这种场景下,InheritableThreadLocal可以将Context传递给线程池中的任意线程。2.线程池是全局线程池如果线程池是全局线程池:没有核心线程,非核心线程的keepAliveTime等于0:线程只有在使用的时候才创建,因为keepAliveTime等于0,线程用完就可以释放。在这种场景下,相当于创建了子线程,由当前线程来执行任务,所以可以实现透传;没有非核心线程:前一个(核心线程数)任务的提交都会创建线程,都是当前线程创建的。所以只有这几个任务的执行才能正常获取父线程写入InheritableThreadLocal的数据,后面提交的任务就不知道会拉到哪个核心线程执行;其他:....所以,如果线程池是全局线程池,那么不管是什么情况,都不建议让本文转载自微信公众号“Java艺术”,可以通过以下二维码。转载本文请联系爪哇艺术公众号。