TreadLocal
ThreadLocal解决的是线程内部变量的问题,并不是为了解决并发与共享变量的问题。
堆中有两个引用指向ThreadLocal,一个是ThreadLocal本身(强引用),一个ThreadLocalMap中Entry的key(弱引用)。
ThreadLocal的缺点?
ThreadLocal内部使用的线性探测法解决hash冲突,低效。可以使用Netty的FastLocalMap
内部变量为什么不在Thread开一个map?
现在的源码,我们无法直接访问Thread中ThreadLocaMap。如果放开的话。猜测:怕使用者用不好,更容易出现问题。
Thread 源码
Thread内部有threadLocals与inheritableThreadLocals两个变量,访问权限为包访问权限。都属于ThreadLocal的内部类,是一个map,线程的私有变量就是存储在这个结构中。内部类当做独立的类来看就行,因为编译后是两个单独的class。这样定义的原因是为了说明ThreadLocal与ThreadLocalMap相关的
public class Thread implements Runnable {
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
/*
* InheritableThreadLocal values pertaining to this thread. This map is
* maintained by the InheritableThreadLocal class.
*/
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
}
ThreadLocalMap的定义
static class ThreadLocalMap {
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
// key是弱引用;看super(k);key是一个ThreadLocal
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;
ThreadLocal.get()逻辑
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
// this就是当前threadLocal对象;找到对应的Entry
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 找不到就返回初始化的值,一般是NUll,如果使用ThreadLocal.withInitial(() -> Thread.currentThread().getName())初始化ThreadLocal;则会返回supplier的值
return setInitialValue();
}
getMap(t)逻辑
直接返回Thread的threadLocals变量
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
map.getEntry(this)逻辑
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
// 根据ThreadLocal对象的hash值计算应该取数组中的位置
// 正常来说一般都使用一个threadLocal;项目中的threadlocal一般都是static final的
Entry e = table[i];
// e != null但是 e.get() != key 说明hash冲突了,在ThreadLocal中没有链表解决冲突,继续往后找。
if (e != null && e.get() == key)
return e;
else
// 如果找不到,就执行此方法继续找
return getEntryAfterMiss(key, i, e);
}
getEntryAfterMiss()
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
// 继续往后找,只要往后找到一个位置是null,则说明没有这个entry
while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)
return e;
if (k == null) // 如果后面Entry的key是null,则清除这个Entry
// 堆中的ThreadLocal对象是被两个引用指向的,一个栈上的强引用,一个key的弱引用
// 一定是栈上的强引用先删除,在gc时才会触发弱引用删除
// 此时key为null,意味着栈上对threadLocal的引用没有了,此时由于key是弱引用,那么只要发生gc,ThreadLocal对象就会被回收,key指向的就null,此时需要清除这个没用的Entry
expungeStaleEntry(i);
else
i = nextIndex(i, len);//往下找,下标+1
e = tab[i];
}
return null;// 找不到就返回null
}
expungeStaleEntry()清除逻辑
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// 如果entry不为null,但是key是key是null。直接删除entry。
// 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)) {// 知道entry是null才
ThreadLocal<?> k = e.get();
if (k == null) {
// 如果entry不为null,但是key是key是null。直接删除entry。
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;
}
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);
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
private void set(ThreadLocal<?> key, Object value) {
// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.
Entry[] tab = table;
int len = tab.length;
// 同hash找到位置
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;// 如果不为null,说明key可能存在,看下是不是一样,一样的话就更新value。否则继续往后找
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {// 如果key==null 则替换
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);// 当前位置是null 则直接添加
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)// cleanSomeSlots也是删除key== null的Entry
rehash();// 扩容 2倍,使用hash&新的长度。
}
关于内存泄漏
很多人都说ThreadLocal有内存泄漏的风险。从分析源码可以看下,如果栈上的强引用删除,执行完一次GC之后,此时Entry中的key是null。也就说明这个Entry此时是没有用的了。当没有触发删除entry的逻辑,entry就会一直存在内存中。
但是,ThreadLocal在设计的时候,已经想到了这些问题,所以在操作ThreadLocal的时候都会对key为null的Entry进行删除,防止内存泄漏。除此之外,ThreadLocal还提供了remove方法,手动删除此key对应的Entry。源码如下:
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;
}
}
}
如果不使用线程池,那么不需要关系ThreadLoca的内存泄漏问题,因为线程执行完,整个线程都被回收了。但是我们一般使用的都是线程池,Tomcat在处理每个请求时都会从线程池中拿一个线程,但是使用完之后并不会回收,而是放回线程池。所以线程的独享变量threadLocalMap就需要手动清除,不然也可能发生数据错乱问题。
最佳实践
public void threadLocalToDo() {
threadlocal.set(xxx);
try {
// do sth
} finally {
threadlocal.remove();
}
}
只要在finally
中手动remove()
,那么肯定不会发生任何问题。
InheritableThreadLocal
这玩意可以理解为就是可以把父线程的 threadlocal 传递给子线程,所以如果要这样传递就用 InheritableThreadLocal ,不要用 threadlocal。
父线程在创建子线程是会判断InheritableThreadLocal 是不是null,不是就会把InheritableThreadLocal复制一份给子线程。