前言
作为面试中被问到最多的问题,该问题不仅仅涉及到线程相关问题,也是多线程和垃圾回收机制的一些问题的切入点,他为每个线程创建了一个只有该线程独立的ThreadLocalMap,可以实现线程隔离的操作。接下来的文章我会从基本原理解析和其衍生应用TransmittableThreadLocal,FastThreadLocal进行解析,有需要的小伙伴按导航阅读。
1.ThreadLocal
ThreadLocal顾名思义,将一个线程变为本地,也就是让一个对象变成一个线程自己持有的,只允许自己线程访问和修改。这究竟是怎么实现的,接下来我们一步一步解开他的神秘面纱。
先整体看一下ThreadLocal这个对象长啥样,直接贴代码;
看一下里面几个重点的属性
1.1 threadLocalHashCode
具体的参数定义见注释
//全局变量
private final int threadLocalHashCode = nextHashCode();
//hashCode原子类
private static AtomicInteger nextHashCode =
new AtomicInteger();
//魔数
private static final int HASH_INCREMENT = 0x61c88647;
// 计算hashcode方法
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
这里要说一下HASH_INCREMENT的取值原因:
0x61c88647对应的十进制为1640531527;
通过理论与实践,当我们用0x61c88647作为魔数累加为每个ThreadLocal分配各自的ID也就是threadLocalHashCode再与2的幂取模,得到的结果分布很均匀,ThreadLocalMap使用的是线性探测法,均匀分布的好处在于很快就能探测到下一个临近的可用slot,从而保证效率。
由于threadLocalHashCode是全局变量,而nextHashCode是静态方法,相当于每次创建ThreadLocal时都会有不一样的threadLocalHashCode
1.2 #SET()
使用懒加载的方式创建_ThreadLocalMap ,_这里有一点比较重要的是_ThreadLocalMap _现在属于当前线程对象Thread,若存在map则调用ThreadLocalMap的#set()方法,不存在则调用ThreadLocalMap的构造器创建赋值给当前线程。(具体的#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);
}
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
1.3 #get()
也是懒加载创建ThreadLocalMap,获取当前线程的ThreadLocalMap,若存在则调用#getEntry()方法(具体见下文),否则调用#setInitialValue()方法初始化ThreadLocalMap
#setInitialValue() 会初始化一个默认值,可以实现该接口来初始化默认值,其实现可以由内部类SuppliedThreadLocal ,这里继承了Supplier函数式接口,当创建ThreadLocal对象时可以使用该函数式接口传入默认值。
static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {
private final Supplier<? extends T> supplier;
SuppliedThreadLocal(Supplier<? extends T> supplier) {
this.supplier = Objects.requireNonNull(supplier);
}
@Override
protected T initialValue() {
return supplier.get();
}
}
其中Thread中的构造器支持该方法
public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
return new SuppliedThreadLocal<>(supplier);
}
public ThreadLocal() {
}
这样可以通过使用withInitial方法创建一个带有默认值的ThreadLocal
ThreadLocal<Object> threadLocal = ThreadLocal.withInitial(Object::new);
1.2 ThreadLocalMap
ThreadLocal的内部类,使用内部类_Entry_存储具体值,先上代码
//存储具体值得对象
private Entry[] table;
//table大小
private int size = 0;
//table初始化容量
private static final int INITIAL_CAPACITY = 16;
//扩容得阈值
private int threshold;
1.2.1 Entry
ThreadLocalMap的内部类,存储数据的基本对象。
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
K,V结构,K是ThreadLocal实例对象,V是存储的值,并且继承弱引用。注意此处会有一个高频考点:
Q:Thread会不会产生内存泄漏?
A: 有可能会。
1. 分析可能产生内存泄漏的原因
当ThreadLocal没有强引用时,发生GC势必会将该对象回收,此时ThreadLocalMap会产生一个Key为NULL但是Value不为空的Entery,由于ThreadLocalMap是由Thread引用,所有当前线程不结束的话就会有一个一直存在的强引用链:
Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value
此时永远无法回收,造成内存泄露。
ThreadLocal的解决办法是每次set或者getEntery时会将key为null的Entery对象擦除;
因为现在的项目基本是由tomcat线程池来复用线程,很容易造成读到别的线程未清除的数据,但是由于不是每次使用者会调用这些方法,所有每次使用完之后尽量手都删除;
1.2.2 构造方法
ThreaLocalMap 有两个构造方法
1.普通构造器
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);
}
- 创建一个Entery数组
- 将传入的ThreadLocal的threadLocalHashCode与(16-1)取模获得该Value所在的数组索引(为什么这样设计看前面threadLocalHashCode讲解)
- 设置数组大小和数组扩容的阈值
2.带有父子关系的构造器
private ThreadLocalMap(ThreadLocalMap parentMap) {
Entry[] parentTable = parentMap.table;
int len = parentTable.length;
setThreshold(len);
table = new Entry[len];
for (Entry e : parentTable) {
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++;
}
}
}
}
- 根据传入的父线程的ThreadLocalMap的大小创建一个新的Entry[]
- 遍历父线程的ThreadLocalMap
- 若Entry不为空,获取Entery中的Key(ThreadLocal)
- 若Key不为空,调用#childValue()方法
- 创建新的Entry对象,存入该对象的key,value
- 计算索引,使用线性探测法放入第一步创建的新数组中
- 若Entry不为空,获取Entery中的Key(ThreadLocal)
1.2.3 #SET()
set方法
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
//每次调用threadLocalHashCode都会有不一样的
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
//开放地址法每次寻找+1的索引
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();
}
首先讲一下流程:
- 获取当前key(ThreadLocal)的threadLocalHashCode & (len-1) 来获取索引位置
- 遍历,看一下当前的索引位置是否有值
- 若当前索引的k和传入的key相等,则直接将本次的value值覆盖
- 若当前索引的k为空,说明对应的ThreadLocal已经被回收,则执行#replaceStaleEntry方法
- 若没找到可以替换过时的和已经存在的key,则新建一个entery
- 若是没有清楚过时的entery并且table的大小已经>=阈值,则执行#rehash方法,先尝试将过时的entery删除,看看是否能将table缩小,若不行,则进行2倍扩容
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
int slotToExpunge = staleSlot;
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
// Start expunge at preceding stale entry if it exists
if (slotToExpunge == staleSlot)
slotToExpunge = i;
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
#replaceStaleEntry:这个方法的代码比较复杂,其实只是干了三件事
- 遍历查找传入索引的前面的第一个的空值
- 遍历查找传入索引的后面的第一个的空值,判断后面是否有和传入key相同的k,则和传入的空值所在的索引位置替换一下(上述两部只是在给定的索引区间内删除过时entery)
- 清理整个tabale过时entery,重新挪动不过时的entery,保证table中的连续性
其中#prevIndex和#nextIndex都属于开放地址法来寻址,该方法好处是介绍hash冲突,有兴趣小伙伴可以自己查找资料
1.2.4 #getEntry()
获取key(ThreadLocal)的Entery对象,也是根据threadLocalHashCode取摸运算获取索引,如果table表中存在该值,并且k值和传入key值一样则返回该entery,否则调用#getEntryAfterMiss,一直查找下一个索引位置是否有相同key的值,有则返回,中间还会清除区间内的过时entery
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);
}
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;
}
1.3 总结
至此,ThreadLocal大部分功能已经看完了,主要是对Entery的封装和防止内存泄漏的清除null值比较复杂,最终ThreadLocalMap落到了Thread中的threadLocals属性属性
/* 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;
2. InheritableThreadLocal
ThreadLocal的子类,用来实现父线程传递给子线程相同信息的类
所以该类使用和ThreadLocal使用方式相差不多
首先看一下整个类
package java.lang;
import java.lang.ref.*;
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
protected T childValue(T parentValue) {
return parentValue;
}
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
}
该类比较简单,重写了#getMap()和#createMap(),创建Map和获取Map时都指向了Thread中的inheritableThreadLocals对象,也就是说当执行#set()或者#get()方法时,会赋值给Thread中的inheritableThreadLocals,但是这些还是看不出来如何将父线程的变量传给子线程。
其实子线程去获取父线程的变量是在线程创建时完成的,我们看一下Thread的构造方法
private Thread(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
//。。。省略无关代码
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
//。。。省略无关代码
}
可以看出来inheritThreadLocals这个值为true并且 parent.inheritableThreadLocals不为空时会调用#createInheritedMap()方法父类的inheritableThreadLocals对象传入当前子线程。而当我们继承Thread或者传入一个Runable接口时,inheritThreadLocals都为true,下面是一段测试代码
public static void main(String[] args) {
InheritableThreadLocal<String> ctx = new InheritableThreadLocal<>();
ctx.set("我是主线程中的ThreadLocal");
new Thread(()->{
System.out.println(Thread.currentThread().getName()+ctx.get());
}).start();
System.out.println(Thread.currentThread().getName()+ctx.get());
}
我在创建子线程时对构造器代码打了断点如下,inheritThreadLocals为true
![在这里插入图片描述](https://img-blog.csdnimg.cn/f49add38eaf940fca5febd499a19c3fd.png)
而父线程的inheritableThreadLocals已经set过值了,所以这时拿到了父线程数据调用#createInheritedMap()方法
![在这里插入图片描述](https://img-blog.csdnimg.cn/d64b7320a7634326bcca27b4cd65d209.png)
该方法调用了ThreadlocalMap的另一个构造器,详情看1.2.2 中的解释
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
return new ThreadLocalMap(parentMap);
}
至此,该类的其他使用和ThreadLocal无异。子线程之所以能获得父线程放到InheritableThreadLocal的数据,是因为在创建子线程时,复制了父线程的inheritableThreadLocals属性,触发复制的时机是创建子线程的时候。但是注意必须是主线程创建出来的子线程,线程池中的线程不适用该场景,因为线程池中的线程已经是创建好的线程,那么这么分析是不是可以在创建任务的时候来进行复制呢,顺着这个思路,那么接下来分析的TransmittableThreadLocal可以适用在线程池中的场景的原理。
3.TransmittableThreadLocal
这是阿里开源的一个类,主要是处理上文中提到的子线程用父线程的变量必须要在当前线程下,而在线程池中取到的线程不能和子线程共享变量的问题,下面我复制官网上的功能介绍大家来看一下
官网说的很详细了,其最本质的实现原理也是上文分析中的线程池的线程在提交任务的时候进行threadLocal变量的赋值既可以实现,接下来我们分析下TransmittableThreadLocal
先看下整体的类
其中大致的方法和InheritbaleThreadLocal差不多,我们从#set()方法开始解析
3.1 #set()
@Override
public final void set(T value) {
if (!disableIgnoreNullValueSemantics && null == value) {
// may set null to remove value
remove();
} else {
super.set(value);
addThisToHolder();
}
}
- 这里有一个判断空值是否可以存在,构造器可以传入disableIgnoreNullValueSemantics的值,不传默认false
- 由于继承了InheritbaleThreadLocal类,先调用父类的#set()方法,详情见1.1.2的详解
- 重点方法,#addThisToHolder()
3.1.1 #addThisToHolder()
private void addThisToHolder() {
if (!holder.get().containsKey(this)) {
holder.get().put((TransmittableThreadLocal<Object>) this, null);
}
}
该方法判断holder中是否存在本线程,不存在则新增;所以holder是一个很关键的变量;
3.1.1.1
private static final InheritableThreadLocal<WeakHashMap<TransmittableThreadLocal<Object>, ?>> holder =
new InheritableThreadLocal<WeakHashMap<TransmittableThreadLocal<Object>, ?>>() {
@Override
protected WeakHashMap<TransmittableThreadLocal<Object>, ?> initialValue() {
return new WeakHashMap<TransmittableThreadLocal<Object>, Object>();
}
@Override
protected WeakHashMap<TransmittableThreadLocal<Object>, ?> childValue(WeakHashMap<TransmittableThreadLocal<Object>, ?> parentValue) {
return new WeakHashMap<TransmittableThreadLocal<Object>, Object>(parentValue);
}
};
holder为全局的一个InheritableThreadLocal变量,用WeakHashMap缓存了所有该线程设置了TransmittableThreadLocal。具体如何用到该该对象的,我们看接下来对提交任务时的分析
3.2 提交任务时的复制
3.2.1 使用实例
要想要实现提交任务时让子线程获取父线程的TransmittableThreadLocal中的变量,则需要将我们的线程池用_TtlRunnable或者TtlCallable _来包装线程池中的线程,阿里也提供了工具类_TtlExecutors _来直接包装线程池
下面都写一个简单的使用DEMO
public class Demo {
public final static TransmittableThreadLocal<Integer> ctxTTL =new TransmittableThreadLocal<>();
//模拟tomcate线程池
private static ExecutorService tomcatThreadPool=Executors.newFixedThreadPool(5);
//用TtlExecutors包装一个线程池来实现任务执行时使用线程池来复用线程
private static ExecutorService businessThreadPool=TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(5));
//模拟线程进入controller层时候处理业务
static class Controller implements Runnable{
private int i;
public ControlleThread(int i){
this.i=i;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+":"+i);
ctxTTL.set(i);
//使用异步线程池处理任务
businessThreadPool.submit(new BusinessTask(Thread.currentThread().getName()));
}
}
//执行子任务的实际线程
static class BusinessTask implements Runnable{
private String parentName;
public BusinessTask (String parentName) {
this.parentName=parentName;
}
@Override
public void run() {
System.out.println("异步线程执行时的名称"+parentName+"====="+ctxTTL.get());
}
}
public static void main(String[] args) {
//提交10个任务
for (int i = 0; i <10 ; i++) {
tomcatThreadPool.submit(new ControlleThread(i));
}
}
}
//执行结果
pool-1-thread-1:0
pool-1-thread-4:3
pool-1-thread-5:4
pool-1-thread-2:1
pool-1-thread-3:2
pool-1-thread-4:5
pool-1-thread-5:6
pool-1-thread-4:7
pool-1-thread-5:8
pool-1-thread-4:9
异步线程执行时的名称pool-1-thread-4=====3
异步线程执行时的名称pool-1-thread-4=====7
异步线程执行时的名称pool-1-thread-5=====8
异步线程执行时的名称pool-1-thread-4=====9
异步线程执行时的名称pool-1-thread-3=====2
异步线程执行时的名称pool-1-thread-2=====1
异步线程执行时的名称pool-1-thread-5=====6
异步线程执行时的名称pool-1-thread-4=====5
异步线程执行时的名称pool-1-thread-5=====4
异步线程执行时的名称pool-1-thread-1=====0
结果上来看每一个执行任务的子线程获取到的TransmittableThreadLocal都是其父线程set进去的值,符合预期;<br />包装过的线程池在提交任务时使用的是_TtlRunnable_来提交的接下来看一下提交任务时是如何进行复制的
@Override
public Future<?> submit(@NonNull Runnable task) {
return executorService.submit(TtlRunnable.get(task, false, idempotent));
}
public static TtlRunnable get(@Nullable Runnable runnable, boolean releaseTtlValueReferenceAfterRun, boolean idempotent) {
if (null == runnable) return null;
//若自定义了runnable并且实现了TtlEnhanced接口则直接返回该runnable
//否则创建新的TtlRunnable
if (runnable instanceof TtlEnhanced) {
// avoid redundant decoration, and ensure idempotency
if (idempotent) return (TtlRunnable) runnable;
else throw new IllegalStateException("Already TtlRunnable!");
}
return new TtlRunnable(runnable, releaseTtlValueReferenceAfterRun);
}
3.2.2 _TtlRunnable _
提交任务的时候会创建一个新的TtlRunnable,
首先看下TtlRunnable创建时做的事情,看一下构造器
private TtlRunnable(@NonNull Runnable runnable, boolean releaseTtlValueReferenceAfterRun) {
this.capturedRef = new AtomicReference<Object>(capture());
this.runnable = runnable;
this.releaseTtlValueReferenceAfterRun = releaseTtlValueReferenceAfterRun;
}
- 调用#capture(),这个方法比较重要,我们一起看一下
public static Object capture() {
return new Snapshot(captureTtlValues(), captureThreadLocalValues());
}
private static HashMap<TransmittableThreadLocal<Object>, Object> captureTtlValues() {
HashMap<TransmittableThreadLocal<Object>, Object> ttl2Value = new HashMap<TransmittableThreadLocal<Object>, Object>();
for (TransmittableThreadLocal<Object> threadLocal : holder.get().keySet()) {
ttl2Value.put(threadLocal, threadLocal.copyValue());
}
return ttl2Value;
}
private static HashMap<ThreadLocal<Object>, Object> captureThreadLocalValues() {
final HashMap<ThreadLocal<Object>, Object> threadLocal2Value = new HashMap<ThreadLocal<Object>, Object>();
for (Map.Entry<ThreadLocal<Object>, TtlCopier<Object>> entry : threadLocalHolder.entrySet()) {
final ThreadLocal<Object> threadLocal = entry.getKey();
final TtlCopier<Object> copier = entry.getValue();
threadLocal2Value.put(threadLocal, copier.copy(threadLocal.get()));
}
return threadLocal2Value;
}
private static class Snapshot {
final HashMap<TransmittableThreadLocal<Object>, Object> ttl2Value;
final HashMap<ThreadLocal<Object>, Object> threadLocal2Value;
private Snapshot(HashMap<TransmittableThreadLocal<Object>, Object> ttl2Value, HashMap<ThreadLocal<Object>, Object> threadLocal2Value) {
this.ttl2Value = ttl2Value;
this.threadLocal2Value = threadLocal2Value;
}
}
总体来说就是新建了一个Snapshot对象,这个对象接收了两个hashmap
- HashMap<TransmittableThreadLocal, Object> ttl2Value,#captureTtlValues()方法遍历父线程(当前提交任务的线程)中的holder中的key,也就是3.1中#set方法里调用的#addThisToHolder()里存放的当前线程的TransmittableThreadLocal对象,这里有点绕,其实就是提交任务的时候把父线程存储的变量放到一个新的hashMap
- HashMap<ThreadLocal, Object> threadLocal2Value ,#captureThreadLocalValues()方法,把父线程(当前提交任务的线程)threadLocalHolder中的threadlocal变量和复制好的变量存到该Map中
创建好TtlRunnable之后最重要的看一下该类重写的rua方法
@Override
public void run() {
final Object captured = capturedRef.get();
if (captured == null || releaseTtlValueReferenceAfterRun && !capturedRef.compareAndSet(captured, null)) {
throw new IllegalStateException("TTL value reference is released after run!");
}
final Object backup = replay(captured);
try {
runnable.run();
} finally {
restore(backup);
}
}
- 首先获取到构造器中创建的Snapshot对象
- 执行#replay()方法,该方法就是将线程中的Snapshot对象重新复制出来一个backup,然后再把线程的赋值给当前子线程
public static Object replay(@NonNull Object captured) {
final Snapshot capturedSnapshot = (Snapshot) captured;
return new Snapshot(replayTtlValues(capturedSnapshot.ttl2Value), replayThreadLocalValues(capturedSnapshot.threadLocal2Value));
}
private static HashMap<TransmittableThreadLocal<Object>, Object> replayTtlValues(@NonNull HashMap<TransmittableThreadLocal<Object>, Object> captured) {
HashMap<TransmittableThreadLocal<Object>, Object> backup = new HashMap<TransmittableThreadLocal<Object>, Object>();
//遍历当前线程的holder中的所有key
for (final Iterator<TransmittableThreadLocal<Object>> iterator = holder.get().keySet().iterator(); iterator.hasNext(); ) {
TransmittableThreadLocal<Object> threadLocal = iterator.next();
// 放入到backup
backup.put(threadLocal, threadLocal.get());
// clear the TTL values that is not in captured
// avoid the extra TTL values after replay when run task
//传入的父线程的captured没有该对象则将holder中该key移除
if (!captured.containsKey(threadLocal)) {
iterator.remove();
threadLocal.superRemove();
}
}
//把父线程的变量重新set到子线程
// set TTL values to captured
setTtlValuesTo(captured);
// call beforeExecute callback
doExecuteCallback(true);
return backup;
}
private static HashMap<ThreadLocal<Object>, Object> replayThreadLocalValues(@NonNull HashMap<ThreadLocal<Object>, Object> captured) {
final HashMap<ThreadLocal<Object>, Object> backup = new HashMap<ThreadLocal<Object>, Object>();
for (Map.Entry<ThreadLocal<Object>, Object> entry : captured.entrySet()) {
final ThreadLocal<Object> threadLocal = entry.getKey();
backup.put(threadLocal, threadLocal.get());
final Object value = entry.getValue();
if (value == threadLocalClearMark) threadLocal.remove();
else threadLocal.set(value);
}
return backup;
}
- 执行传入的runable本身的run方法
- 最后要执行 #restore(backup)方法,将原先线程的Snapshot对象再放回到执行完的当前线程
3.3.3 总结
TTL设计的精妙之处就是这个holder的设计,至于为什么这么设计的希望读者有兴趣可以一探究竟;
4.FastThreadLocal
netty用于提升threadlocal性能而进行的又一次封装的类,threadLocal使用内置的threadLocalMap进行存储数据,本质其实是一个Hash表,使用线性探测的方法寻找插入的slot,如果没有空闲的slot就不断往后查找,这种方式容易遇到hash冲突。为了避免hash冲突和提升效率,FTL使用数组来存储threadLocal的数据,并且在每个FTL创建时分配一个不重复的index,获取数据时直接用该index来获取数据,效率会大大提升
首先看一下整体结构
其中有一个全局变量 index
该属性在整个JVM中是全局唯一的,也就是说,JVM中第一个实例化的FastThreadLocal的index为0,第二个为1,依次类推。
接下来介绍一下几个基础的类之后我们再看一下FTL的其他方法
4.1 FastThreadLocalThread
FTL的想要使用数组来存储数据提升效率的话,线程需要使用_FastThreadLocalThread_
该类继承Thread,看一下整体结构
而该类的使用可以看一下Netty的创建客户端或者服务端时,会创建的默认线程工厂_DefaultThreadFactory,_而该工厂会创建FastThreadLocalThread类作为当前使用的线程
里面就是一些构造方法,其中有个比较重要的成员属性:
private _InternalThreadLocalMap _threadLocalMap
该类存储了ThreadLocal相关的数据,接下来看一下这个类
4.1.1 InternalThreadLocalMap
这个类中的属性和方法比较多,主要是操作当前线程存储的ThreadLocal对象的
4.1.1.1 #get()
看一下#get()方法,获取当前线程的InternalThreadLocalMap对象
public static InternalThreadLocalMap get() {
Thread thread = Thread.currentThread();
if (thread instanceof FastThreadLocalThread) {
return fastGet((FastThreadLocalThread) thread);
} else {
return slowGet();
}
}
//若是FastThreadLocalThread线程则执行该方法
private static InternalThreadLocalMap fastGet(FastThreadLocalThread thread) {
InternalThreadLocalMap threadLocalMap = thread.threadLocalMap();
if (threadLocalMap == null) {
thread.setThreadLocalMap(threadLocalMap = new InternalThreadLocalMap());
}
return threadLocalMap;
}
//普通线程执行该方法
private static InternalThreadLocalMap slowGet() {
InternalThreadLocalMap ret = slowThreadLocalMap.get();
if (ret == null) {
ret = new InternalThreadLocalMap();
slowThreadLocalMap.set(ret);
}
return ret;
}
1.若当前线程是FastThreadLocalThread,则获取当前FastThreadLocalThread线程的InternalThreadLocalMap。若不存在则初始化一个threadLocalMap,并且设置到当前FastThreadLocalThread,我们看一下初始化的构造方法
private static final int INDEXED_VARIABLE_TABLE_INITIAL_SIZE = 32;
public static final Object UNSET = new Object();
private InternalThreadLocalMap() {
indexedVariables = newIndexedVariableTable();
}
private static Object[] newIndexedVariableTable() {
Object[] array = new Object[INDEXED_VARIABLE_TABLE_INITIAL_SIZE];
Arrays.fill(array, UNSET);
return array;
}
初始化一个32位的大小的数组,并且给数组中的每个位置初始化一个空对象UNSET
2.若当前线程不是FastThreadLocalThread,则从正常的线程的ThreadLocal对象获取InternalThreadLocalMap(InternalThreadLocalMap对象存储在ThreadLocal中);为了提高性能,Netty 还是避免使用了JDK 的 threadLocalMap,他的方式是曲线救国:在JDK 的 threadLocal 中设置 Netty 的 InternalThreadLocalMap ,然后,这个 InternalThreadLocalMap 中设置 Netty 的 FastThreadLcoal
4.1.1.2 #setIndexedVariable
该方法主要是给数组赋值
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 final int ARRAY_LIST_CAPACITY_EXPAND_THRESHOLD = 1 << 30;
private static final int ARRAY_LIST_CAPACITY_MAX_SIZE = Integer.MAX_VALUE - 8;
private void expandIndexedVariableTableAndSet(int index, Object value) {
Object[] oldArray = indexedVariables;
final int oldCapacity = oldArray.length;
int newCapacity;
if (index < ARRAY_LIST_CAPACITY_EXPAND_THRESHOLD) {
newCapacity = index;
newCapacity |= newCapacity >>> 1;
newCapacity |= newCapacity >>> 2;
newCapacity |= newCapacity >>> 4;
newCapacity |= newCapacity >>> 8;
newCapacity |= newCapacity >>> 16;
newCapacity ++;
} else {
newCapacity = ARRAY_LIST_CAPACITY_MAX_SIZE;
}
Object[] newArray = Arrays.copyOf(oldArray, newCapacity);
Arrays.fill(newArray, oldCapacity, newArray.length, UNSET);
newArray[index] = value;
indexedVariables = newArray;
}
1.如果传入的索引小于当前数组的长度,则将当前数组索引的位置的值取出,然后将传入的新值value赋值,返回若取出的旧的值是UNSET空对象则为true,否则为false,当且仅当第一次创建新的threadLocal变量时会返回true
2.若索引超出了数组长度则调用#expandIndexedVariableTableAndSet()方法扩容。此时统一返回TRUE,也就是说扩容时都会返回true;
当index小于ARRAY_LIST_CAPACITY_EXPAND_THRESHOLD,1左右30位,也就是2的30次方时,扩容到最近的2的幂次方大小,比如原来32,扩容到64,具体的实现这段代码是不是很熟悉,和HahsMap的#tableSizeFor()方法一样。
public clas HashMap {
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
}
也就是将传入的数经过一系列右移将值变成了全是1的二进制数,然后在+1就找到了最近的2次幂的数;
然后将旧的数组复制到新的数组,将UNSET对象赋值给新数组的空值,并且将传入的value存入index位置
4.1.1.3 #indexedVariable()
获取数组索引位置的值,若索引小于当前map中存在数组的长度,则返回该值,否则返回UNSET空对象
public Object indexedVariable(int index) {
Object[] lookup = indexedVariables;
return index < lookup.length? lookup[index] : UNSET;
}
4.2 #SET()
我们从FTL的#set()方法开始分析一下原理
public final void set(V value) {
//1.
if (value != InternalThreadLocalMap.UNSET) {
InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.get();
setKnownNotUnset(threadLocalMap, value);
} else {
remove();
}
}
//2.
private void setKnownNotUnset(InternalThreadLocalMap threadLocalMap, V value) {
if (threadLocalMap.setIndexedVariable(index, value)) {
addToVariablesToRemove(threadLocalMap, this);
}
}
//3.
private static void addToVariablesToRemove(InternalThreadLocalMap threadLocalMap, FastThreadLocal<?> variable) {
Object v = threadLocalMap.indexedVariable(variablesToRemoveIndex);
Set<FastThreadLocal<?>> variablesToRemove;
if (v == InternalThreadLocalMap.UNSET || v == null) {
variablesToRemove = Collections.newSetFromMap(new IdentityHashMap<FastThreadLocal<?>, Boolean>());
threadLocalMap.setIndexedVariable(variablesToRemoveIndex, variablesToRemove);
} else {
variablesToRemove = (Set<FastThreadLocal<?>>) v;
}
//4
variablesToRemove.add(variable);
}
1.首先判断当前对象是否是缺省值 UNSET,若不是的话从当前线程获取InternalThreadLocalMap对象(参考4.1.1.1), 并且执行#setKnownNotUnset()方法(参考4.1.1.2),否则调用#remove()方法;
- 若#setIndexedVariable()返回为true,则执行#addToVariablesToRemove()方法
- 看一下判断#indexedVariable()(参考4.1.1.3)方法返回的对象是不是UNSET对象(第一次调用此方法时variablesToRemoveIndex若没有做特殊处理则返回的是0值,也就是当InternalThreadLocalMap初始化时获取到的第一个index对象),若是或者空值的话则将创建一个新的IdentityHashMap集合,并且放到这个0索引位置上;
- 否则将v值强转为set赋值给variablesToRemove
- 最终目的时将本次传入的FastThreadLocal放到这个set集合中,也就是索引0存在的位置,为了方便以后清除代码时将全部FastThreadLocal清楚,防止内存泄漏
4.3#get()
再看一下#get()方法
//1.
public final V get() {
InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.get();
Object v = threadLocalMap.indexedVariable(index);
if (v != InternalThreadLocalMap.UNSET) {
return (V) v;
}
return initialize(threadLocalMap);
}
//2.
private V initialize(InternalThreadLocalMap threadLocalMap) {
V v = null;
try {
v = initialValue();
} catch (Exception e) {
PlatformDependent.throwException(e);
}
threadLocalMap.setIndexedVariable(index, v);
addToVariablesToRemove(threadLocalMap, this);
return v;
}
1.调用当前线程的 #InternalThreadLocalMap.get()(参考4.1.1.1)方法,若不是UNSET对象,则直接返回,否则调用#initialize()方法
2.初始化,支持使用函数表达式创建FTL时传入一个默认初始化的值,然后调用#setIndexedVariable()方法设置V值,并且调用#addToVariablesToRemove()方法,上述文章已经阐述过该方法。
4.3 总结
FTL之所以快,就是使用了数组代替了hash表结构,用空间换时间;