作用
- 多个线程之间的数据隔离
- 单个线程内部的简化参数(传递上下文)
使用
使用可太简单了,一个set 一个get
1 | ThreadLocal<String> threadLocal = new ThreadLocal<>(); |
原理说明
set和get究竟是怎么回事
set方法
1 | public void set(T value) { |
get方法
1 | public T get() { |
由前两行可以看到根据每个currentThread获取了map,因此不同的线程获取的map自然就不同,每个线程只能操控自己的map,而对于同一个线程,不管在哪里获取的map都是同一个。
而对于每个map,key为ThreadLocal这个类,value为我们set的值
当然这只是分析,我们可以进一步看getMap源码验证上图
先看getMap方法体:
1 | ThreadLocalMap getMap(Thread t) { |
直接返回了Thread类的一个属性成员
1 | public class Thread implements Runnable { |
到这里就可以完全说明不同的线程获取的map自然就不同,每个线程有自己的map了
map真的是map吗
再看看这个map,他叫ThreadLocalMap,跟hashmap一样吗?
部分源码如下:
1 | static class ThreadLocalMap { |
对比hashmap分析,我贴上Hashmap的部分源码
1 | public class HashMap<K,V> extends AbstractMap<K,V> |
我们都知道hashmap在树化之前是数组+链表,首先是Entry[] table,类似于hashmap“数组+链表”中的数组Node<K,V>[] table
那链表呢?答案是没有。从上面可以看出,hashmap的有指针域Node<K,V> next;,但是ThreadLocalMap的Entry是一个弱引用
综上所述,ThreadLocalMap其实并没有实现Map接口,只是一个Entry数组
ThreadLocalMap如何解决hash冲突
既然我们知道了ThreadLocalMap只是一个Entry数组,如果定义了多个ThreadLocal
1 | ThreadLocal<String> threadLocal1 = new ThreadLocal<>(); |
存东西的时候遇到hash冲突怎么解决呢,先说结论:hashmap遇到hash冲突后会跟在此下标对应的链表后面,而ThreadLocalMap没有链表,遇到hash冲突后计算下一个下标,直到数组在某个下标处没有值
1 | private void set(ThreadLocal<?> key, Object value) { |
get时候也是一样:计算hash下标–>判断key是否相等–>相等直接返回,不相等看下一个位置–>继续判断是否相等…
内存泄漏问题
key使用了弱引用,不存在内存泄漏问题,但是value存在
先看结构
当我们ThreadLocal tl = new ThreadLocal();的时候,实际上有两个引用指向了ThreadLocal,一个是tl本身是一个强引用,一个是getMap(当前线程)得到的ThreadLocalMap,它里面的Entry的key是一个弱引用,指向了ThreadLocal。
为什么这里要使用弱引用呢?
如果是强引用,则tl和key都是强引用,我们一般都是操作tl,也就是tl = null,但是key依然存在强引用,于是导致无法被gc回收,存在内存泄漏问题。
使用弱引用就不会内存泄漏?
不是的,即使key是弱引用,但是如果key被回收变为null,value无法被访问,依然存在内存泄漏问题。解决的办法是主动调用tl.remove();
对于Entry,key是弱引用,在gc时会被回收,但是value得等到该ThreadLocal/线程没有强引用时才会收,如果线程一直存在,比如线程池会复用线程,那么此时value就一直无法回收,造成内存泄露问题。
解决方法也很简单,养成好习惯,主动调用remove()方法
1 | ThreadLocal<String> localName = new ThreadLocal(); |
线程之间传递
Java中的对象都是在堆中的,是线程共享的,ThreadLocal也一样,只是通过一些手段来实现线程之间的隔离,如果要实现父子线程之间的数据共享,可以使用InheritableThreadLocal
其原理是判断父组件是否有InheritableThreadLocal,有就直接赋值给子线程
1 | //Thread的init方法中 |
常见场景
Spring的事务控制
最著名的肯定是Spring的事务控制了,操作数据库最终都是dao层的JDBC,那么就需要Connection连接对象,原则上来说增删改查操作都由Connection操作。本来也没什么,但是Spring为了把事务都控制在service业务层(虽然这些什么什么层本质一样,是自己抽象出来的概念),做了两件事:
- 为了保证原子性:保证单个线程中的数据库操作使用的是同⼀个数据库连接,而且为了让开发者不需要将同一个Connection对象作为参数在dao的函数里传来传去,使用时感知不到Connection对象,于是使用了ThreadLocal+AOP的方式,保证了
- 为了保证隔离性:使用ThreadLocal,各个线程之间的事务互不影响
贴一个自己使用的场景:存放登录信息
参考