ThreadLocal不只要应付面试,更要真的理解,真的会用

前言

记得我几年前第一次面试的时候,就是被问了这个,记得面试官直接就让我说说ThreadLocal的实现原理以及平时有没有见过哪些地方用到了。
我当时初入职场,仍是一个大菜鸟,因此直接就被干蒙了,至今还记忆犹新。java

闲来无事,总结一下这块,其实仔细想一想这个ThreadLocal,总体思路其实挺清晰的,但有些细节会有难度,可能会涉及到一些比较深的平时不用的知识,说实话我也尚未彻底理清楚,但一直都在努力中。web

概念

定义

咱们说的ThreadLocal是java.lang包下的一个类,这个类提供特殊的线程局部变量,使得每一个访问该变量的线程在其内部都有一个独立的初始化变量副本。
用人话解释:
先说普通类中定义的变量,咱们都知道是多个线程共有的。
而ThreadLocal这个类中有个特殊的变量,特殊就特殊在针对不一样的线程,在用这个ThreadLocal的时候,都能拿到本线程独有的值,你能够set,能够get,线程之间互不影响。面试

其实ThreadLocal这个概念,并非java语言独有的,其实不少语言都有这个概念,只不过java中是用哈希表实现了这个概念。sql

特色

简单,开销小,线程安全。数据库

哪里用ThreadLocal

一、Quartz的SimpleSemaphore,提供资源隔离

在这里插入图片描述
在这里插入图片描述
看上图:
SimpleSemaphore里面就有个方法:obtrainLock方法,用synchronized锁
这个方法中有个很重的while操做(消费者处理完全部事情,须要等待新的事情,这个等待是一个while循环)
lockName是这个方法的入参,这个while方法的判断逻辑是若是locks这个HashSet中有这个lockName,这个线程就执行wait()方法,因为obtrainLock自己是一个所方法,而后再去执行wait(),你的线程就被彻底阻塞在这里排队了。
试想,若是没有ThreadLocal先过滤,那么同一个线程的屡次调用这个obtrainLock方法,带着相同的lockName,就会屡次进入这个while循环,其实同一个线程是不须要屡次进入这个操做的
因此经过在这个加锁操做以前用ThreadLocal判断(isLockOwner方法),将同一个线程带着相同lockName调用这个方法的次数,就减小到一次了,即只会第一次进入while循环,其余的都被isLockOwner方法挡住了
最终使得访问后面很重的操做的频率大大下降,算是一个优化。安全

二、Mybatis的SqlSessionManager,资源持有

咱们知道Mybatis连数据库后,会有个链接池,里面会维护有多个链接,每次操做数据库,都须要拿到链接,再去操做,拿链接就是那个sqlSession.getConnection方法,每次操做均可能拿到任何一个链接。
若是想要支持事务,那必须让一次事务的全部操做,都必须让同一个链接处理,这样才能要么一块儿成功,要么一块儿失败,而一次事务的每一个操做都须要从线程池中拿链接,那如何保证一次事务的每次操做拿到的都是同一个链接呢?
一次事务的多个操做通常都是一个线程去执行的,那其实问题就变成如何保证一个线程拿到的老是相同的一个链接,这里就用到了ThreadLocal,将当前线程拿到的链接保存在ThreadLocal中,下次该线程拿链接,就直接从ThreadLocal中拿这个链接,这样就保证了同一个线程永远拿到同一个链接,而其余线程拿哪一个链接不受这个线程的影响。分布式

咱们看看具体的代码实现:
先是定义ThreadLocal,存放的就是SqlSession,每个链接对应一个SqlSession
在这里插入图片描述
而后开始将一个线程的SqlSession放入ThreadLocal中
在这里插入图片描述
真正用的时候,好比commit,rollback等方法,就都从ThreadLocal中获取链接了。
在这里插入图片描述svg

三、Spring的TransactionContextHolder

在这里插入图片描述
TransactionContext也叫分布式事务资源池,保存的是当前环境的上下文,里面有个PlatformTransactionManager,这个就是执行commit和rollback的类,因此在分布式事务中也要保住同一个线程用同一个PlatformTransactionManager去执行commit或rollback,因此最终TransactionContext用ThreadLocal保存起来,达到效果。优化

四、登陆

登陆的时候,能够把每一个线程的登陆信息放在ThreadLocal中,就保证了同一我的的操做始终在同一个线程中。线程

ThreadLocal核心源码解读

一、首先,每一个Thread中,都有一个成员变量threadLocals

这个是专门为ThreadLocal加的,具体threadLocals的赋值过程,是在ThreadLocal中
threadLocals的类型是ThreadLocal.ThreadLocalMap,这个ThreadLocalMap是ThreadLocal中的自定义的一个内部map类,key是ThreadLocal对象,value是每一个线程的那个独有的变量副本。
在这里插入图片描述

二、ThreadLocal的get方法

在这里插入图片描述
先拿到当前线程
getMap方法,就是从当前线程中拿ThreadLocalMap,这个就是Thread中那个成员变量。
ThreadLocalMap的key是当前这个ThreadLocal对象,value就是咱们这个get方法真正要返回的值。
若是能拿到ThreadLocalMap,那么就返回ThreadLocalMap中当前ThreadLocal对象对应的value值。
若是拿不到ThreadLocalMap,就去初始化value,最后再返回value。

总结

咱们看到ThreadLocal的实现,就能清楚的知道为何ThreadLocal能够保存不一样线程的不一样值了。
是由于其实最终这些值仍是保存在了各个线程中的一个map中,而ThreadLocal仅仅是做为这个map的一个key。
那么对于一个线程,若是他遇到多个ThreadLocal,其实线程中的那个map就有多对值了。
有没有一种反向操做的感受,乍一看觉得这些值都是保存在ThreadLocal中的,最终发现仍是在线程中保存。

注意

要注意的是,每一个线程中的ThreadLocalMap是ThreadLocal中定义的一个静态类,至关于ThreadLocal重写了一个map,那有人会问了,为何不直接用HashMap呢?

其实这是一个涉及到java垃圾回收的问题,重写的这个ThreadLocalMap,主要就是为了这个事情搞的。

咱们知道其实HashMap中真正的数据是在一个个Entry中的,其实ThreadLocalMap也是这样,只不过ThreadLocalMap中的Entry是继承了WeakReference这个类。咱们知道ThreadLocalMap中的key值,实际上是ThreadLocal对象,在set某个对象的时候,须要根据这个对象的hash值去hash表中找槽,若是找到对应的槽后,槽上原来的对象被回收了,那对于的hash表上的位置的值就是null,那么ThreadLocalMap就会对这种已经废弃掉的null值对应的槽作一些处理(主要是从新回收这些槽,并从新分配hash表大小等)。这样至关于同步了垃圾回收的结果。

这就是为何要重写hashMap了,由于hashMap不会处理这些逻辑,不处理就会形成槽不断的被已经回收的ThreadLocal的空对象占用着释放不出来,最后影响hash的查找,由于时间久了,每次正常hash后应该放的槽都被null占了,只能继续向后移着放。