当使用ThreadLocal中来缓存当前线程信息的时候,由于不断创建销毁线程是一个很大的开销,我们会使用线程池来管理闲置的线程,线程池中的线程是固定的,多线程的状态下会将同一个线程不断的重用。
这样我们用线程来缓存数据信息的时候。顾名思义,线程池会重用固定的几个线程,一旦线程重用,那么很可能首次从ThreadLocal 获取的值是之前其他用户的请求遗留的值。这时,ThreadLocal 中的用户信息就是其他用户的信息。
测试代码
private final ThreadLocal<String> currentUser = ThreadLocal.withInitial(() -> null);
@ResponseBody
@GetMapping("la2")
public JSONObject wrong() {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
String ipAddr = getIpAddr(request);
//设置用户信息之前先查询一次ThreadLocal中的用户信息
String before = Thread.currentThread().getName() + ":" + currentUser.get();
//设置用户信息到ThreadLocal
currentUser.set(ipAddr);
JSONObject jsonObject = new JSONObject();
//设置用户信息之后再查询一次ThreadLocal中的用户信息
String after = Thread.currentThread().getName() + ":" + currentUser.get();
//汇总输出两次查询结果
Map result = new HashMap();
result.put("before", before);
result.put("after", after);
jsonObject.put("1",result);
return jsonObject;
}
/**
* 获取Ip地址的方法
* @param request
* @return
*/
public static String getIpAddr(HttpServletRequest request) {
String ipAddress = null;
try {
ipAddress = request.getHeader("x-forwarded-for");
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("WL-Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getRemoteAddr();
if (ipAddress.equals("127.0.0.1")) {
// 根据网卡取本机配置的IP
InetAddress inet = null;
try {
inet = InetAddress.getLocalHost();
} catch (UnknownHostException e) {
e.printStackTrace();
}
ipAddress = inet.getHostAddress();
}
}
// 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
if (ipAddress != null && ipAddress.length() > 15) { // "***.***.***.***".length()
// = 15
if (ipAddress.indexOf(",") > 0) {
ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));
}
}
} catch (Exception e) {
ipAddress="";
}
// ipAddress = this.getRequest().getRemoteAddr();
return ipAddress;
}
测试结果
# 正常情况下应该
{"1":{"before":"http-nio-9641-exec-1:null","after":"http-nio-9641-exec-1:10.200.124.239"}}
# 多刷新几次 当线程池中的线程全部调用过,他们之前获得的信息没有被清理
{"1":{"before":"http-nio-9641-exec-6:10.200.124.239","after":"http-nio-9641-exec-6:10.200.124.239"}}
ThreadLocal 清理后
private final ThreadLocal<String> currentUser = ThreadLocal.withInitial(() -> null);
@ResponseBody
@GetMapping("la2")
public JSONObject wrong() {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
String ipAddr = getIpAddr(request);
//设置用户信息之前先查询一次ThreadLocal中的用户信息
String before = Thread.currentThread().getName() + ":" + currentUser.get();
//设置用户信息到ThreadLocal
currentUser.set(ipAddr);
JSONObject jsonObject = new JSONObject();
try {
//设置用户信息之后再查询一次ThreadLocal中的用户信息
String after = Thread.currentThread().getName() + ":" + currentUser.get();
//汇总输出两次查询结果
Map result = new HashMap();
result.put("before", before);
result.put("after", after);
jsonObject.put("1", result);
} finally {
currentUser.remove();
}
return jsonObject;
}
/**
* 获取Ip地址的方法
*
* @param request
* @return
*/
public static String getIpAddr(HttpServletRequest request) {
String ipAddress = null;
try {
ipAddress = request.getHeader("x-forwarded-for");
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("WL-Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getRemoteAddr();
if (ipAddress.equals("127.0.0.1")) {
// 根据网卡取本机配置的IP
InetAddress inet = null;
try {
inet = InetAddress.getLocalHost();
} catch (UnknownHostException e) {
e.printStackTrace();
}
ipAddress = inet.getHostAddress();
}
}
// 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
if (ipAddress != null && ipAddress.length() > 15) { // "***.***.***.***".length()
// = 15
if (ipAddress.indexOf(",") > 0) {
ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));
}
}
} catch (Exception e) {
ipAddress = "";
}
// ipAddress = this.getRequest().getRemoteAddr();
return ipAddress;
}
测试结果
# 不管如何测试都是不会拿到上一个被调用线程时的ip地址
{"1":{"before":"http-nio-9641-exec-1:null","after":"http-nio-9641-exec-1:10.200.124.239"}}
总结
使用ThreadLocal 线程池来缓存信息的时候,用完一定要清理,防止奇怪线程的bug出现