概述

最近在对一个项目进行重构,用到了ThreadLocal。场景如下:外围系统会调用接口上传数据,在接口中要记录数据的变化Id,在上传数据完后需要集中在一个地方把这些Id以消息形式发送出去。

使用场景样例代码

public Result<Void> uploadOrder(TotalPayInfoVo totalPayInfoVo) {
try {
saveTotalPayInfoVo(totalPayInfoVo);

//发送消息
UnitWork.getCurrent().pushMessage();
} catch (Exception e) {
cashLogger.error("uploadOrder error,data: {}, error: {}", JSON.toJSONString(totalPayInfoVo), e);
throw new RuntimeException("保存失败", e);
} finally {
UnitWork.clean();//
}

return ResultUtil.successResult();避免内存泄漏
}

ThreadLocal使用源码

/**
* 工作单元,在同一个线程中负责记录一个事件或者一个方法或者一个事务过程中产生的变化,等操作结束后再处理这种变化。
*/
public class UnitWork {
private UnitWork() {
}

private static ThreadLocal<UnitWork> current = new ThreadLocal<UnitWork>() {
protected UnitWork initialValue() {
return new UnitWork();
}
};

/**
* 状态变化的instance
*/
private Set<String> statusChangedInstances = new HashSet<>();

public void addStatusChangedInstance(String instance) {
statusChangedInstances.add(instance);
}

/**
* 推送消息
*/
public void pushMessage() {

for(String id : statusChangedInstances){
//异步发消息
}
}

public static UnitWork getCurrent() {
return current.get();
}

/**
* 删除当前线程的工作单元,建议放在finally中调用,避免内存泄漏
*/
public static void clean() {
current.remove();
}

}

思考问题

为了避免内存泄漏,每次用完做一下clean清理操作。发送消息的过程是异步的,意味着clean的时候可能和发送消息同时进行。那么会不会把这些Id清理掉?那么可能造成消息发送少了。要回答这个问题,首先要搞懂ThreadLocal的引用关系,remove操作做了什么?

ThreadLocal解读

ThreadLocal可以分别在各个线程保存变量独立副本。每个线程都有ThreadLocalMap,顾名思义,类似Map容器,不过是用数组Entry[]来模拟的。那么既然类似Map,肯定会存在Key。其实Key是ThreadLocal类型,Key的值是ThreadLocal的HashCode,即通过threadLocalHashCode计算出来的值。
这个Map的Entry并不是ThreadLocal,而是一个带有弱引用的Entry。既然是弱引用,每次GC的时候都会回收。

static class Entry extends WeakReference<ThreadLocal> {
/** The value associated with this ThreadLocal. */
Object value;

Entry(ThreadLocal k, Object v) {
super(k);
value = v;
}
}

而Key对应的value就是要保存在线程副本Object,这里指的就是UnitWork的实例。调用ThreadLocal的get方法时,首先找到当前线程的ThreadLocalMap,然后根据这个ThreadLocal算出来的hashCode找到保存线程副本Object。他们的关系对应如下:

关于ThreadLocal内存泄漏引起的思考_弱引用

ThreadLocal在remove的时候,会调用Entry的clear,即弱引用的clear方法。把Key->ThreadLocal的引用去掉。接下来的expungeStaleEntry会把entry中value引用设置为null。

/**
* 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;
}
}
}

现在可以回答之前提前的问题。虽然ThreadLocal和当前线程都会与Object脱离了引用的关系,但是最重要一点就是异步的线程仍然存在一条强引用路径到Object,即到UnitWork实例的强引用。因此GC然后不会回收UnitWork的实例,发消息还是不会少发或者出现空指针情况。