1.ThreadLocal
1.1 TheadLocal介绍和用途
ThreadLocal是一个可以提供线程局部变量的一个类,每个线程包含变量的副本,所有线程的副本的修改都是互相独立的,互不影响,通常使用场景是用户登录态相关的内容。
1.2 原理介绍
首先我们看下数据结构简介图:
Thread类中包含ThreadLocalMap对象,ThreadLocalMap是ThreadLocal的静态内部类,ThreadLocalMap中的对象都是以ThreadLocal对象作为key存储对应的value。
从上面的结构图,我们已经窥见ThreadLocal的核心机制:
- 每个Thread线程内部都有一个ThreadLocalMap。
- Map里面存储线程本地对象(key)和线程的变量副本(value)
- 但是,Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程的变量值。
这里我们以ThreadLocal的set方法来分析其原理,get方法与其类似:
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方法首先获取当前线程,然后获取当前线程的ThreadLocalMap对象,接着以ThreadLocal对象为key,传入的value值作为Map的value进行存储。因为每个线程都拥有自己的ThreadLocalMap对象,所以ThreadLocal的set方法设置的值并不影响其他线程,简单来说就是ThreadLocal对象是线程的本地变量。
1.3 内存泄漏问题
1.3.1 弱引用
/**
* 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;
}
}
从源码里面我们可以看到ThreadLocal的key使用的是弱引用,这里使用弱引用是为了防止内存泄漏。下图是ThreadLocal的整个引用链关系,key使用弱引用可以保证ThreadLocal对象在不再使用的情况下可以被正常回收,如果ThreadLocal对象没有使用,则说明其他对象没有引用此对象,此时只有ThreadLocalMap的Entry对象对其有引用,因为是弱引用,所以在GC的时候此对象会被回收,如果是强引用,则ThreadLocal对象将无法被回收。
1.3.2 正确调用remove方法
即便是ThreadLocalMap的key使用的是弱引用,可以被正常清理,但是ThreadLocal对象不再使用时,value。仍然无法被清理,仍然存在内存泄漏的问题。这时会有人问,那直接将value变成弱引用不就可以了吗,这样的话会导致value被错误回收,因为value除了被Entry引用之外没有其他的对象引用,因此会造成误清理,而key本身就是ThreadLocal对象,所以不会存在这个问题
那么我们如果解决内存泄漏的问题吗,这里Java已经给我们考虑好,提供了remove方法将Entry对象清理掉,在我们使用完ThreadLocal对象后调用这个方法可以解决这个问题,使用线程池的时候特别需要关注,因为线程是公用的,所以如果不清理的话会造成ThreadLocal变量线程不安全。
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) {
// 这实际调用的是Reference类的clear方法
e.clear();
expungeStaleEntry(i);
return;
}
}
}
/**
* Clears this reference object. Invoking this method will not cause this
* object to be enqueued.
*
* <p> This method is invoked only by Java code; when the garbage collector
* clears references it does so directly, without invoking this method.
*/
public void clear() {
this.referent = null;
}
从源码中我们可以发现调用clear方法后,entry对象整个都会被回收。
2.FastThreadLocal
1.1 FastThreadLocal介绍
整个是netty框架的一个类,主要配合FastThreadLocalThread使用,保存线程的本地变量,作用和ThreadLocal类似,其性能更高,而且不需要关系内存泄漏的问题
1.2 原理介绍
1.2.1 get方法
下面我们来看下这个的原理,我们将探讨为什么它比Java自带的ThreadLocal的性能更高
上图是FastThreadLocal的大概的数据结构,我们发现实际存储value的结构变成了一个数组,其查询的时间变成o(1),而ThreadLocal的是map,其查询时间为o(m),因此在数据量大的情况下,FastThreadLocal的性能更高,但是其所需的空间更大,典型的***拿空间换时间***。下面我们以其get方法为例来说明其原理。
从下面的代码可以看出这个get方法包含三个步骤:
- 获取当前线程的InternalThreadLocalMap
- 从Map中获取value
- 如果value不是默认值(new Object())),则直接返回对象
- 否则返回初始化对象
/**
* Returns the current value for the current thread
*/
public final V get() {
// 1.获取当前线程的InternalThreadLocalMap
InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.get();
// 2.根据index从Map中获取value
Object v = threadLocalMap.indexedVariable(index);
// 3.如果value不是默认值(new Object())),则直接返回对象
if (v != InternalThreadLocalMap.UNSET) {
return (V) v;
}
// 4.否则返回初始化对象
return initialize(threadLocalMap);
}
首先我们开看下get方法步骤1:获取当前线程的InternalThreadLocalMap,
public static InternalThreadLocalMap get() {
// 1.获取当前线程
Thread thread = Thread.currentThread();
// 2.判断当前线程是否FastThreadLocalThread,如果是则调用fastGet方法
if (thread instanceof FastThreadLocalThread) {
return fastGet((FastThreadLocalThread) thread);
} else {
// 3.否则调用slowGet方法
return slowGet();
}
}
private static InternalThreadLocalMap fastGet(FastThreadLocalThread thread) {
InternalThreadLocalMap threadLocalMap = thread.threadLocalMap();
if (threadLocalMap == null) {
thread.setThreadLocalMap(threadLocalMap = new InternalThreadLocalMap());
}
return threadLocalMap;
}
private static InternalThreadLocalMap slowGet() {
// 1.获取InternalThreadLocalMap类型的ThreadLocal对象,这里的UnpaddedInternalThreadLocalMap是InternalThreadLocalMap
// 的父类,在执行父类的构造方法的时候会创建一个初始化slowThreadLocalMap对象
ThreadLocal<InternalThreadLocalMap> slowThreadLocalMap = UnpaddedInternalThreadLocalMap.slowThreadLocalMap;
// 2.获取ThreadLocal对象中的InternalThreadLocalMap对象
InternalThreadLocalMap ret = slowThreadLocalMap.get();
// 如果为null,则直接创建新的InternalThreadLocalMap对象,并给ThreadLocal设置value值
if (ret == null) {
ret = new InternalThreadLocalMap();
slowThreadLocalMap.set(ret);
}
return ret;
}
这是InternalThreadLocalMap的一个方法,这个方法其实适配了Java的Thread线程,如果在非FastThreadLocalThread线程中创建FastThreadLocal对象,则也可以使用。主要包含三个步骤:
- 获取当前线程
- 判断当前线程是否FastThreadLocalThread,如果是则调用fastGet方法,实际上我们看到fastGet方法就是返回当前线程的InternalThreadLocalMap对象
- 否则调用slowGet方法,这个用到ThreadLocal对象进行曲线救国,返回当前线程的InternalThreadLocalMap对象
接着我们看下get方法的步骤2:从Map中获取value,
public Object indexedVariable(int index) {
// 1.获取所有的变量的数组
Object[] lookup = indexedVariables;
// 根据下标从数据中获取数据
return index < lookup.length? lookup[index] : UNSET;
}
这个方法是InternalThreadLocalMap中的一个方法,我最开始在看这个方法的时候感觉有点蒙蔽,这个index是如何计算的呢?我们可以发现index是从InternalThreadLocal类中传入的,并且传入的是其属性,那么我们接下来看下这个index是什么时候被赋值的。
private final int index;
public FastThreadLocal() {
index = InternalThreadLocalMap.nextVariableIndex();
}
从下面的代码中我们可以发现index是一个final变量,是在构造函数中被初始化的,调用的是InternalThreadLocal的nextVariableIndex方法。
static final AtomicInteger nextIndex = new AtomicInteger();
public static int nextVariableIndex() {
int index = nextIndex.getAndIncrement();
if (index < 0) {
nextIndex.decrementAndGet();
throw new IllegalStateException("too many thread-local indexed variables");
}
return index;
}
我们可以发现每次调用nextVariableIndex方法时nextIndex都会自增,这样我们就明白其原理了,每次我们创建一个InternalThreadLocalMap对象时InternalThreadLocal都会为其按照顺序分配一个index,这个index对于每个InternalThreadLocal都是不变的,get时直接根据下标从数组中获取数据,set时根据下标对数组设置数据。
这里其实有个比较有意思的东西,实际上对于FastThreadLocalThread来说nextIndex完全没有必要设置为AtomicInteger,这里这样设置是为了保证在普通线程里面使用FastThreadLocal时保证线程安全,对于FastThreadLocalThread来说始终都是一个线程,但是对于普通的Thread来说,可能在当前线程里面使用多线程进行多个FastThreadLocal的创建,这样就会出现线程安全问题。
好了,扛不住了,明天继续分析后续的代码吧,睡觉。
1.2.2 set方法
set方法的大部分原理和get方法类似,但是也有些许不同,这里主要讲解set方法的一些特殊处理。
/**
* Set the value for the current thread.
*/
public final void set(V value) {
if (value != InternalThreadLocalMap.UNSET) {
// 这个方法和set类似,这里不再说明
InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.get();
setKnownNotUnset(threadLocalMap, value);
} else {
// 如果放入的是空对象,则直接清理掉
remove();
}
}
我们发现开始的获取InternalThreadLocalMap方法和get一样,这里主要说明setKnownNotUnset方法,remove方法会在后面的removeAll方法进行详细讲解。
/**
* @return see {@link InternalThreadLocalMap#setIndexedVariable(int, Object)}.
*/
private void setKnownNotUnset(InternalThreadLocalMap threadLocalMap, V value) {
// 判断是否当前ftl对象需要添加到set集合中,并设置设置当前value值
if (threadLocalMap.setIndexedVariable(index, value)) {
// 如果需要,则将其添加到需要清理的set集合中
addToVariablesToRemove(threadLocalMap, this);
}
}
/**
* @return {@code true} if and only if a new thread-local variable has been created
* 当且仅当ftl对象是新创建的时候返回true,这里可以理解为会将每次创建的ftl对象添加到清理的set集合中,这个集合永远处于fslm中的0槽位
* 通过这个set,我们可以快速清理整个ftl对象,而不需要遍历fslm的数组
*/
public boolean setIndexedVariable(int index, Object value) {
Object[] lookup = indexedVariables;
if (index < lookup.length) {
Object oldValue = lookup[index];
lookup[index] = value;
return oldValue == UNSET;
} else {
expandIndexedVariableTableAndSet(index, value);
return true;
}
}
private static void addToVariablesToRemove(InternalThreadLocalMap threadLocalMap, FastThreadLocal<?> variable) {
// 从ftlm中获取需要清理的ftl类型的set对象
// private static final int variablesToRemoveIndex = InternalThreadLocalMap.nextVariableIndex(); 实际上variablesToRemoveIndex是ftlm的一个槽位,用来专门存储需要清理的ftl
Object v = threadLocalMap.indexedVariable(variablesToRemoveIndex);
Set<FastThreadLocal<?>> variablesToRemove;
// 如果此对象为空,则重新创建一个set集合用来存放需要清理的ftl对象
if (v == InternalThreadLocalMap.UNSET || v == null) {
variablesToRemove = Collections.newSetFromMap(new IdentityHashMap<FastThreadLocal<?>, Boolean>());
threadLocalMap.setIndexedVariable(variablesToRemoveIndex, variablesToRemove);
} else {
// 否则进行强转
variablesToRemove = (Set<FastThreadLocal<?>>) v;
}
// 将需要清理的对象添加到set集合中
variablesToRemove.add(variable);
}
我们发现setKnownNotUnset很简单,只有两个步骤:
- 判断是否当前ftl对象需要添加到set集合中,并设置设置当前value值
- 如果需要,则将其添加到需要清理的set集合中
我们首先看下步骤1——setIndexedVariable方法:
这个方法主要分为两种情况:
情况一:不需要扩容,则直接将value放入Object数组中即可,然后根据当前index原来的值是否是UNSET来判断此次操作是否是新增
情况二:需要扩容,则进行扩容,每次扩容都是2倍扩容,这个和HashMap的扩容方法类似,然后返回true(需要扩容表明一定是新增)
所以此方法的返回值也是耐人寻味的:当且仅当ftl对象是新创建的时候返回true,这里为什么需要这个操作,刚开始的时候一直很懵逼,后来突然想明白了,其实很简单,ftlm想将所有的ftl对象保存到一起,这样在清理ftl对象的时候可以快速清理掉所有对象,判断是新建的原因为不想多次添加重复值,提高效率。
最后我们来看下addToVariablesToRemove方法,将ftl对象添加到需要清理的对象里面,主要分为以下步骤:
- 从ftlm中获取需要清理的ftl类型的set对象
- private static final int variablesToRemoveIndex = InternalThreadLocalMap.nextVariableIndex(); 实际上variablesToRemoveIndex是ftlm的一个槽位,用来专门存储需要清理的ftl,这里我们可以看到一个变量variablesToRemoveIndex,我们发现是一个static类型的常量,这个变量最终会初始化为0(InternalThreadLocalMap.nextVariableIndex()方法在get方法里面有讲解,这里不再赘述),所以实际上我们可以确定保存所有ftl对象的set集合实际上是***放在ftlm的0槽位***的,这个是永远不会变化的。
- 如果此对象为空,则重新创建一个set集合用来存放需要清理的ftl对象
- 将需要清理的对象添加到set集合中
1.2.3 removeAll方法
我们知道,jdk自带的ThreadLocal对象存在内存泄漏的问题,需要手动进行清理,而ftl则不需要,下面我们来看下为什么ftl不需要手动清理。
final class FastThreadLocalRunnable implements Runnable {
private final Runnable runnable;
private FastThreadLocalRunnable(Runnable runnable) {
this.runnable = ObjectUtil.checkNotNull(runnable, "runnable");
}
@Override
public void run() {
try {
runnable.run();
} finally {
FastThreadLocal.removeAll();
}
}
static Runnable wrap(Runnable runnable) {
return runnable instanceof FastThreadLocalRunnable ? runnable : new FastThreadLocalRunnable(runnable);
}
}
上面的代码是netty的FastThreadLocalRunnable,netty的线程池创建的就是这种线程,我们发现其run方法在finally块中统一执行了FastThreadLocal.removeAll()方法,这样在每次线程执行结束的时候都会清理掉ftl对象,不会造成内存溢出的问题。因为ftl也可以用于普通线程,所以在普通线程中使用的时候我们记得一定要执行removeAll方法,否则会造成内存溢出问题。
/**
* Removes all {@link FastThreadLocal} variables bound to the current thread. This operation is useful when you
* are in a container environment, and you don't want to leave the thread local variables in the threads you do not
* manage.
*/
public static void removeAll() {
// 获取当前线程的ftlm对象
InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.getIfSet();
if (threadLocalMap == null) {
return;
}
try {
// 从ftlm中获取保存所有ftl对象的set
Object v = threadLocalMap.indexedVariable(variablesToRemoveIndex);
if (v != null && v != InternalThreadLocalMap.UNSET) {
@SuppressWarnings("unchecked")
Set<FastThreadLocal<?>> variablesToRemove = (Set<FastThreadLocal<?>>) v;
FastThreadLocal<?>[] variablesToRemoveArray =
variablesToRemove.toArray(new FastThreadLocal[0]);
// 遍历set集合,执行remove方法清理ftl对象
for (FastThreadLocal<?> tlv: variablesToRemoveArray) {
tlv.remove(threadLocalMap);
}
}
} finally {
// 最后将ftlm清理掉
InternalThreadLocalMap.remove();
}
}
public static InternalThreadLocalMap getIfSet() {
Thread thread = Thread.currentThread();
if (thread instanceof FastThreadLocalThread) {
return ((FastThreadLocalThread) thread).threadLocalMap();
}
return slowThreadLocalMap.get();
}
下面我们来分析removeAll方法:
- 获取当前线程的ftlm对象,getIfSet这个方法和前面的InternalThreadLocalMap.get()方法一样,这里不再赘述
- 从ftlm中获取保存所有ftl对象的set
- 遍历set集合,执行remove方法清理ftl对象
- 最后将ftlm清理掉
我们先来看下ftl的remove方法:
/**
* Sets the value to uninitialized for the specified thread local map;
* a proceeding call to get() will trigger a call to initialValue().
* The specified thread local map must be for the current thread.
*/
@SuppressWarnings("unchecked")
public final void remove(InternalThreadLocalMap threadLocalMap) {
if (threadLocalMap == null) {
return;
}
// 获取当前ftl对象的value值,并从ftlm的Object数据中将此对象删除
Object v = threadLocalMap.removeIndexedVariable(index);
removeFromVariablesToRemove(threadLocalMap, this);
// 如果当前value不是空对象,则执行ftl的onRemoval回调方法
if (v != InternalThreadLocalMap.UNSET) {
try {
onRemoval((V) v);
} catch (Exception e) {
PlatformDependent.throwException(e);
}
}
}
主要包含以下步骤:
- 获取当前ftl对象的value值,并从ftlm的Object数据中将此对象删除
- 如果当前value不是空对象,则执行ftl的onRemoval回调方法
我们看下removeFromVariablesToRemove方法,我们发现这个方法主要就是从set集合删除ftl对象。
private static void removeFromVariablesToRemove(
InternalThreadLocalMap threadLocalMap, FastThreadLocal<?> variable) {
Object v = threadLocalMap.indexedVariable(variablesToRemoveIndex);
if (v == InternalThreadLocalMap.UNSET || v == null) {
return;
}
@SuppressWarnings("unchecked")
Set<FastThreadLocal<?>> variablesToRemove = (Set<FastThreadLocal<?>>) v;
variablesToRemove.remove(variable);
}
最后是执行onRemoval回调方法,这个方法在ftl中是protected方法,由使用者重写此方法进行回调通知。
好了,以上就是整个ThreadLocal和FastThreadLocal的一个对比,花了挺长时间的,这个是第一个自己读源码并手写的笔记,花了大概一周的时间,有点惭愧,希望这是一个好的开头吧。