源码分析:InheriableThreadLocal传递数据的原理和ThreadLocal导致的内存泄露原因
Posted talk.push
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了源码分析:InheriableThreadLocal传递数据的原理和ThreadLocal导致的内存泄露原因相关的知识,希望对你有一定的参考价值。
文章目录
ThreadLocal有什么问题?
挺巧的,去年今天写了这个demo,今天复盘的时候又想起了ThreadLocal。ThreadLocal可以为每一个线程保存一份数据,通常可以用来解决多线程环境下变量的竞争。但却无法做到父子线程这种上下文环境下变量的父子线程传递,也就是说仅仅靠ThreadLocal是无法做到在子线程中获取主线程中set的变量的。那么,InheritableThreadLocal可继承ThreadLocal就是来解决这个问题的。
package com.jeff.study.concurrent.lock;
/**
* @author jeffSmile
* @date 2020-04-28 上午 11:30
* @desc
*/
public class TestThreadLocal
// public static ThreadLocal<String> threadLocal = new ThreadLocal<String>();
public static ThreadLocal<String> threadLocal = new InheritableThreadLocal<>();
public static void main(String[] args)
//主线程必须在子线程创建之前set共享变量,否则子线程还是访问不到
threadLocal.set("hello world");
Thread thread = new Thread(new Runnable()
@Override
public void run()
System.out.println("thread:" + threadLocal.get());
);
// threadLocal.set("hello world");
thread.start();
System.out.println("main:" + threadLocal.get());
InheriableThreadLocal如何复制数据到子线程?
搞清楚了问题,我们接下来就看看InheriableThreadLocal是如何把主线程中set进去的数据,复制到子线程中的。这样从Thread的构造器说起,这里搬出源码,只保留重要的部分。
public Thread(Runnable target)
init(null, target, "Thread-" + nextThreadNum(), 0);
private void init(ThreadGroup g, Runnable target, String name,
long stackSize)
init(g, target, name, stackSize, null, true);
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals)
if (name == null)
throw new NullPointerException("name cannot be null");
this.name = name;
//创建线程的父线程,这里就是主线程
Thread parent = currentThread();
.......
......
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
/* Stash the specified stack size in case the VM cares */
this.stackSize = stackSize;
/* Set thread ID */
tid = nextThreadID();
判断主线程parent的inheritableThreadLocals变量是否为空,这是一个Map变量。如果不为空则复制这个Map给子线程的相应变量。
//Thread.java
/*
* InheritableThreadLocal values pertaining to this thread. This map is
* maintained by the InheritableThreadLocal class.
*/
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
这个变量的赋值是由主线程在执行InheritableThreadLocal.set(data);时设置进去的。第一次set时当前Thread(主线程)的threadLocals变量为空,所以执行createMap方法。
public void set(T value)
Thread t = Thread.currentThread();
//第一次时thread的threadLocals这个map为空
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
由于我们使用的InheriableThreadLocal,所以执行的createMap也是InheriableThreadLocal的所实现方法。InheriableThreadLocal中直接创建了一个ThreadLocalMap赋值给了成员变量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);
所以,当我们看到Thread创建时的如下代码时,知道子线程会复制一份主线程的this.inheritableThreadLocals变量。
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
子线程复制父线程变量细节剖析
接下来就分析下复制的原理:
this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
createInheritedMap是ThreadLocal中的静态方法,这个方法很简单直接创建一个ThreadLocalMap返回了,参数就是父Thread的内部变量inheritableThreadLocals。
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap)
return new ThreadLocalMap(parentMap);
那么,数据传递的机制一定在ThreadLcoalMap的构造器里了。这里就是把parent线程ThreadLocalMap中的Entry[]数组拿出来遍历,组装成新的Entry再放进child线程的ThreadLocalMap的Entry[]数组。
private ThreadLocalMap(ThreadLocalMap parentMap)
Entry[] parentTable = parentMap.table;
int len = parentTable.length;
setThreshold(len);
table = new Entry[len];
for (int j = 0; j < len; j++)
Entry e = parentTable[j];
if (e != null)
@SuppressWarnings("unchecked")
ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
if (key != null)
Object value = key.childValue(e.value);
Entry c = new Entry(key, value);
int h = key.threadLocalHashCode & (len - 1);
while (table[h] != null)
h = nextIndex(h, len);
table[h] = c;
size++;
这样就实现了数据的复制。这样以来在子线程中使用threadlcoal.get()方法就可以从child线程自己的ThreadLocalMap中拿到数据了。
ThreadLocalMap是个什么鬼?
//ThreadLocal.java
/**
* ThreadLocalMap is a customized hash map suitable only for
* maintaining thread local values. No operations are exported
* outside of the ThreadLocal class. The class is package private to
* allow declaration of fields in class Thread. To help deal with
* very large and long-lived usages, the hash table entries use
* WeakReferences for keys. However, since reference queues are not
* used, stale entries are guaranteed to be removed only when
* the table starts running out of space.
*/
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.
*/
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
首先,ThreadLocalMap是ThreadLocal的一个静态内部类。看下这个静态内部类头顶的注释:
/**
* ThreadLocalMap is a customized hash map suitable only for
* maintaining thread local values. No operations are exported
* outside of the ThreadLocal class. The class is package private to
* allow declaration of fields in class Thread. To help deal with
* very large and long-lived usages, the hash table entries use
* WeakReferences for keys. However, since reference queues are not
* used, stale entries are guaranteed to be removed only when
* the table starts running out of space.
*
* ThreadLocalMap是自定义的哈希映射,仅适用于维护线程局部值。
* 没有操作导出到ThreadLocal类之外。
* 该类是包私有的,以允许在Thread类中的声明字段。
* 为了帮助处理非常长的使用寿命,哈希表条目使用WeakReferences作为键。
* 但是,由于不使用参考队列,因此仅在表空间不足时,才保证删除过时的条目。
*/
在ThreadLocalMap中定义了Entry静态内部类,这个类继承了WeakReference<ThreadLocal<?>>。看下关于这个Entry的注释,也就是说Entry中利用父类WeakReference的referent成员变量来保存key(threadLcoal)。
//ThreadLocalMap.java
/**
* 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.
*
* 此哈希映射中的条目使用其主引用字段作为键(始终是ThreadLocal对象)
* 扩展了WeakReference。
* 注意,空键(即entry.get()== null)意味着不再引用该键,
* 因此可以从表中删除该条目。 在下面的代码中,此类条目称为“陈旧条目”
*/
static class Entry extends WeakReference<ThreadLocal<?>>
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v)
super(k);
value = v;
说白了,threadLcoal在ThreadLocalMap中就是一个WeakReference内部维护的弱引用。弱点引用变量只能生存到下一次GC之前,无论内存是否充足都会被回收掉。
只所以设计成弱引用,是为了应对线程长期存活导致threadLocal因为强引用导致无法回收的情况,比如在线程池中线程是长期存活的,如果这个线程中长期对ThreadLocal强引用,那么将可能导致内存泄露。
但是ThreadLocal虽然设计为弱引用,但还是可能导致内存泄露!
这是因为尽管ThreadLocal作为弱引用被下一次GC回收而变为null,但是value却是强引用的,那么在ThreadLocalMap中对应的Entry是无效的,但却不会释放内存!除非你主动调用ThreadLocal#remove方法.在这个方法中会对key==null的Entry进行回收。
/**
* Remove the entry for key.
*/
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;
/**
* Expunge a stale entry by rehashing any possibly colliding entries
* lying between staleSlot and the next null slot. This also expunges
* any other stale entries encountered before the trailing null. See
* Knuth, Section 6.4
*
* @param staleSlot index of slot known to have null key
* @return the index of the next null slot after staleSlot
* (all between staleSlot and this slot will have been checked
* for expunging).
*/
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();
//清除无效Entry
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;
以上是关于源码分析:InheriableThreadLocal传递数据的原理和ThreadLocal导致的内存泄露原因的主要内容,如果未能解决你的问题,请参考以下文章