ThreadLocal的实现机制
原文地址:《ThreadLocal的实现机制》
1、ThreadLocal的作用
ThreadLocal 用一种存储变量与线程绑定的方式,在每个线程中用自己的 ThreadLocalMap 安全隔离变量,这里的ThreadLocalMap可以近似的理解为Map,这个Map与线程绑定,每个线程都拥有一个Map,Map中存储的值以ThreadLocal对象为key,value为指定的具体对象。
2、ThreadLocal的内部组成
ThreadLocal类中对于对象的操作基本上都是通过操作ThreadLocalMap来完成实现的。其中ThreadLocalMap对于对象的储存方式如下:
可以看出,每个线程拥有一个ThreadLocalMap,在线程内部以key-value的形式对值进行存储。ThreadLocalMap对象中包含一个table,其实为一个Entry数组,每个Entry在这里主要用到两个属性,一个为value即代码中需要保存的值,另一个为referent,在操作中通过判断referent是否为当前的ThreadLocal来判断是都对当前的Entry进行操作。由于Entry继承了WeakReference,而弱引用对内存敏感,所以在操作每个Entry时候需要对referent进行非空判断,如果当前referent对象为空,则判断当前的Entry为脏数据或者过期数据,需要对其占用的空间进行垃圾回收(在代码中将value设置为空,jvm垃圾处理器会同意处理这些没有引用的对象)
3、ThreadLocal的关键实现代码
关于ThreadLocal的操作代码不外乎是set、get、remove操作。
3.1、首先看一下set方法。
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
获取当前线程对象的引用,通过getMap方法获取ThreadLocalMap,方法中直接返回了Thread对象的threadLocals属性。对threadLocals属性进行非空判断,如果为空则新建ThreadLocalMap对象,并将传入的value值保存在新建ThreadLocalMap对象中的Entry数组中,以当前ThreadLocal对象为Entry中的referent属性进行保存,并通过ThreadLocal中获取hashcode方法获取当前存储的值在Entry数组中下标索引的位置。具体的set代码如下:
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
这个方法为ThreadLocalMap的私用方法,由于ThreadLocalMap为ThreadLocal的内部类,可以在ThreadLocal中通过ThreadLocalMap对象进行直接调用。整个set方法可以分为三个部分:获取当前ThreadLocal的hashCode、Entry数组中添加或更新元素、检测部分槽位数据是否有效并判断是否需要扩展数组长度。关于ThreadLocal对象的hashCode产生可以阅读这边文章《深入理解 ThreadLocal》 ,文中给出了实例来证明ThreadLocal中hashCode产生方法是非常有效的。
接下来看一下更新或添加元素到数组中的过程:
根据计算出的索引位置获取Entry数组中指定的Entry对象,判断当前Entry对象是否为空,如果为空则直接使用当前ThreadLocal对象和待保存的值构建一个新的Entry对象并保存在数组当前位置,如果当前获取的Entry对象不为空,则表明存在hash冲突,当前数组下标位置被占用了。在代码中使用开放地址法来解决hash冲突问题。 在当前数组位置存在Entry元素,并且Entry有效的情况,将进入for循环,判断数组中下一个元素是否为空或者有效,如果条件成立则移除Entry对象并使用 当前ThreadLocal对象和待保存的值构建一个新的Entry对象并保存在数组当前位置 。
当新的值被成功添加至ThreadLocalMap时,会对整个数组中的一些元素进行有效性检测。并判断当前数组是否需要进行扩展,如果需要则执行rehash操作。
关于如何删除数组中的元素具体代码如下:
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// Rehash until we encounter null
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
代码中除了对指定元素的删除操作还有相当一部分的代码来完成部分元素的rehash操作,其操作包含的元素包含从当前staleSlot+1元素开始一直到循环为空元素下标为止。
3.2、ThreadLocal中的get方法
具体的代码实现如下:
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings(“unchecked”)
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
如果ThreadLocalMap为空则直接返回初始值,并创建ThreadLocalMap将初始值添加Map中的Entry数组中。
如果map不为空,则通过计算当前ThreadLocal对象的hash值,确定Entry在数组中的位置,并判断Entry中的referent是否为当前对象,如果为当前对象则返回Entry的value属性值,如果referent不是当前对象,则通过开放地址法确定下一个Entry对象位置,直至找到满足条件的Entry位置。在使用开放地址法需要Entry过程中也会对操作的Entry对象进行有效性检测,如果Entry无效则删除Entry。实现代码如下:
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)
return e;
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
3.3、ThreadLocal中的remove方法
remove方法可以理解为删除数组元素方法,在remove方法中,需要使用当前的ThreadLocal对象来获取Entry元素在数组中的下标。如果满足条件(没有hash冲突的情况下,计算出的hash值就是当前需要操作元素的下标),则直接将当前Entry对象中的referent属性置为null(将当前的Entry变为无效Entry),通过删除无效的Entry来移除Entry。每次remove方法都有可能删除ThreadLocalMap多个无效的Entry并产生部分元素rehase操作。具体的代码如下:
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}
其中Entry的clear方法,只是简单的将当前Entry变为无效数据。
4、关于ThreadLocal的总结
首先ThreadLocal的线程安全只是保证线程中产生的变量不会被其他线程修改,如果在多线程环境,把多个线程的共享变量存储到当前线程的ThreadLocalMap中,是无法保证变量值的更改是符合程序预期的。
由于ThreadLocal对象的hashCode值产生方式设计的很巧妙,很难产生hash冲突,所以在代码中有很多rehash操作都是不会执行或者只有极少数情况下会执行。如果ThreadLocalMap中存储的对象数量巨大,并且频繁出现hash冲突,在操作ThreadLocal时可能会频繁产生移除无效Entry和rehash操作。
整个ThreadLocalMap中的Entry数组长度在源码中只有扩展没有收缩。这样处理会不会导致因为Entry数组空元素数量巨大而产生的内存浪费。
通过上述的梳理,应该可以对ThreadLocal内存的实现有一个较为清晰的认识。并且明白为什么ThreadLocal可以做到变量的线程级隔离。
上一篇: 联想YOGA S940上架:4K分辨率+3D曲面玻璃
下一篇: ThreadLocal实现解析