Threadlocal源码解析
- 一、基本结构
- 二、ThreadLocal操作
- set操作
- get操作
- remove操作
- 三、内存泄露?
- 四、ThreadLocalMap
- 核心变量
- 数组下标计算方式
- 阈值计算
- 扩容
- 下标冲突(hash冲突)
从名称上来看可以理解为线程本地变量,也可以认为是线程局部变量,线程与线程之间都是隔离的,所以说也是线程安全的,是典型的空间换时间的设计理念
一、基本结构
先看一下该类的重要成员和重要的内部类:
/** 以下三个不用管,只需要记住是用来计算类的Hash的就可以了 */
private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode = new AtomicInteger();
// 用来保证hash更均匀
private static final int HASH_INCREMENT = 0x61c88647;
// 重点内部类(内容省略,只需要记住是个Map)
static class ThreadLocalMap {}
上面最关键的就是ThreadLocalMap 这个Map,其他三个都是用来计算Threadlocal自身的hash的,因为在ThreadLocalMap 里面Key 就是Threadlocal自身,所以综合下来线程Thread与 ThreadLocalMap 关系如下:
每个线程都会持有一个ThreadLocalMap, Threadlocal只是里面的key而已,一个线程要是有多个变量那应该是这样:
ThreadLocalMap和线程Thread是如何关联的呢?注意我图示Thread下面的 threadLocals
了嘛,线程类内部本身就有threadLocals
这个属性,这个属性就是ThreadLocalMap,如下:
二、ThreadLocal操作
经过上面相信大家已经对Thread、ThreadLocalMap、ThreadLocal三者结构有一定了解了,那ThreadLocal到底干了些什么?其实就是把自身当做key,作为一个中间者来交互,实现增删改操作
set操作
逻辑很简单:
- 从线程中获取map,获取到了就把自身作为key加上值 写入到map中
- map不存在就重新创建一个给线程,并写入key-value
源代码如下:
public void set(T value) {
Thread t = Thread.currentThread();
// 获取当前线程里面的ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null)
// map不为空 就把自身当做key,从map里面取值
map.set(this, value);
else
// map为空则为线程创建一个map 并写入值
createMap(t, value);
}
// 为线程创建一个map
void createMap(Thread t, T firstValue) {
// 创建一个map赋值给线程的threadLocals属性
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
// 线程获取Map就是获取线程的threadLocals属性
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
get操作
也是一样从线程中获取map,然后从以自身为key从map中获取value
如果map不存在则会新建一个map,set 一个null值,并返回null值
源代码如下:
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
// map存在 则以自身为key 从map中获取value
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// map不存在 则创建一个新的map 并返回null
return setInitialValue();
}
// 初始化map
private T setInitialValue() {
// 这个值就是null 下面返回的也是null set的值也是null
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
remove操作
移除也简单就是以自身为key,移除值
源代码如下:
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
三、内存泄露?
上面三个操作基本上就是ThreadLocal全部了(下面说map的),ThreadLocal有关最多的话题就是内存问题,难道用ThreadLocal很容易发生内存溢出吗?
个人觉得ThreadLocal跟内存溢出没关系,从根本上看ThreadLocalMap 是作为属性与Thread绑定的,Thread不结束,map的引用就不会释放,所以内存不会被回收,本质上是线程没结束,内存释放不了;从使用角度上来讲,我线程没结束,我也意识到了要清除map里面部分值来释放内存,但是突然发现清不了,因为key没了,value还存在,为什么会这样?
因为ThreadLocal作为key是弱引用,可以被回收,而value是强引用,试想你key都没了,还怎么移除value?
ThreadLocalMap内Entry定义如下,继成了WeakReference :
所以说如果Thread能很快的正常结束,那无事发生,如果Thread的生命周期很长,长时间存在的那种,那么在使用ThreadLocal的时候就要注意,不用的变量要及时的remove掉;线程不都很快结束吗?什么线程会长时间存在?想想线程池的核心线程
四、ThreadLocalMap
关于这个map我就不分操作一个一个说了,说几个关键的
核心变量
// 默认大小
private static final int INITIAL_CAPACITY = 16;
// 数组
private Entry[] table;
// 元素数量
private int size = 0;
// 扩容因子
private int threshold;
底层数据结构就是数组,但没有链表了,因为不是以链表的形式来解决hash冲突的
数组下标计算方式
threadLocalHashCode & (length-1)
threadLocalHashCode:就是开头说的ThreadLocal那三个来计算得出的
length:数组长度
阈值计算
数组长度的2/3
源代码如下:
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
扩容
- 双倍扩容
- 遍历之前的key-value,重新计算key的下标,然后放入新数组
源代码如下:
private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
// 扩容两倍
int newLen = oldLen * 2;
Entry[] newTab = new Entry[newLen];
int count = 0;
// 遍历之前的数组 重新计算之前key的hash 在写入新数组
for (int j = 0; j < oldLen; ++j) {
Entry e = oldTab[j];
if (e != null) {
ThreadLocal<?> k = e.get();
if (k == null) {
// 如果key为null 则把value也置为null
e.value = null; // Help the GC
} else {
int h = k.threadLocalHashCode & (newLen - 1);
while (newTab[h] != null)
// 如果下标已经有值了(hash冲突了) 则往后找空位
h = nextIndex(h, newLen);
newTab[h] = e;
count++;
}
}
}
// 重新计算阈值
setThreshold(newLen);
size = count;
table = newTab;
}
下标冲突(hash冲突)
与HashMap不同,这个是用开放寻址法(线性探测法),就是说如果当前下标冲突了,就往后找空位,直至找到一个空位放入
源代码如下:
// 传入长度和下标索引
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}