ThreadLocal简介
在多线程访问同一个变量场景下,我们可以使用加锁(synchronized)以解决线程安全问题,而ThreadLocal为我们提供了一种全新的思路。与它绑定的thread各自维护一份共享变量,所以共享变量的线程安全问题得以解决。下面我们通过源码看看它是如何做到的以及它存在的问题。
ThreadLocal类
在idea上可以看到,ThreadLocal的类结构如下,我们一一介绍
- 内部类:
- SuppliedThreadLocal:为ThreadLocal初始化value值
- ThreadLocalMap:后面介绍
- 变量:
- HASH_INCREMENT:一个16进制整数值
- nextHashCode:与HASH_INCREMENT可以获取较为均匀的哈希码,并且是原子性的
- threadLocalHashCode
- 方法:
- ThreadLocal()
- childValue():某些场景下需要将父线程的变量传递给子线程,可以使用InhroitableThreadLocal完成。在这里是提供一种警示
- createInheritedMap():创建本地线程的映射
- createMap():创建一个ThreadLocalMap
- get():获取map中的value
- getMap():获取map
- initialValue():默认初始化null
- nextHashCode()
- remove():移除map中的key
- set():设值map中的value
- setInitialValue():设值初始化value
- withInitial()
ThreadLocal重点介绍
在介绍ThreadLocal中的几个主要方法之前,我们有必要弄清楚ThreadLocal,Thread以及ThreadLocalMap三者之间的关系。按照我的理解Thread和本地value作为键值对存储与ThreadLocalMap中,而ThreadLocal维护这个map。参考了许多大神的博客以及视频之后,我发现我的理解是有误的。其实看源码应该知道,ThreadLocalMap一方面是ThreadLocal的内部类,另一方面是Thread的成员变量。而该map的键值对分别为ThreadLocal和共享变量。
- set()源码:
- 首先获取当前线程
- 获取当前线程绑定的ThreadLocalMap结构
- 如果map不为null,调用内部类ThreadLocalMap中的set方法(后文介绍)
- 如果map为null则调用createMap创建新的map
- 进一步跟踪createMap,里面是new ThreadLocalMap(),目的一目了然了
- get()源码
- 如果map不为null,调用getEntry方法
- 进入getEntry方法,首先获取下标i位置的entry e
- 如果e为null或者e的key值不等于当前k,则调用getEntryAfterMiss方法(后文介绍)
- 如果map为null,则调用setInitialVlaue方法,里面代码简单
- 如果map不为null,调用getEntry方法
ThreadLocalMap介绍
在看源码中,其实ThreadLocal的set/get方法都是调用的内部类ThreadLocalMap的set/get方法,下面分析一下ThreadLocalMap。
- 首先声明了一个Entry内部类,构造函数的两个参数分别为ThreadLocal类型的k,且为弱引用;以及Object的v
- 它的成员变量有Entry类型的数组table,初始容量大小为16,数组扩容阈值为当用到2/3时,数组的下标nextIndex,prevIndex
- 这里的prevIndex存在,是因为Entry数组被视为环形,所以当nextIndex为最后一个位置时,则需要取到数组的第一个位置
- set()源码
- 在多线程环境下,每个线程进入到这个set()时,都会通过下面这段代码获取table数组的下标i
int i = key.threadLocalHashCode & (len-1);
-
然后以该i值为起点,遍历整个table,目的是寻找是否有以当前线程为k的元素。如果寻找到这个k,则直接设值
- 在遍历代码块中,有判断k==null,出现这个的原因是我们前面介绍的弱引用,它的特点是随时会被垃圾回收
- 其中调用的replaceStaleEntry(),我不是很理解,这里找了一篇文章,可以去看下
- https://www.linuxidc.com/Linux/2018-01/149993.htm
- 如果遍历中没有找到相关的key,则新建一个entry放入table数组中
- 在多线程环境下,每个线程进入到这个set()时,都会通过下面这段代码获取table数组的下标i
ThreadLocal其它
- 为什么Entry设置为环形?
- 因为需要使用线性探测来解决哈希冲突
- 为什么ThreadLocal设置为弱引用类型?
- 在多线程场景下,如果key使用强引用,则有可能造成内存泄露。在使用弱引用时,由于key会随时被回收,则减少了内存泄露的可能性,但是依然有内存泄露的可能性。为什么呢?虽然key会被回收,但是大量的value依然存在!
- 如果想完全避免内存泄漏,需要使用remove方法