ThreadLocal
线程私有的存储数据结构,底层是 Map
Thread、ThreadLocal、ThreadLocalMap、Entry
Set()
方法中使用了 getMap(thread)
方法获取线程对应的 Map,再看 getMap(thread)
方法的源码
显然,threadLocals 是线程类 Thread 的一个成员变量
类型是 ThreadLocal.ThreadLocalMap
,其实 ThreadLocal 中的一个子类
为什么 ThreadLocal 可以解决线程安全问题
到目前为止,已经可以看到,ThreadLocal 是每个线程都有一个自己的 Map 来存储数据,存储的地方是分开的
再看 ThreadLocalMap 的源码,发现下面有个子类 Entry
就是 ThreadLocalMap 中存储的实体
Entry
继承了 WeakReference<ThreadLocal<?>>
弱引用
强引用与弱引用
弱引用与强引用的区别
- 强引用
引用 -> 对象
- 在 GC 时不会被回收,只有当
引用 -> null
,根据可达性分析,落空的对象才会被 GC 回收- 弱引用
引用 -> WeakReference弱引用对象 ……> 真实对象
- 一旦 GC,则
WeakReference弱引用对象
到真实对象
的引用就消失了,真实对象被回收,而引用
到WeakReference
的引用依然保持- 即
引用 -> WeakReference弱引用 -> null
- ThreadLocal 使用弱引用主要是为了让 ThreadLocal 生命周期与 Thread 生命周期解耦
ThreadLocal 内存结构图
- 虚线为弱引用、实线为强引用
- Entry 的 Key 为弱引用,指向 ThreadLocal 对象
- Entry 的 Value 为强引用,指向堆内存中的数据
- 每个 Thread 线程内部有一个 Map:
ThreadLocalMap
- Map 里面存储 Entry 实体,是一种
key-value
结构,其中 Key 为 ThreadLocal 对象;Value 是线程变量副本- Thread 内部的 Map 是由 ThreadLocal 维护的,由 ThreadLocal 负责向 Map 获取和设置线程的变量值
- 对于不同的线程,每次获取副本 value 值时,别的线程并不能获得当前线程的 value 值,形成了副本的隔离,互不干扰
这样设计的好处
- 每个 Map 存储的 Entry 数目更少,降低哈希冲突
- 当 Thread 销毁时,ThreadLocalMap 也会被销毁
内存泄漏
- 弱引用情况下:如果令
RESOURCE_1 = null
则,ThreadLocal 对象会被 GC 回收,两个弱引用从Key<ThreadLocal>
变为Key<null>
,但此时 Entry 中的 value 还指向着堆中的数据,就发生了内存泄漏
- 强引用情况下:Key -> ThreadLocal 的引用会导致 ThreadLocal 不能被回收
- 这种情况下,可以通过 遍历
KeySet
找到key == null
,然后把对应的value
也设置为null
就可以在 GC 时回收堆空间,ThreadLocal 的remove()
方法实现了上述逻辑
ThreadLocal remove() 方法
- 又调用了
ThreadLocalMap
的 remove 方法,遍历所有 Entry 查看 key 是否为 null,其中又调用了 Entry 的clear()
方法,以及expungeStaleEntry(i)
问题
看起来,应当手动使用 remove() 方法回收空间,否则就会内存泄漏,这样感觉有点蠢
- 很自然的有一种想法,在
set
新数据时,对可以被回收的空间进行回收 set
方法确实这么做了get
方法其实也对空间进行了回收
set 方法回收空间
- ThreadLocal 的 set 方法中调用了 ThreadLocalMap 的 set 方法
- 可以看到,如果 Key 不存在,表示会额外占用 Map 的空间,那么尝试替换已经失效的 Entry 占用的空间
replaceStaleEntry
方法,其中就调用了expungeStaleEntry
方法清除失效的 Entry
get 方法回收空间
- Get 方法调用了 getEntry 方法
- get 方法又调用了 getEntryAfterMiss 方法,其中使用 enpungeStaleEntry 对空间进行回收
弱引用可以解决内存泄漏问题吗
只靠弱引用不能解决内存泄漏问题
- 通过弱引用只是实现了对失效 Entry 的标记 (key == null)
- 还需要找到这些 Entry 进行回收
Hash 计算与 Hash 冲突
计算索引
这里定义了一个AtomicInteger类型,每次获取当前值并加上HASH_INCREMENT,HASH_INCREMENT = 0x61c88647,这个值跟斐波那契数列(黄金分割数)有关,其主要目的就是为了让哈希码能均匀的分布在2的n次方的数组里, 也就是Entry[] table中,这样做可以尽量避免hash冲突
计算hash的时候里面采用了 hashCode & (size - 1)
的算法,这相当于取模运算 hashCode % size
的一个更高效的实现。正是因为这种算法,我们要求size必须是2的整次幂,这也能保证在索引不越界的前提下,使得hash发生冲突的次数减小。
开放定址法——线性探测处理哈希冲突
- 首先还是根据key计算出索引 i,然后查找i位置上的Entry
- 若是Entry已经存在并且key等于传入的key,那么这时候直接给这个Entry赋新的value值
- 若是Entry存在,但是key为null,则调用replaceStaleEntry来更换这个key为空的Entry
- 不断循环检测,直到遇到为null的地方,这时候要是还没在循环过程中return,那么就在这个null的位置新建一个Entry,并且插入,同时size增加1
- 最后调用cleanSomeSlots,清理key为null的Entry,最后返回是否清理了Entry,接下来再判断 sz 是否>= thresgold达到了rehash的条件,达到的话就会调用rehash函数执行一次全表的扫描清理
Spring @Transaction 与 ThreadLocal
- 通过 Spring 管理数据库事务,
一个线程
发起事务,在这个线程内获取数据库连接
,多个DAO
对 数据库表进行操作- 如果自己实现这些的代码,直观的想法是在各个操作数据库的方法之间,传递同一个 Connection 连接对象,既然如此,不如把 Connection 对象存到当前线程里,用到的时候取出来,而不是在方法间传来传去
- @Transaction 注解就是用 ThreadLocal 来存储当前数据库连接的
使用 ThreadLocal 的场景
在一次 Request 响应中,不使用多线程的情况下,就是用一个线程来完成整个后台逻辑的,那么一些信息就可以存储在 ThreadLocal 中,在整个 Controller -> Service -> Dao 之间都可以使用
面试题
1、和Synchronized的区别
问:他和线程同步机制(如:Synchronized)提供一样的功能,这个很吊啊。
答:放屁!同步机制保证的是多线程同时操作共享变量并且能正确的输出结果。ThreadLocal不行啊,他把共享变量变成线程私有了,每个线程都有独立的一个变量。举个通俗易懂的案例:网站计数器,你给变量count++的时候带上synchronized即可解决。ThreadLocal的话做不到啊,他没发统计,他只能说能统计每个线程登录了多少次。
2、存储在jvm的哪个区域
问:线程私有,那么就是说ThreadLocal的实例和他的值是放到栈上咯?
答:不是。还是在堆的。ThreadLocal对象也是对象,对象就在堆。只是JVM通过一些技巧将其可见性变成了线程可见。
3、真的只是当前线程可见吗
问:真的只是当前线程可见吗?
答:貌似不是,貌似通过InheritableThreadLocal类可以实现多个线程访问ThreadLocal的值,但是我没研究过,知道这码事就行了。
4、会导致内存泄漏么
问:会导致内存泄漏么?
答:分析一下:
1、ThreadLocalMap.Entry的key会内存泄漏吗?2、ThreadLocalMap.Entry的value会内存泄漏吗?先看下key-value的核心源码
staticclass Entry extends WeakReference<ThreadLocal<?>> { Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; }}
先看继承关系,发现是继承了弱引用,而且key直接是交给了父类处理super(key),父类是个弱引用,所以key完全不存在内存泄漏问题,因为他不是强引用,它可以被GC回收的。
弱引用的特点:如果这个对象只被弱引用关联,没有任何强引用关联,那么这个对象就可以被GC回收掉。弱引用不会阻止GC回收。这是jvm知识。
再看value,发现value是个强引用,但是想了下也没问题的呀,因为线程终止了,我管你强引用还是弱引用,都会被GC掉的,因为引用链断了(jvm用的可达性分析法,线程终止了,根节点就断了,下面的都会被回收)。
这么分析一点毛病都没有,但是忘了一个主要的角色,那就是线程池,线程池的存在核心线程是不会销毁的,只要创建出来他会反复利用,生命周期不会结束掉,但是key是弱引用会被GC回收掉,value强引用不会回收,所以形成了如下场面:
Thread->ThreadLocalMap->Entry(key为null)->value
由于value和Thread还存在链路关系,还是可达的,所以不会被回收,这样越来越多的垃圾对象产生却无法回收,早晨内存泄漏,时间久了必定OOM。
解决方案ThreadLocal已经为我们想好了,提供了remove()方法,这个方法是将value移出去的。所以用完后记得remove()。
5、为什么用Entry数组而不是Entry对象
这个其实主要想考ThreadLocalMap是在Thread里持有的引用。
问:ThreadLocalMap内部的table为什么是数组而不是单个对象呢?
答:因为你业务代码能new好多个ThreadLocal对象,各司其职。但是在一次请求里,也就是一个线程里,ThreadLocalMap是同一个,而不是多个,不管你new几次ThreadLocal,ThreadLocalMap在一个线程里就一个,因为ThreadLocalMap的引用是在Thread里的,所以它里面的Entry数组存放的是一个线程里你new出来的多个ThreadLocal对象。
6、你学习的开源框架哪些用到了ThreadLocal
Spring框架。
DateTimeContextHolder
RequestContextHolder
7、ThreadLocal里的对象一定是线程安全的吗
未必,如果在每个线程中ThreadLocal.set()进去的东西本来就是多线程共享的同一个对象,比如static对象,那么多个线程的ThreadLocal.get()获取的还是这个共享对象本身,还是有并发访问线程不安全问题。