ThreadLocal提供了线程的局部变量,每个线程都可以通过set()和get()来对这个局部变量进行操作,但不会和其他线程的局部变量进行冲突,实现了线程的数据隔离。
应用场景
在Java的 Web项目大部分都是基于 Tomcat、Jetty容器,每次访问都是一个新的线程,这样让我们联想到了 ThreadLocal,每一个线程都独享一个 ThreadLocal,在接收请求的时候 可以set特定内容,在需要的时候 get这个值。比如进行请求的监控:响应时长、状态码等等。
@Order(Ordered.HIGHEST_PRECEDENCE)
@Component
public class ApiMonitorInterceptor implements HandlerInterceptor {
private static final Logger logger = LoggerFactory.getLogger("monitorLog");
//创建ThreadLocal
private ThreadLocal stopwatchThreadLocal = new NamedThreadLocal<>("api_monitor");
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//接收请求时,进行计时。
stopwatchThreadLocal.set(Stopwatch.createStarted());
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
try {
String requestURI = request.getRequestURI();
String requestMethod = request.getMethod();
String addr = request.getHeader("x-forwarded-for");
Integer httpStatus = response.getStatus();
//停止计时
Stopwatch stopwatch = stopwatchThreadLocal.get().stop();
Long elapsedTime = stopwatch.elapsed(TimeUnit.MILLISECONDS);
Long endTime = System.currentTimeMillis();
ApiMonitor apiMonitor = new ApiMonitor();
apiMonitor.setRequestURI(requestURI)
.setRequestMethod(requestMethod)
.setAddr(addr)
.setHttpStatus(httpStatus)
.setElapsedTime(elapsedTime)
.setEndTime(endTime);
logger.info(JsonUtils.toJsonString(apiMonitor));
} finally {
//删除对应的Entry对象
stopwatchThreadLocal.remove();
}
}
}
上述例子就是一个简单的利用ThreadLocal 来监控请求时长的demo。
ThreadLocal解剖
当为ThreadLocal类的对象set值时,首先获得当前线程的ThreadLocalMap类属性,然后以ThreadLocal类的对象为key,设定value。get值时则获取该对象的设置的值。
//set方法
public void set(T value) {
// 得到当前线程对象
Thread t = Thread.currentThread();
//获取ThreadLocalMap
ThreadLocalMap map = getMap(t);
// 如果map存在,则将当前线程对象t作为key,要存储的对象作为value存到map里面去
if (map != null)
map.set(this, value);
else
//如果不存在,则创建一个
createMap(t, value);
}
//创建ThreadLocalMap
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
//get方法
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
ThreadLocalMap是ThreadLocal的一个内部类。用Entry类来进行存储,我们的值都是存储到这个Map上的,key是当前ThreadLocal对象!
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> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal> k, Object v) {
super(k);
value = v;
}
}
...
}
Thread维护了ThreadLocalMap变量。
public class Thread implements Runnable {
...
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
...
}
因此,可以知道Thread为每个线程维护了ThreadLocalMap这么一个Map,而ThreadLocalMap的key是LocalThread对象本身,value则是要存储的对象。
总结
- 每个Thread维护着一个ThreadLocalMap的引用。
- ThreadLocalMap是ThreadLocal的内部类,用Entry来进行存储。
- 调用ThreadLocal的set()方法时,实际上就是往ThreadLocalMap设置值,key是ThreadLocal对象,值是传递进来的对象。
- 调用ThreadLocal的get()方法时,实际上就是往ThreadLocalMap获取值,key是ThreadLocal对象。
- ThreadLocal本身并不存储值,它只是作为一个key来让线程从ThreadLocalMap获取value。
内存泄露
在前面ThreadLocalMap的源码中,可以知道,当使用ThreadLocal保存一个value时,会在ThreadLocalMap中的数组插入一个Entry对象,按理说key-value都应该以强引用保存在Entry对象中,但在ThreadLocalMap的实现中,key被保存到了WeakReference对象中。
这就导致了一个问题,ThreadLocal在没有外部强引用时,发生GC时会被回收,但是如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,那时就会发生内存泄露。
避免内存泄露
既然已经发现有内存泄露的隐患,自然有应对的策略,在调用ThreadLocal的get()、set()可能会清除ThreadLocalMap中key为null的Entry对象,这样对应的value就没有GC Roots可达了,下次GC的时候就可以被回收,当然如果调用remove方法,肯定会删除对应的Entry对象。
如果使用ThreadLocal的set方法之后,没有显示的调用remove方法,就有可能发生内存泄露,因此,使用完ThreadLocal之后,记得调用remove方法。