**
什么是ThreadLocal?
**
在理解ThreadLocal前我们要明白什么情况下会使用ThreadLocal。
在做项目时,我们按照三层架构来开发的情况下,会有Service层调用Mapper层来完成相应的业务逻辑。拿具体案例来说,Service在做转账这个业务逻辑时,会有一个人的账户余额增加,另一个账户余额减少。但这里Service会调用Mapper中的两个方法来完成,第一个方法是修改对应数据库表中转账人的余额减少的方法,另一个方法是收账人余额增加的方法。
新建maven项目:
pom文件如下:
项目目录结构如下:
TransferStart用来模拟请求,调用Service:
TransferService用来调用Mapper的方法完成转账的业务逻辑:
AccountMapper提供关于账户的相关操作:
ConnectionPool封装Durid连接池,提供Connection连接:
运行前看一下数据库表的原始数据:
运行后的数据:
我们可以看到当用户账户增加和减少操作之间没有异常时成功了,现在我们在service执行账户增加和减少中加入一个异常再来运行。
数据库表的初始值:
运行后:
我们发现此时出现了错误,1账户减少,2账户未增加,没有保证事务的原子性。
要保证事务的原子性,我们可以在Service获取Connection对象,然后关闭自动提交,成功则提交,失败则回滚。并且Mapper方法所用的Connection要和Service获取的一致,我们不得不将Connection对象传入Mapper中去(代码省略…),这样会导致代码耦合度变高,出现了一些与业务逻辑无关的参数。
于是乎我们将Connection设置为Mapper的属性并赋值,然后在Service中去拿到那一个Connection对象,但多线程运行下会导致使用同一个Connection对象出现问题,这个时候ThreadLocal便派上用场了,它可以让每个线程运行时,有自己单独的副本,各个线程都用自己的Connection对象提交事务。此时可以对Util下的连接池更改,让同一个线程操作时获取的Connection对象是同一个对象。
ConnectionPool更改后代码如下:
TransferService更改后代码如下:
此时再运行时,同一个线程中Service和Mapper中拿到的Connection对象则会是同一个,看一下运行结果:
运行前数据库表:
运行后:
数据库表:
我们发现回滚成功,证明Service和Mapper中拿到的是同一个Connection对象。
那么ThreadLocal是怎么做到各个线程有自己的资源副本呢?
不看源码的话我们可能会这样理解:
一个threadLocal对象中有一个存放节点的数组threadLocalMap对象,而节点的Key对应着我们的线程,value就是该线程独有的数据。这样确实能实现,但这样的话threadLocal对象的节点数组可能就会很大,它会存放很多线程的数据,而且不方便管理。所以没有采用此设计方案,真正的设计方案如下:
不仔细看你可能觉得这是同一张图,但这儿是有很大区别的。这里的话ThreadLocalMap对象不再放在ThreadLocal对象中,而是在Thread对象中。当我们通过一个ThreadLocal对象去设值时,则会在当前Thread对象的ThreadLocalMap数组对象中去设置一个节点,该节点的key就是ThreadLocal对象,值就是我们通过ThreadLocal对象设置的值。这样就能做到每个线程对应的同一个ThreadLocal对象都有自己的值。
首先我们来看一下ThreadLocalMap这个存放数据的数组类在哪儿。
可以看到ThreadLocalMap是ThreadLocal类的内部静态类,但这只是ThreadLocalMap的类定义的位置,其实它实例化是创建在每个线程内部。下图是Thread类中ThreadLocalMap属性的位置。
回到我们刚才通过ThreadLocal对象去获取和设置每个线程自己的Connection对象的例子。
我们来看看ThreadLocal中的get方法会做什么:
首先获取当前是哪一个线程,再去通过getMap方法获取这个线程自己的一个ThreadLocalMap对象,下图是getMap方法:
因为是第一次获取,这个时候我们获取的map是为空的,那么他会return一个setInitialValue方法返回的值,下图是setInitialValue方法。
此时在setInitialValue方法中getMap返回当前对象的ThreadLocalMap对象也为空,便会走createMap为当前线程创建一个ThreadLocalMap对象,并放入值为initialValue方法返回的值,这个值是空的。
createMap则会为当前线程t创建一个新的ThreadLocalMap对象,第一个节点的key为this(即为我们调用此方法的threadLocal对象),value值则为initialValue方法返回的值null。
此时我们知道了第一次get时是空值,创建了对应线程的map和初始节点(值为空)。再回到我们例子中。
下一步我们判断了connection是否为空,为空则从连接池中取出连接并通过ThreadLocal对象来set。
set也会拿到当前线程的map调用ThreadLocalMap的set去设置,这时便会覆盖我们刚才的value为空的节点。所以在下次拿的时候,只要是同一个线程,拿到的值便会是同一个。