问题
Web项目,用户每次登录系统会将Session信息存储到redis中,然后返回给客户端token,该token作为用户会话的标示,此后所有的请求都会走一个拦截器,此拦截器根据token将Session信息从redis中拿回来放到ThreadLocal中。当并发较多时,会出现Session信息错误的情况。
经分析出错的流程是:用户1登录系统并访问系统此时使用的是线程A并将用户1的Session信息存到线程A的TheadLocal,然后一个没有登录用户2也访问了系统,服务器再次分配线程A来处理此请求,此时用户2取到了用户1的Session信息。
ThreadLocal
TheadLocal是jdk提供的一个很好用的线程内共享变量工具。在Web开发时,服务器端可以使用TheadLocal来存储请求的Request、Response。不用显示的在方法调用栈传递。能很方便的在线程内方法调用栈的任何地方获取我们存进的实例变量。
class WebContext {
private TheadLocal<HttpServletRequest> request = new TheadLocal<>();
private TheadLocal<HttpServletResponse> response = new TheadLocal<>();
public static void setRequest(HttpServletRequest req) {
request.set(req);
}
public static HttpServletRequest getRequest() {
request.get();
}
public static void setResponse(HttpServletResponse resp) {
response.set(resp);
}
public static HttpServletResponse getResponse() {
response.get();
}
}
class AbcController {
public Object userInfo() {
HttpServletRequest request = WebContext.getRequest();
// request.getParameter()
// ...
}
}
复制代码
看下TheadLocal的代码:
// TheadLocal两个重要的方法
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
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();
}
复制代码
TheadLocal不管是set方法还是get方法,都会先Thread t = Thread.currentThread();
,获取当前线程实例。然后获取线程实例中的TheadLocalMap实例ThreadLocalMap map = getMap(t);
这也是为什么在同一个线程中获取线程变量得到的会是同一个实例,由于每一个线程实例都存储自己的线程变量所以,这些变量是线程安全的。
public class Thread implements Runnable {
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
}
复制代码
以上是Thread类里声明的threadLocals
属性。可以看出实际存储线程变量的是ThreadLocal.ThreadLocalMap
类,这个类和Map类似存储键值对(Key—Value),其中Key就是线程实例,Value就是线程变量。
Tomcat线程池
由于操作系统对线程的创建是很消耗资源的,所以很多应用会采用线程池的方式,系统初始化是一次性创建一定量的,这些线程就会复用共享,也就是说线程使用完之后是不会被销毁的,直到应用停止。
这样导致的结果就是,不同用户请求复用相同的线程导致,不同用户取得的Session信息相同,导致Session信息错乱。
解决
在一个设置一个拦截器,在请求的前端
设置Session信息到本地线程变量,在后端
清空本地线程变量Session信息。
// 代码简化了
public class SessionInspector extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(...) throws Exception {
// ThreadLocal.set(Session)
return true;
}
public void afterCompletion(...) {
// ThreadLocal.clear()
}
}
复制代码
总结
ThreadLocal为我们提供了方便的线程内共享变量的方式,提供了一种保证数据线程安全的方式。但是要考虑在多线程环境中线程复用的情况,如果ThreadLocal
内保存信息是会话等与用户相关的信息要在线程任务结束时清理ThreadLocal
内的信息。