处理并发问题有两种思路,一种是使用时间换空间,比如sync同步处理保证同一时间只能有一个线程访问并发资源;还有一种是使用空间换时间,比如ThreadLocal这种,为每个线程开辟空间存放并发资源,保证各线程可以同时访问。本篇主要讲解ThreadLocal使用原理及扩展。
1、ThreadLocal基本使用
public class ThreadLocalTest {
private static volatile ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);
public static void main(String[] args) {
testThreadLocal();
}
private static void testThreadLocal() {
threadLocal.set(1);
System.out.println(threadLocal.get());
new Thread(()-> System.out.println(threadLocal.get())).start();
}
}
这段代码中,TL变量初始值为0,在主线程中将其改变为1,但是新开了一个线程再次获取TL变量时,发现其值仍为初始值0。这就是线程局部变量TL的特性:
在同一线程内对局部变量的改变不影响其它线程内的局部变量的值。
2、ThreadLocal原理解读
由TL的特性,可以主要解读其set,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);
}
ThreadLocalMap 可以理解为类似Map的一个容器,getMap方法这里返回的是Thread类的threadLocals属性。其key为当前TL对象的引用,值为TL变量的值。这样每个线程操作的其实是自己独立的threadLocals属性里面的TL对象,对其它线程的TL对象自然不影响。
3、ThreadLocal内存泄漏问题
3.1、引用关系
ThreadLocal,ThreadLocalMap,Thread之间的引用关系如下图:
其中黑线代表强引用,蓝线代表弱引用。
如果将threadLocalRef置为null,那么threadLocalObj在gc(垃圾回收)的时候势必被回收(弱引用),这时候entry的key就变为了null。同时存在着这样一条引用链:currentThreadRef->currentThreadObj->threadLocalMap->entry->valueRef->threadLocalValue,导致在垃圾回收的时候进行可达性分析的时候,entry的value可达从而不会被回收掉,但是该value永远不能被访问到(不可能通过为null的key访问到value),这样就造成了内存泄漏。
当然,如果线程执行结束后,threadLocalObj,currentThreadObj都会被回收,因此threadLocalMap,entry也都会被回收掉。可是,在实际使用中我们都是会用线程池去维护我们的线程,为了复用线程是不会结束的,所以ThreadLocal内存泄漏就值得我们关注。
3.2、为什么使用弱引用
假设Entry的key使用强引用,在业务代码中执行threadLocalIRef==null操作,以清理掉threadLocalObj实例的目的,但是因为threadLocalMap的Entry强引用threadLocalObj,因此在gc的时候进行可达性分析,threadLocal依然可达,对threadLocal并不会进行垃圾回收,这样就无法真正达到业务逻辑的目的,出现逻辑错误
3.3、ThreadLocal对内存泄漏的改进
在ThreadLocal的生命周期里,针对ThreadLocal存在的内存泄漏的问题,当调用其set,get,remove方法时,都会清理key为null的脏entry。以set为例:
- set
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length; //tab长度为2的次方
int i = key.threadLocalHashCode & (len-1); //计算key的hash值
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();
}
在该方法中针对脏entry做了这样的处理:
如果当前table[i]!=null的话说明hash冲突就需要向后环形查找entry插入位置,若在查找过程中遇到脏entry就通过replaceStaleEntry进行处理;
如果当前table[i]==null的话说明新的entry可以在i位置直接插入,但是插入后会调用cleanSomeSlots方法检测并清除脏entry
- cleanSomeSlots
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
do {
i = nextIndex(i, len);
Entry e = tab[i];
if (e != null && e.get() == null) {
n = len;
removed = true;
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0);
return removed;
}
该方法从tab的i+1位置开始,一直搜索到i+1+log2(len)。若中途未发现脏entry,则搜索结束,否则调用expungeStaleEntry方法清理脏entry,并往后重新搜索log2(len)个tab位。
- expungeStaleEntry
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
//清除当前脏entry
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
Entry e;
int i;
//往后环形继续查找,直到遇到table[i]==null时结束
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {//rehash
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
该方法清理掉当前脏entry后,并没有闲下来,而是继续向后搜索,若再次遇到脏entry继续将其清理,直到哈希桶(table[i])为null时退出。因此方法执行完的结果为 从当前脏entry(staleSlot)位到返回的i位,这中间所有的脏entry都会被清理。
为避免内存泄漏,推荐使用remove方法移除不用的ThreadLocal变量。
4、ThreadLocal扩展
4.1、InheritableThreadLocal
TL的特性是在同一线程内对局部变量的改变不影响其它线程内的局部变量的值,即每个线程里的TL对象都是独立互不影响的。如果希望当前线程的TL对象也能够被子线程使用,可以使用ITL。
- 使用方法
public class ThreadLocalTest {
private static volatile ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);
private static volatile InheritableThreadLocal<Integer> integerInheritableThreadLocal = new InheritableThreadLocal<>();
public static void main(String[] args) {
//testThreadLocal();
testInheritableThreadLocal();
}
private static void testInheritableThreadLocal() {
integerInheritableThreadLocal.set(1);
System.out.println(integerInheritableThreadLocal.get());
new Thread(()-> System.out.println(integerInheritableThreadLocal.get())).start();
}
}
在主线程中将ITL变量设置为1,子线程获取ITL也为1,证明ITL的值被传递到了子线程。
-
基本原理
ITL继承了TL并重写了getMap,createMap,childValue方法。
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
//父线程传递给子线程,这里返回的是父线程ITL对象的引用,应注意线程安全问题
protected T childValue(T parentValue) {
return parentValue;
}
//利用Thread的inheritableThreadLocals变量作为副本变量
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
//Thread 中 inheritableThreadLocals变量不存在时的初始化动作
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
}
可以看出ITL利用的是Thread的inheritableThreadLocals变量作为线程副本变量保存ITL对象。那么父线程的ITL对象如何传递到子线程的呢?初步猜测是子线程初始化时,将父线程的inheritableThreadLocals属性复制给了子线程。果然在Thread的init()方法中找到如下片段:
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
注意的是一旦子线程被创建以后,再操作父线程中的ITL变量,那么子线程是不能感知的。因为父线程和子线程还是拥有各自的inheritableThreadLocals,只是在创建子线程的“一刹那”将父线程的inheritableThreadLocals属性复制给子线程,后续两者就没啥关系了(传递引用的情况除外,这时父子线程实际还是共享同一个引用对象)。
4.2、TransmittableThreadLocal
JDK的ITL类可以完成父线程到子线程的值传递。但对于使用线程池等会池化复用线程的执行组件的情况,线程由线程池创建好,并且线程是池化起来反复使用的;这时父子线程关系的ThreadLocal值传递已经没有意义,应用需要的实际上是把任务提交给线程池时的主线程的ThreadLocal值传递到子线程,这里就需要使用到TTL。
使用TTL对分布式跟踪系统 、日志收集系统有很大作用。
传送门:https://github.com/alibaba/transmittable-thread-local
- 使用方法
public class ThreadLocalTest {
private static volatile ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);
private static volatile InheritableThreadLocal<Integer> integerInheritableThreadLocal = new InheritableThreadLocal<>();
private static volatile TransmittableThreadLocal<Integer> transmittableThreadLocal = new TransmittableThreadLocal<>();
public static void main(String[] args) {
// testThreadLocal();
// testInheritableThreadLocal();
testTransmittableThreadLocal();
}
private static void testTransmittableThreadLocal() {
transmittableThreadLocal.set(1);
Runnable ttlRunnable = TtlRunnable.get(()->{
System.out.println(Thread.currentThread().toString()+":"+transmittableThreadLocal.get());
});
ExecutorService executorService = Executors.newFixedThreadPool(2);
//开启5个线程
for(int i=0;i<5;i++){
executorService.execute(ttlRunnable);
}
}
}
主线程将TTL值设为1,使用ttlRunnable包装任务提交到线程池中,在每个开启的线程中获取TTL都为1,证明TTL可以在由线程池开启的线程间传递。
- 基本原理
public class TransmittableThreadLocal<T> extends InheritableThreadLocal<T> implements TtlCopier<T>
首先看TTL继承ITL,所以其具备ITL的功能,其set,get方法也是调用的ITL的set,get方法。
public final T get() {
T value = super.get();
if (this.disableIgnoreNullValueSemantics || null != value) {
this.addThisToHolder();
}
return value;
}
public final void set(T value) {
if (!this.disableIgnoreNullValueSemantics && null == value) {
this.remove();
} else {
super.set(value);
this.addThisToHolder();
}
}
private void addThisToHolder() {
if (!((WeakHashMap)holder.get()).containsKey(this)) {
((WeakHashMap)holder.get()).put(this, (Object)null);
}
}
private static InheritableThreadLocal<WeakHashMap<TransmittableThreadLocal<Object>, ?>> holder = new InheritableThreadLocal<WeakHashMap<TransmittableThreadLocal<Object>, ?>>() {
protected WeakHashMap<TransmittableThreadLocal<Object>, ?> initialValue() {
return new WeakHashMap();
}
protected WeakHashMap<TransmittableThreadLocal<Object>, ?> childValue(WeakHashMap<TransmittableThreadLocal<Object>, ?> parentValue) {
return new WeakHashMap(parentValue);
}
}
比较重要的holder变量,其为一个ITL类型的静态变量,值为WeakHashMap类型,其key为TTL对象,意味着这个key不被外部所引用时,WeakHashMap的key,value都会被GC回收,避免内存泄漏问题。
TTL对象的set,get方法调用的是父类即ITL的方法,并将调用者(TTL对象)注入holder变量的key值,同时holder变量的value永远保存为null。所以holder变量保存了当前线程的所有TTL对象。
以上分析可以看出,单纯的使用TTL是达不到支持线程池本地变量的传递的。ITL能在线程间传递的原理是线程初始化时调用Init方法,将父线程的值复制给子线程,但是使用线程池就不能在线程初始化的时候传递了,因为线程池会复用线程,那么TTL是何时传递给子线程的呢,以TtlRunnable的启动方法run为例分析。
- run
private final AtomicReference<Object> capturedRef = new AtomicReference(Transmitter.capture());
public void run() {
/**捕获父线程线程变量,注意,这里的capturedRef属性在父线程new
*TtlRunnable时就确定了,所以captured是父线程的线程变量。
*/
Object captured = this.capturedRef.get();
if (captured != null && (!this.releaseTtlValueReferenceAfterRun || this.capturedRef.compareAndSet(captured, (Object)null))) {
/**
* 重点方法replay,此方法用来给当前父线程线程变量赋给子线程,返回的
* backup是此子线程原来就有的本地变量值,backup用于恢复数据(如果任
* 务执行完毕,意味着该子线程会归还线程池,那么需要将其原生本地变量
* 恢复)
*/
Object backup = Transmitter.replay(captured);
try {
this.runnable.run();
} finally {
//恢复子线程原生本地变量的值,和replay类似,下面不再分析
Transmitter.restore(backup);
}
} else {
throw new IllegalStateException("TTL value reference is released after run!");
}
}
- replay
replay中核心代码如下
private static HashMap<TransmittableThreadLocal<Object>, Object> replayTtlValues(@NonNull HashMap<TransmittableThreadLocal<Object>, Object> captured) {
HashMap<TransmittableThreadLocal<Object>, Object> backup = new HashMap();
Iterator iterator = ((WeakHashMap)TransmittableThreadLocal.holder.get()).keySet().iterator();
while(iterator.hasNext()) {
TransmittableThreadLocal<Object> threadLocal = (TransmittableThreadLocal)iterator.next();
backup.put(threadLocal, threadLocal.get());
/**
*若父线程变量不包含子线程本地变量,则移除子线程ThreadLocalMap里
*面TTL和holder变量的TTL。即子线程只能拥有父线程的所有TTL。
*/
if (!captured.containsKey(threadLocal)) {
iterator.remove();
threadLocal.superRemove();
}
}
//将父线程TTL值注入子线程
setTtlValuesTo(captured);
TransmittableThreadLocal.doExecuteCallback(true);
return backup;
}
private static void setTtlValuesTo(@NonNull HashMap<TransmittableThreadLocal<Object>, Object> ttlValues) {
Iterator var1 = ttlValues.entrySet().iterator();
while(var1.hasNext()) {
Entry<TransmittableThreadLocal<Object>, Object> entry = (Entry)var1.next();
TransmittableThreadLocal<Object> threadLocal = (TransmittableThreadLocal)entry.getKey();
threadLocal.set(entry.getValue());
}
}
灵魂发问
- 子线程里面的原生本地变量是如何产生的?
TTL是继承至ITL,线程池第一次启用时会触发Thread的init方法的,这时主线程的变量会被传递给子线程,作为子线程的原生本地变量保存起来。后续的replay操作和restore操作也是围绕着这个原生变量(即原生holder里的值)来进行设置、恢复的,设置的是当前父线程捕获到的本地变量,恢复的是子线程原生本地变量。 - holder变量的意义
holder变量作用是保存当前线程的所有TTL变量,其实Thread里的变量inheritableThreadLocals也保存着这些值。但无奈inheritableThreadLocals是default修饰,其访问范围只限于本包访问,所以创建了holder变量方便访问。
THE END