多个线程共享变量可以使用static实现,那如果想实现每个线程有自己的共享变量怎样实现呢?
我们先来讨论一个实际问题:(当然这个方案并不是一个好主意因为发生异常时可能导致value无法被清理-详见倒数第二句话)
web后台项目用户登录后操作网数据库插入一条数据,我们不希望在controller、service、dao之间显式的定义参数传递用户名等信息,我们可以在登录拦截器里把校验后的用户信息保存到一个地方,然后再dao层直接取出,这样插入数据的时候就可以知道是谁操作的,这个功能可以使用ThreadLocal实现。
第一步:定义一个用户全局上下文对象
public class UserContextHolder {
public static ThreadLocal<String> context = new ThreadLocal<>();
}
第二部:登录拦截器保存用户名到ThreadLocal
public class MyInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//从sso系统获取用户名,这里假设是tom
UserContextHolder.context.set("tom");
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
}
第三步:dao中获取用户名
public void insert(){
String name = UserContextHolder.context.get();
System.out.println("dao层拿到用户名"+name);
}
- 源码
第一次设置值时调用的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时会执行 createMap(t, value);
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
这里可以看到,方法是把当前线程对象的threadlocals属性初始化了,ThreadLocalMap我们先把它理解成一个map后面再详细解释他,可以看到map的key和当前ThreadLocal对象有关,值就是我们自己设置的值
先看一下Thread类确实有一个threadlocals属性,这个属性创建线程时不会自己初始化,只有当调用ThreadLocal的set、get方法时才会初始化
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
接下来看一下ThreadLocalMap的构造方法
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);
}
ThreadLocalmap中有一个Entry数组private Entry[] table;
Entry是一个弱引用对象
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对象帮助理解
table[i] = new Entry(firstKey, firstValue);设置数组[i]位置为一个Entry对象,对象的key为当前ThreadLocal对象,value是我们设置的值。
在同一个线程中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();
}
可以看到是从Thread对象中获取threadlocals(其实是一个ThreadLocalMap),然后map不为空就获取当前ThreadLocal对象为key的Entry对象,然后获取值,最后如果都没有的话会执行初始化方法setInitialValue();
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
protected T initialValue() {
return null;
}
初始化默认会设置一个null到ThreadLocalMap,我们可以自定义一个类继承ThreadLocal重写initialValue()方法设置初始值。
分析完以上部分我们大概就可以知道ThreadLocal的原理了,看图
3. ThreadLocal,内存泄漏问题
前面我们说到ThreadLocalMap内部有一个Entry数组,Entry的key为ThreadLocal对象的弱引用,弱引用对象在没有强引用的情况下在系统gc时会被回收。
如果再一个执行时间很长(甚至和应用生命周期一样长)的线程中Threadlocal对象一但没有强引用那么JVM在Gc时会回收该ThreadLocal对象,也就是Thread->threadLocals(ThreadLocalMap)->table(Entry[])->Entry->key将为null,这种情况下value已无法访问但是却还是强引用不会被回收。
所以前面我们把ThreadLocal定义成了static的,static也代表了ThreadLocal对象和该类的生命周期一样长,使用remove方法可以手动移除相关线程的value及ThreadLocal引用
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;
}
}
}
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
tab[staleSlot].value = null;
tab[staleSlot] = null;
。。。。。。
}
因此我们可以在value使用结束后remove一下,所以我们最开始的拦截器案例好多人会加这样一句
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
UserContextHolder.context.remove();
}
如果每次请求响应都是一个新的线程,响应结束后该线程也销毁的话,我们就可以不必清空value。当然tomcat使用线程池处理用户请求,当然这个方案并不是一个好主意因为发生异常时可能导致value无法被清理,因为线程执行不到postHandle方法。
在使用线程池的情况下,没有及时清理ThreadLocal,不仅是内存泄漏的问题,更严重的是可能导致业务逻辑出现问题。所以,使用ThreadLocal就跟加锁完要解锁一样,用完就清理。