提出问题:
- ThreadLocal 是如何和线程绑定的,ThreadLocal 的存储结构,如何解决hash冲突的
- ThreadLocal是否会导致内存泄漏,为啥key要使用WeakReference
- 在一个父线程开启多个子线程的情况下如何使用ThreadLocal
下面我们一个一个问题来
1. ThreadLocal 的存储结构,如何解决hash冲突的

ThreadLocal 是如何和线程绑定的:
上面的图简单描述他们的调用关系,简单来说就是当我们用ThreadLocal设置值得时候,
- 会先获取当前线程,然后取得线程里面的
ThreadLocalMap,没有就重新建一个, ThreadLocalMap和HashMap差不多。里面有个Entry[] table,Entry的key值是ThreadLocal本身,value值是要设置的值。
通过ThreadLocal获取值得时候,也是先获取当前线程,然后取得线程里面的ThreadLocalMap。所以每个线程都使用的是自己线程里面的ThreadLocalMap, 同一个线程new的多个ThreadLocal只会创建一个ThreadLocalMap, 实现了线程隔离。get, 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);
}
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 getMap(Thread t) {
return t.threadLocals;
}
ThreadLocal 的存储结构
- 一个
ThreadLocal包含一个ThreadLocalMap, - 一个ThreadLocalMap包含一个Entry[],
- Entry包含一个key值一个value值,key值就是ThreadLocal本生,value值为设置的值,
同一个线程如果new了多个ThreadLocal,但是只会创建一个ThreadLocalMap,所有的值都保存在同一个Entry[], 这时候就可能存在hash冲突,hash冲突简单的解释下就是两个不同的key值通过hash计算后获得的值是一样的
如何解决hash冲突?
线性探测法 : 讲的通俗一点,就是发现蹲坑的时候发现坑已经被占了,就找后面一个坑,如果后面一个坑空闲,则占用这个空闲的坑;如果后面一个坑也被占了,则一直往后面的坑进行遍历,直到找到空闲的坑,否则就一直憋着。 查找的时候,也是先通过hash计算出位置,然后判断key是否相等,相等就获取value值,不相等就查找后面一位,直到找到相等的。 ThreadLocalMap的set代码如下
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;
int i = key.threadLocalHashCode & (len-1);
//循环查找直到找到为空的位置
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
//如果key值相等就更新
if (k == key) {
e.value = value;
return;
}
//k为null,也就是Entry的key已经被回收了,当前的Entry是一个陈旧的元素(stale entry)
if (k == null) {
// 用新元素替换掉陈旧元素,同时也会清理其他陈旧元素,防止内存泄露
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
//清理key值为null的元素,同时判断是否需要扩容
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
2. ThreadLocal是否会导致内存泄漏,为啥key要使用WeakReference
内存泄漏是啥意思, 简单来栈内存里面的引用被清楚了,但是堆里面的数据无法回收,但是程序里面里面又用不了这个对象,就叫内存泄漏,大量的内存泄漏会导致内存溢出。
我们看下Entry的代码
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
ThreadLocal<?>也就是key值使用了弱引用,弱引用简单来说就是垃圾回收器发现一个对象上只有弱引用,此时不管内存够不够,这个对象都会被回收。
Entry 为啥要使用WeakReference?
我的理解:
- 如果我们不需要使用这个ThreadLocal,
- 然后将ThreadLocal设置为null,栈和对内存的ThreadLocal 被清除,
- 但是多个ThreadLocal同用一个的ThreadLocalMap并不会被清除,如果都是强引用,ThreadLocalMap里面这个被清除的ThreadLocal的Entry也无法被清除掉。
但是如果Entry弱引用了ThreadLocal,如果ThreadLocal被清除掉,这个弱引用的ThreadLocal就没有任何强引用,key值也将被垃圾回收器回收为null, 然后ThreadLocalMap在get,set, remove方法中会调用cleanSomeSlots(i, sz)来清理掉key值为null的元素。
// expunge entry at staleSlot
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
结论:
ThreadLocal大概率是不会产生内存泄漏的,除非你将一个ThreadLocal设置为null后,在也没有调用ThreadLocalMap里面的get,set, remove, 那么这个ThreadLocal里面的Entry的value值将无法回收,但是为了保险起见还是建议不用这个value值之后使用ThreadLocal.remove() 方法移除掉这个值。最好放在finally里面,如下
try {
ThreadLocalCache.TEST.set("XXXX");
........
} finally {
ThreadLocalCache.TEST.remove();
}
3. 在一个父线程开启多个子线程的情况下如何使用ThreadLocal
在一个父线程开启多个子线程的情况下如何使用ThreadLocal,子线程是获取不到父线程设置的值得,因为子线程会自己new一个ThreadLocalMap, 这个ThreadLocalMap里面并没有父线程ThreadLocalMap的值,基于这种情况,java提供了一个InheritableThreadLocal。InheritableThreadLocal的代码很简单
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
/**
* Computes the child's initial value for this inheritable thread-local
* variable as a function of the parent's value at the time the child
* thread is created. This method is called from within the parent
* thread before the child is started.
* <p>
* This method merely returns its input argument, and should be overridden
* if a different behavior is desired.
*
* @param parentValue the parent thread's value
* @return the child thread's initial value
*/
protected T childValue(T parentValue) {
return parentValue;
}
/**
* Get the map associated with a ThreadLocal.
*
* @param t the current thread
*/
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
/**
* Create the map associated with a ThreadLocal.
*
* @param t the current thread
* @param firstValue value for the initial entry of the table.
*/
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
}
它继承了ThreadLocal, 在getMap的时候返回了Thread里面的inheritableThreadLocals, 这个变量在线程里面的定义是使用如下, 简化了下不需要的代码,
public
class Thread implements Runnable {
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc) {
if (parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
}
}
简单来说就是在线程初始化的时候判断父类有没有inheritableThreadLocals, 如果有就把父类的inheritableThreadLocals复制给子类。获取值得时候就获取了有值得inheritableThreadLocals。
但是真实的项目中一般不会自己去new Thread,而是通过线程池来创建Thread,这个时候就涉及线程的复用,因为父线程的值传给子线程是在创建子线程的时候copy的,如果重用子线程,就会导致子类中的值不对,可能为空,可能为上个父线程的值,这个时候就要 使用alibaba提供的TransmittableThreadLocal
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
<version>2.2.0</version>
</dependency>
使用
public static final ThreadLocal<Integer> test = new TransmittableThreadLocal<>();
test.set(1111);
test.get();
//Executor需要用TtlExecutors包一下
public Executor executor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(50);
executor.setThreadNamePrefix("collectorExecutor-");
executor.initialize();
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
return TtlExecutors.getTtlExecutor(executor);
}
里面的具体实现逻辑,等我看懂了在来分享吧。
本文详细探讨了ThreadLocal的工作原理,包括其如何与线程绑定,存储结构以及解决哈希冲突的方法。文章指出,ThreadLocal通过ThreadLocalMap存储线程局部变量,使用弱引用避免内存泄漏,但在多线程环境下,直接使用ThreadLocal无法在父线程和子线程间传递值。为解决此问题,文章提到了InheritableThreadLocal和TransmittableThreadLocal的区别与应用。
4304

被折叠的 条评论
为什么被折叠?



