这是我参与2022首次更文挑战的第2天,活动详情查看:2022首次更文挑战
前言
- ThreadLocal的使用场景在实际项目中是比较多的一个类,其主要能解决数据在线程间共享问题。也是后端开发中比较基础且必须掌握的一个知识点。
- 线程间数据共享与不共享需要根据实际场景。
- 比如场景要求线程间交替打印某某或累计计数等,就需要共享数据;
- 比如传入线程内的用户信息等,就要避免线程共享;
- 网上其实也有非常多关于ThreadLocal的文章,这篇文章结合自己对其的理解把大致原理输出一下。
示例
- ThreadLocal的用法比较简单,下面是一个简单的示例demo。
- 在demo中创建了三个线程,每个线程都是对同一个ThreadLocal对象进行操作,但最后输出打印结果却是各自的线程名称,说明其具有线程隔离效果。
/**
* @Author: ZRH
*/
@Slf4j
public class ThreadLocalDemo {
private static ThreadLocal<String> local = ThreadLocal.withInitial(() -> Thread.currentThread().getName());
private static ThreadPoolExecutor executor = new ThreadPoolExecutor(3, 3, 30, TimeUnit.SECONDS, new LinkedBlockingQueue<>(5));
public static void main (String[] args) {
for (int i = 0; i <= 2; i++) {
executor.execute(() -> {
log.info("本次打印的线程名称:{}", local.get());
});
}
}
}
--------------------------------------打印结果
10:24:10.357 [pool-1-thread-2] INFO com.redisson.web.demo.ThreadLocalDemo - 本次打印的线程名称:pool-1-thread-2
10:24:10.356 [pool-1-thread-3] INFO com.redisson.web.demo.ThreadLocalDemo - 本次打印的线程名称:pool-1-thread-3
10:24:10.357 [pool-1-thread-1] INFO com.redisson.web.demo.ThreadLocalDemo - 本次打印的线程名称:pool-1-thread-1
复制代码
源码解析
- ThreadLocal线程隔离是通过为每个线程都复制一份变量数据,所以每个线程都是操作自己本地的变量。
- ThreadLocal可以通过普通的构造函数进行创建实例,也可以通过静态方法withInitial(...)创建实例。
- SuppliedThreadLocal是ThreadLocal的一个扩展的内部类,用于设定指定的初始值。
/**
* Creates a thread local variable. The initial value of the variable is
* determined by invoking the {@code get} method on the {@code Supplier}.
*
* @param <S> the type of the thread local's value
* @param supplier the supplier to be used to determine the initial value
* @return a new thread local variable
* @throws NullPointerException if the specified supplier is null
* @since 1.8
*/
public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
return new SuppliedThreadLocal<>(supplier);
}
/**
* Creates a thread local variable.
* @see #withInitial(java.util.function.Supplier)
*/
public ThreadLocal() {
}
复制代码
- 在Thread类中有ThreadLocal成员属性变量,初始值为null。
- ThreadLocalMap是ThreadLocal的内部类,里面有个entry[]数组,在其构造函数中传入的k最后会被WeakReference所引用,所以这里以ThreadLocal实例为key才是弱引用,value值是强引用。
- 在ThreadLocalMap构造函数中,会先创建一个大小为16的entry[]数组,然后通过firstKey计算出一个hash下标,并把value所在的entry对象加入数组,并设置下次扩容限制大小。
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;
......
/**
* 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);
}
......
}
复制代码
- 通过源码了解是通过ThreadLocal.set(...)方法向ThreadLocalMap中添加元素,其执行流程:
- 先获取当前线程对象Thread,然后通过当前线程获取ThreadLocalMap集合
- 如果ThreadLocalMap不为null,就通过ThreadLocal实例为key和value添加到ThreadLocalMap集合中
- 如果ThreadLocalMap为null,就new ThreadLocalMap()对象并以ThreadLocal实例为key和value加入到集合中
/**
* Sets the current thread's copy of this thread-local variable
* to the specified value. Most subclasses will have no need to
* override this method, relying solely on the {@link #initialValue}
* method to set the values of thread-locals.
*
* @param value the value to be stored in the current thread's copy of
* this thread-local.
*/
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
......
/**
* Create the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @param t the current thread
* @param firstValue value for the initial entry of the map
*/
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
复制代码
- 在上述的ThreadLocalMap对象结构和HashMap有的类似,但是在其set方法中有区别,最主要的是其发生hash冲突后是通过开放寻址法来解决,而HashMap是通过链表+红黑树解决。
- 首先计算key对应的数组下标,
- 然后判断数组内的key和当前插入的key是否相同,
- 如果相等就替换value,然后返回。
- 如果不相等,就调用replaceStaleEntry(...)方法清除数组内无用key的entry元素,
- 然后执行nextIndex(...)方法得到下标+1位置的数组元素,再次进入循环操作,
- 最后如果都没有匹配到,就清除一些无用Entry解决,然后在判断是否需要再次进行扩容。
/**
* Set the value associated with key.
*
* @param key the thread local object
* @param value the value to be 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();
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();
}
复制代码
- 元素插入ThreadLocal后肯定是还要获取的,通过ThreadLocal.get(...)方法向ThreadLocalMap中获取元素,其执行流程:
- 先获取当前线程Thread对象,然后在获取ThreadLocalMap对象,
- 然后通过getEntry(...)方法,以当前ThreadLocal实例为参数获取Entry元素,
- 如果获取Entry成功,就直接返回,否则执行setInitialValue()方法。
......
/**
* Returns the value in the current thread's copy of this
* thread-local variable. If the variable has no value for the
* current thread, it is first initialized to the value returned
* by an invocation of the {@link #initialValue} method.
*
* @return the current thread's value of this thread-local
*/
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中getEntry(...)方法中:
- 首先根据传进来的ThreadLocal实例参数为key,并计算出一个数组下标,
- 根据下标获取数组内Entry元素,如果元素和本次key相同就直接返回。
- 否则进入getEntryAfterMiss(...)方法继续查找:
- 先判断传入的entry是否是本次key相同,
- 如果不相同就执行expungeStaleEntry(...)方法,用于清除数组内key为null的entry元素,
- 然后执行nextIndex(...)方法得到数组中下标+1的元素,再次遍历循环
- 最后如果还是没有找到,就返回null。
......
/**
* Get the entry associated with key. This method
* itself handles only the fast path: a direct hit of existing
* key. It otherwise relays to getEntryAfterMiss. This is
* designed to maximize performance for direct hits, in part
* by making this method readily inlinable.
*
* @param key the thread local object
* @return the entry associated with key, or null if no such
*/
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);
}
/**
* Version of getEntry method for use when key is not found in
* its direct hash slot.
*
* @param key the thread local object
* @param i the table index for key's hash code
* @param e the entry at table[i]
* @return the entry associated with key, or null if no such
*/
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;
}
复制代码
内存泄漏原因
- 在上述的分析中,可以发现ThreadLocalMap中entry对象内的key(ThreadLocal实例)是弱引用,那就其存活时机不会超过一个GC。
- 所以当外部没有对当前ThreadLocal实例有强引用时,那就让其被JVM系统回收清理掉。
- 为啥这样设计,网上解释也有很多,读者可以自行了解,这里按下不表。
- 在实际项目中,一般会使用线程池方式创建线程执行任务,例如Tomact线程池。当弱引用的key被回收掉了后,其实对应entry也应该被回收掉,但因为线程复用的原因,使Thread->ThreadLocalMap->Entry链路继续存在,导致无用的entry一直回收不了,所以会发生内存泄漏。
- 其实在ThreadLocal源码中有很多针对entry回收的处理,比如ThreadLocalMap.remove(...),ThreadLocalMap.getEntryAfterMiss(...)和ThreadLocalMap.set(...)等方法中都有清除无用entry操作。
- 但上述还是无法避免可能会出现的内存泄漏,所以这里提一下ThreadLocal使用会造成内存泄漏的场景:
- 在实际项目中,使用线程池方式执行任务,就会有线程的复用情况。
- 在使用ThreadLocal对象时,没有手动remove(),也没有重新get()或者set()元素或触发扩容操作。
最后
- ThreadLocal除了上述会出现内存泄漏外,还有其它不足之处。就是在当前线程中创建子线程时无法把ThreadLocal中变量传递下去。
- 进阶:如果想实现父子线程传递变量,可以使用InheritableThreadLocal对象传递。
- 虚心学习,共同进步 -_-