源码解析前我们先分析一下ThreadLocal实现原理:每个Thread维护一个ThreadLocalMap映射表,这个映射表的key是ThreadLocal实例本身,value是真正需要存储的Object。也就是说ThreadLocal本身不存储值,它只是作为一个key来让线程从ThreadLocalMap获取value。值得注意的是图中(图片摘自网络)的虚线,表示ThreadLocalMap是使用ThreadLocal的弱引用作为key的,弱引用的对象在GC时会被回收。
public T get() {} //获取当前线程中ThreadLocal副本
public void set(T value) {} //用来设置当前线程中ThreadLocal副本
public void remove() {} //移除当前线程中ThreadLocal副本
protected T initialValue() {} //是一个protected方法,一般使用时需要重写,默认返回为null
/**
* Returns the value in the current thread's copy of this
* thread-local variable. If the variable has no value for the
* current thread, it is first initialized to the value returned
* by an invocation of the {@link #initialValue} method.
*
* @return the current thread's value of this thread-local
*/
public T get() {
//获取当前线程
Thread t = Thread.currentThread();
//获取当前线程中的ThreadLocalMap,每个线程都会有一个类型为ThreadLocalMap的inheritableThreadLocals变量
ThreadLocalMap map = getMap(t);
//如果当前线程中ThreadLocalMap为null,则返回初始化值
if (map != null) {
//获取map中ThreadLocal副本
ThreadLocalMap.Entry e = map.getEntry(this);
//获取存储的变量值
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//返回初始化值,一般setInitialValue()需要重写,自定义初始化值
return setInitialValue();
}
方法注释内容大致为: 返回当前线程的此线程局部变量副本中的值。 如果变量没有当前线程的值,则首先将其初始化为调用initialValue()方法返回的值。
ThreadLocalMap.Entry e = map.getEntry(this);
以上Entry类是内部类ThreadLocalMap中的内部类,集成了WeakReference类,如下所示:
/**
* 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. */
//与此ThreadLocal关联的值
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
该类注释的大致意思是: 此hash map的entries使用它的“主引用”字段作为键(它始终是ThreadLocal对象)继承自WeakReference类。 请注意,null键(即entry.get()== null)表示不再引用该键,因此可以从table中删除该entry。 这些entries在下面的代码中称为“陈旧entry”。
既然Entry继承了WeakReference,说明ThreadLocal使用了弱引用,如果entry.get()==null,当前ThreadLocal副本会立即被GC掉,避免高并发情况下出现内存溢出。
/**
* Sets the current thread's copy of this thread-local variable
* to the specified value. Most subclasses will have no need to
* override this method, relying solely on the {@link #initialValue}
* method to set the values of thread-locals.
*
* @param value the value to be stored in the current thread's copy of
* this thread-local.
* 要存储在当前线程的此线程本地副本中的值
*/
public void set(T value) {
//获取当前线程
Thread t = Thread.currentThread();
//从当前线程中的ThreadLocalMap中获取存储的当前副本的值
ThreadLocalMap map = getMap(t);
//如果map不为空,覆盖当前副本中的值,否则新建副本
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
该方法的大致意思是: 将此线程局部变量的当前线程副本设置为指定值。 大多数子类都不需要重写此方法,仅依靠initialValue()方法来设置线程局部变量值。
/**
* Removes the current thread's value for this thread-local
* variable. If this thread-local variable is subsequently
* {@linkplain #get read} by the current thread, its value will be
* reinitialized by invoking its {@link #initialValue} method,
* unless its value is {@linkplain #set set} by the current thread
* in the interim. This may result in multiple invocations of the
* {@code initialValue} method in the current thread.
*
* @since 1.5
*/
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
该方法注释的大概意思是: 删除当前线程ThreadLocal的变量值。 如果当前线程随后通过该线程从get()方法中获取ThreadLocal的变量值,那么它的值将通过调用initialValue()方法重新获取初始值,除非它的值是当前线程临时调用set()方法重新设置了值。 这可能导致在当前线程中多次调用initialValue()方法。
/**
* Returns the current thread's "initial value" for this
* thread-local variable. This method will be invoked the first
* time a thread accesses the variable with the {@link #get}
* method, unless the thread previously invoked the {@link #set}
* method, in which case the {@code initialValue} method will not
* be invoked for the thread. Normally, this method is invoked at
* most once per thread, but it may be invoked again in case of
* subsequent invocations of {@link #remove} followed by {@link #get}.
*
* This implementation simply returns {@code null}; if the
* programmer desires thread-local variables to have an initial
* value other than {@code null}, {@code ThreadLocal} must be
* subclassed, and this method overridden. Typically, an
* anonymous inner class will be used.
*
* @return the initial value for this thread-local
*/
protected T initialValue() {
return null;
}
这个方法默认返回是null,注意看一下它的修饰符是protected,一般情况这个方法是要使用者去实现的,设置默认的初始值。
ThreadLocal常用于解决数据库连接、session管理等,我在项目中用于做注解切面日志,记录了日志时间变量,不同线程中使用自己的变量副本,来统计方法的耗时,以下是我的实现代码:
/**
* @Author: Helon
* @Description: 自定义注解(切面日志)
* @Data: Created in 2018/4/16 13:39
* @Modified By:
*/
//target:注解的作用目标
//ElementType.TYPE:接口、类、枚举、注解
//ElementType.FIELD:字段、枚举的常量
//ElementType.METHOD:方法
//ElementType.CONSTRUCTOR:构造函数
//ElementType.LOCAL_VARIABLE:局部变量
//ElementType.ANNOTATION_TYPE:注解
//ElementType.PACKAGE:包
@Target({ElementType.METHOD, ElementType.TYPE})
//保留策略:
// RetentionPolicy.SOURCE:注解只保留在源文件,编译成class文件时候,注解被遗弃;
//RetentionPolicy.CLASS:注解被保留在class文件,但jvm加载class时候会被遗弃,默认值;
//RetentionPolicy.CLASS:注解不仅被保存到class文件中,jvm加载之后仍然存在;
@Retention(RetentionPolicy.RUNTIME)
//Documented:注解表明这个注解应该被 javadoc工具记录.
// 默认情况下,javadoc是不包括注解的.
// 但如果声明注解时指定了 @Documented,则它会被 javadoc 之类的工具处理, 所以注解类型信息也会被包括在生成的文档中
@Documented
public @interface AspectLog {
String description() default "操作日志";
}
/**
* @Author: Helon
* @Description: 日志切面
* @Data: Created in 2018/4/16 10:36
* @Modified By:
*/
@Aspect
@Component
public class WebLogAspect {
/***
* 用于计算时间
*/
ThreadLocal<Long> startTime = new ThreadLocal<>();
/***
* 选取按包路径进行切入
*/
/* @Pointcut("execution(public * com.chtwm.qyjr.controller..*.*(..))")
public void webLog(){}*/
//选取切入点为自定义注解
@Pointcut("@annotation(com.chtwm.qyjr.aspect.AspectLog)")
//切入点的加载顺序,值越小优先级越高
@Order(1)
public void webLog(){}
/***
* 方法执行前切入,且从aspectLog对象中获取描述信息
*/
@Before("webLog()&&@annotation(aspectLog)")
public void doBefore(JoinPoint joinPoint, AspectLog aspectLog) throws Throwable {
ServletRequestAttributes attributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
//记录开始时间
startTime.set(System.currentTimeMillis());
LogUtil.KAFKA.info("[切面日志doBefore]-日志描述:【{}】,开始执行", aspectLog.description());
LogUtil.KAFKA.info("[切面日志doBefore]-请求URL:{}", request.getRequestURL().toString());
LogUtil.KAFKA.info("[切面日志doBefore]-HTTP请求方式:{}", request.getMethod());
LogUtil.KAFKA.info("[切面日志doBefore]-请求参数:{}", Arrays.toString(joinPoint.getArgs()));
}
/***
* 方法执行完成切入,且从aspectLog对象中获取描述信息,returning = "ret"获取响应信息
*/
@AfterReturning(returning = "ret", pointcut = "webLog()&&@annotation(aspectLog)")
public void doAfterReturning(Object ret, AspectLog aspectLog) throws Throwable {
try {
LogUtil.KAFKA.info("[切面日志doAfterReturning]-响应数据:{}", JSONObject.toJSONString(ret));
LogUtil.KAFKA.info("[切面日志doAfterReturning]-耗时:{}", (System.currentTimeMillis() - startTime.get()) + "ms");
LogUtil.KAFKA.info("[切面日志doAfterReturning]-日志描述:【{}】,执行结束", aspectLog.description());
} finally {
//清除当前线程的ThreadLocal副本
startTime.remove();
}
}
}
ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,那么虚拟机GC 的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程一直不结束的话,那么这些key为null的Entry,它的value就会一直存在一条强引用链(如本文开始的结构图):Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。
其实,ThreadLocalMap的设计中已经考虑到这种情况,也加上了一些防护措施,在调用ThreadLocal的get(),set(),remove()方法的时候都会清除掉该线程ThreadLocalMap里所有key为null的value。但是,这些被动的预防措施并不能保证不会泄露,例如以下情况: