ThreadLocal 原理
1. ThreadLocal 基础使用
ThreadLocal 被称为线程本地变量类,当多线程并发操作线程本地变量时,实际上每个线程操作的是其独立拥有的本地值,可以理解为每个线程分别独立维护自己的副本。这样就规避了线程安全问题,从而达到无锁并发。
先来一个简单的使用示例:
@DatapublicclassUserContextExample{@DatastaticclassUserContext{// 定义一个用户上下文类privatefinalintuserId;privatefinalintsessionId;}// 设置为线程本地变量privatestaticfinalThreadLocal<UserContext>USER_CONTEXT_THREAD_LOCAL=newThreadLocal<>();// 设置当前线程的用户上下文publicstaticvoidsetUserContext(intuserId,intsessionId){USER_CONTEXT_THREAD_LOCAL.set(newUserContext(userId,sessionId));}// 获取当前线程的用户上下文publicstaticUserContextgetUserContext(){returnUSER_CONTEXT_THREAD_LOCAL.get();}// 清除当前线程的用户上下文publicstaticvoidclearUserContext(){USER_CONTEXT_THREAD_LOCAL.remove();}// 模拟 Web 请求publicstaticvoidmain(String[]args)throwsInterruptedException{ExecutorServicepool=Executors.newFixedThreadPool(5);for(inti=0;i<10;i++){finalintrequestId=i;pool.execute(()->{try{// 模拟从请求中获取用户信息setUserContext(requestId%3+1,requestId);// 拿到线程本地变量UserContextcontext=getUserContext();// 业务逻辑System.out.println(Thread.currentThread().getName()+" - Processing request for user: "+context.getUserId()+", session: "+context.getSessionId()+", requestId: "+requestId);Thread.sleep(100);}catch(InterruptedExceptione){Thread.currentThread().interrupt();}finally{// 必须清理,这一步尤为关键clearUserContext();}});}// 关闭线程池pool.shutdown();if(!pool.awaitTermination(10,TimeUnit.SECONDS)){pool.shutdownNow();}}} 可以看出,每个线程都独立维护了USER_CONTEXT_THREAD_LOCAL的值 ,相当于这样的结构:
2. 应用场景
线程隔离
这是 ThreadLocal 的最主要应用场景,常见的有:数据库连接管理,Session 数据管理。
对于数据库连接来说,一般完成数据库操作后就要将连接关闭,如果连接不是线程独享的,那么当一个线程完成数据库操作后就不能直接关闭连接,因为尚可能有其他线程连接着该数据库。
跨函数传递数据
一个线程设置 ThreadLocal 之后,对于这个线程的任何方法来说,都可以直接获取到其值,而无需通过方法参数传递。这通常适用于一些需要在函数之间频繁传输的数据。
3. ThreadLocal 原理
ThreadLocal 中使用了一个重要的数据结构用以维护众多线程的本地变量,称为 ThreadLocalMap。这个数据结构和 HashMap 的区别是,它使用了开放寻址法,而非 HashMap 的链地址法,并且它节点中的 key 均为弱引用包装过的,这个很重要,后面会说到。
ThreadLocal 提供的主要 API 其实都是在操作 ThreadLocalMap。其结构如下所示:
每个 Thread 实例拥有一个 Map 实例,每个 Map 实例中有许多 ThreadLocal 实例作为 key,对应的 val 为该 Map 所属 Thread 独立维护的版本。
从逻辑上讲,ThreadLocalMap 应当属于 Thread,但在代码层面 ThreadLocalMap 是作为静态内部类存在于 ThreadLocal 中,这容易让人误以为 ThreadLocalMap 属于 ThreadLocal。这其实是历史遗留问题,在早期的 JDK 版本中,ThreadLocalMap 的确是属于 ThreadLocal 的,也就是每个 ThreadLocal 实例都持有一个 ThreadLocalMap 实例,Map 里面以线程为 key,对应的 val 自然就是该线程维护的版本。这种方案的问题在于,在大部分的应用中,往往线程数是 ThreadLocal 实例数的十倍甚至百倍,如果以线程作为 key,Map 可能需要经常扩容,这样效率就比较低了。因此 JDK8 开始,已经将 ThreadLocalMap 在逻辑上归给 Thread,作为 Thread 的属性存在:ThreadLocal.ThreadLocalMap threadLocals;,不过 ThreadLocalMap 的源码依然存在于 ThreadLocal 类。
ThreadLocalMap 的节点使用弱引用进行了包装:
staticclassEntryextendsWeakReference<ThreadLocal<?>>{/** The value associated with this ThreadLocal. */Objectvalue;Entry(ThreadLocal<?>k,Objectv){super(k);value=v;}} 这个弱引用是什么意思呢?就拿刚刚的USER_CONTEXT_THREAD_LOCAL为例,我们知道这是一个引用,而且是强引用,引用的实例就是new ThreadLocal<>(),只要这个引用还存在,实例就不会被 GC 回收。ThreadLocalMap 的 key 也是一个引用,但它是被WeakReference类包装的。规则是,如果一个实例仅存在弱引用,下一次 GC 就会回收它。引用我们可以理解为一种对实例的追踪方式,弱引用就是一类不会影响 GC 的追踪方式。
privatestaticvoidrefTest(){ObjectstrongRef=newObject();// 强引用WeakReference<Object>weakRef=newWeakReference<>(strongRef);// 弱引用System.gc();System.out.println(strongRef);// java.lang.Object@46f7f36aSystem.out.println(weakRef.get());// java.lang.Object@46f7f36astrongRef=null;System.gc();System.out.println(weakRef.get());// null} 因此,如果这样写USER_CONTEXT_THREAD_LOCAL = null,那么实例就会被回收了。但事实上我们是没办法这样写的,因为已经将其设为 final 了,不能更改了。
需要注意的是,若实例被回收,entry 的 key 变为 null 之后,value 仍然强引用在 entry 中,当后续调用set、get、remove这些方法时,在方法内部才会触发这些 key 为 null 的 entry 的清理,也就是惰性清理的模式。因此,如果线程一直不终止(例如线程池中的线程),并且没有调用 ThreadLocal 的set、get、remove来触发清理,value 会一直存在,造成 value 的内存泄漏。
ThreadLocal 在规范上要设为 static final,因为从语义上来说,ThreadLocal 本身并不存储数据,而是作为键来访问每个线程的 ThreadLocalMap 中的值。一个 ThreadLocal 实例应该对应于一个特定类型的线程局部变量,这个对应关系是全局唯一且不变的,因此用 static 保证一个特定类型的 ThreadLocal 的全局唯一性。final 是为了不使外部修改其引用,一旦引用被修改,如USER_CONTEXT_THREAD_LOCAL = new ThreadLocal<>(),那么原来的实例由于没有强引用了,就会被回收,进而 ThreadLocalMap 中原来指向旧实例的 key 指向 null,进而无法访问原先的 val,造成数据丢失。
这里就有点矛盾,将 ThreadLocal 设为 final 会导致其永远存在强引用,ThreadLocal 实例就永远不会自动释放,key 就永远不指向 null,val 就永远不被清理。看了半天,弱引用也用不上啊。其实本来这个弱引用也只是一种防御性手段,始终记住在使用完一个线程本地变量后调用 remove 手动删除才是正经。
4. 结语
ThreadLocal 本质上还是空间换时间的思想,每个线程修改自己的副本,从而无锁并发执行。