前言
在前面的文章中,我们陆续对concurrent包中的常用类进行了依次介绍,涵盖范围包括各种锁、并发容器、队列,理解这些类的作用以及原理,可以帮助我们更好的应对并发场景下带来的挑战,如果您还对其中哪些类的实现不太熟悉,建议您阅读一下之前的文章。
本篇,我们来介绍一下ThreadLocal的作用及其原理,基于JDK1.8。
ThreadLocal介绍
ThreadLocal是线程内部的数据存储类,通过它可以指定的线程中存储数据,数据存储以后,只有在指定线程中可以获取到存储的数据,可以实现线程之间的数据访问隔离。
每个线程都保持对其线程局部变量副本的隐式引用,这里成立的前提条件是线程是活动的并且 ThreadLocal 实例是可访问的;在线程消失之后,其线程局部实例的所有副本都会被垃圾回收(除非存在对这些副本的其他引用)。
ThreadLocal提供的方法比较简单,如下:
方法名称 | 描述 |
---|---|
T get() | 返回此线程局部变量的当前线程副本中的值。 |
protected T initialValue() | 返回此线程局部变量的当前线程的“初始值”。 |
void remove() | 移除此线程局部变量当前线程的值。 |
void set(T value) | 将此线程局部变量的当前线程副本中的值设置为指定值。 |
方法比较简单,我们可以为当前线程通过set()方法设定指定的值,通过get()方法来进行获取,在线程执行结束的时候,通过remove()方法销毁变量。
那么ThreadLocal是如何做到线程之间数据隔离的呢?我们来看一下具体实现。
ThreadLocal内部实现
ThreadLocal内部采用了一个Map的结构,来存储变量。
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
/**
* The initial capacity -- MUST be a power of two.
*/
private static final int INITIAL_CAPACITY = 16;
/**
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/
private Entry[] table;
/**
* The number of entries in the table.
*/
private int size = 0;
/**
* The next size value at which to resize.
*/
private int threshold; // Default to 0
/**
* Set the resize threshold to maintain at worst a 2/3 load factor.
*/
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
/**
* Increment i modulo len.
*/
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
/**
* Decrement i modulo len.
*/
private static int prevIndex(int i, int len) {
return ((i - 1 >= 0) ? i - 1 : len - 1);
}
/**
* Construct a new map initially containing (firstKey, firstValue).
* ThreadLocalMaps are constructed lazily, so we only create
* one when we have at least one entry to put in it.
*/
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
....
}
其结构与HashMap相似,采用了数组的模型,存储元素。但是不同的是,存储元素的Entry对象继承了WeakReference,这意味着,其引用为null后,会被垃圾回收器进行回收。
最开始我们提到了ThreadLocal的作用,它为每一个线程保存了一份数据副本,而实现的方式是在Thread中提供了一个ThreadLocalMap的引用。
ThreadLocal.ThreadLocalMap threadLocals = null;
接下来,我们来看一个ThreadLocal是如何保存线程局部变量的。
ThreadLocal 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);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
set()方法中,主要进行了几部操作:
1、获取当前线程
2、获取当前线程保存的ThreadLocalMap对象
3、如果ThreadLocalMap不为空,将元素进行存储
4、如果ThreadLocalMap为空,初始化ThreadLocalMap
再看一下ThreadLocalMap是如何新增元素的:
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();
}
看到这里,如果您对HashMap的源码有所了解的话,一定非常的熟悉,但是ThreadLocalMap解决数组位置上元素冲突的方式,与HashMap并不一致,没有采用拉链表方式,而是采用线性探测法(不断加 1)。
根据ThreadLocal中初始化好的 threadLocalHashCode和table的长度进行与计算,计算出该ThreadLocal对应的数组下标,从该位置开始,遍历数组中的Entry元素,寻找当前ThreadLocal的key是否已经存在,如果存在,替换旧值。
如果entry里对应的key为null的话,表明此entry为staled entry,就将其替换为当前的key和value,replaceStaleEntry()的作用是 检查数组中的其他元素是否存在已经失效的情况,在行了不少的垃圾清理动作以防止引用关系存在而导致的内存泄露。
如果不存在,新增一个元素,放入数组中,并会进行启发式的垃圾清理,cleanSomeSlots()用于清理无用的Entry。
这里并不对其实现就进行深究。
在1.8中,为了避免弱引用带来的内存泄露问题,做了大量的引用清理实现。
ThreadLocal get()
我们再来看一下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();
}
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
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;
}
get()方法的实现比较简单,获取当前线程的ThreadLocalMap,获取map中当前threadlocal对象对应的entry,如果entry不为空,获取对应的value,如果为空,重新初始化value。
这里对于getEntry()的实现,我们简要分析一下。
1、根据ThreadLocal中的threadLocalHashCode值计算出该元素在数组中的位置
2、判断元素不为空,且元素存储的key值为要获取的key
3、如果条件2不成立,那么说明可能存在键冲突,则调用getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e)方法返回entry
getEntryAfterMiss()的流程:
1、首要前提,entry元素不为空,获取entry对应的key
2、判断key是否一致,如果是,返回entry
3、如果key为null,那么expungeStaleEntry()方法清除过期元素
4、其他情况则通过nextIndex方法获取下一个索引位置index
5、获取新index处的entry,再死循环2/3/4,直到定位到该key返回entry或者返回null
expungeStaleEntry()的作用是清除无用的元素,该方法对于ThreadLocal非常的重要,它可以清理掉无用的元素,避免出现内存泄露的情况发生。
ThreadLocal remove()
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
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;
}
}
}
remove()的实现较为简单,获取当前线程对应的ThreadLocalMap,移除指定ThreadLocal对应的entry。
同样的,在entry的引用清除后,调用expungeStaleEntry()方法,清除掉引用。
ThreadLocal initialValue与withInitial
ThreadLocal提供了一个protected的初始化方法initialValue,用于实现其的子类可以进行初始化的动作,在JDK 1.8中,新增了withInitial的方法,支持Lambda表达式,该方法与initialValue作用一样,都用于初始化动作。
public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
return new SuppliedThreadLocal<>(supplier);
}
static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {
private final Supplier<? extends T> supplier;
SuppliedThreadLocal(Supplier<? extends T> supplier) {
this.supplier = Objects.requireNonNull(supplier);
}
@Override
protected T initialValue() {
return supplier.get();
}
}
private ThreadLocal<Object> threadLocalWithInitial = ThreadLocal.withInitial(Object::new);
ThreadLocal内存泄露
上面我们介绍了ThreadLocal的内部实现,我们知道了ThreadLocal内部对key值的存储是基于WeakReference弱引用的,意味着只有引用为null的时候,才可以被GC回收。
当一个线程调用ThreadLocal的set方法设置变量时候,当前线程的ThreadLocalMap里面就会存放一个记录,这个记录的key为ThreadLocal的引用,value则为设置的值。
如果当前线程一直存在而没有调用ThreadLocal的remove方法,并且这时候其它地方还是有对ThreadLocal的引用,则当前线程的ThreadLocalMap变量里面会存在ThreadLocal变量的引用和value对象的引用是不会被释放的,这就会造成内存泄露的。
在JDK1.8中,ThreadLocal对于这里做出了一定的优化,在set()、get()与remove()的时候,都会对无用对象进行扫描并清理,但这并不是及时的,也不是每次都会触发执行的,因此味着还是有可能会出现内存溢出的情况。
所以在使用完毕后即使调用remove方法才是解决内存泄露的王道。
关于ThreadLocal内存泄露的文章,可以参考这篇博文,解释的较为详细:
使用ThreadLocal不当可能会导致内存泄露
(http://ifeve.com/使用threadlocal不当可能会导致内存泄露/)
结语
本篇,我们介绍了ThreadLocal的使用及其实现机制,ThreadLocal在多线程开发场景下,是非常常用的,但是如果使用不当,也会造成比较大的麻烦,因此,合理使用好ThreadLocal,并且了解其内部机制,是非常重要的。希望本篇文章,能对您有所帮助。