ThreadLocal的使用
1.ThreadLocal是什么
线程安全问题主要原因就是 资源的共享
通过局部变量可以做到避免共享,那还有没有其他方法可以做到呢?有的,Java语言提供的 线程本地存储(ThreadLocal)就能够做到。
ThreadLocal是java.lang包中提供的类
Java线程中存在私有属性 ThreadLocalMap ,内部的键是ThreadLocal
public
class Thread implements Runnable {
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
思想:在类中创建多个 ThreadLocal
这样线程的每个对象的ThreadMap中就会有多个 ThreadLocal的键值对,互不干扰
ThreadLocal的相关方法:
//get()方法用于获取当前线程的副本变量值
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);//获取当前线程内部的ThreadLocalMap threadLocals
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
//set()方法用于保存当前线程的副本变量值。
public void set(T value) {
Thread t = Thread.currentThread();//获取当前线程
ThreadLocalMap map = getMap(t);//获取当前线程内部的ThreadLocalMap threadLocals
if (map != null)
map.set(this, value);//设置key 为当前ThreadLocal对象
else
createMap(t, value);
}
//remove()方法移除当前线程的副本变量值。
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
2.ThreadLocal使用注意事项
ThreadLocal与内存溢出问题
在线程池中使用ThreadLocal为什么可能导致内存泄露呢?原因就出在线程池中线程的存活时间太长,往往都是和程序同生共死的,这就意味着Thread持有的ThreadLocalMap一直都不会被回收,再加上 ThreadLocalMap中的Entry对ThreadLocal是弱引用(WeakReference),所以只要ThreadLocal结束了自己 的生命周期是可以被回收掉的。但是Entry中的Value却是被Entry强引用的,
所以即便Value的生命周期结束 了,Value也是无法被回收的,从而导致内存泄露。
解决方法:
ExecutorService es;
ThreadLocal tl;
es.execute(()->{
//ThreadLocal增加变量
tl.set(obj);
try {
// 省略业务逻辑代码
}finally {
//自动动清理ThreadLocal
tl.remove();
}
});
3.ThreadLocal使用的场景
3.1 Hibernate的session
private static final ThreadLocal<Session> threadLocal = new ThreadLocal<Session>();
//获取Session
public static Session getCurrentSession(){
Session session = threadLocal.get();
//判断Session是否为空,如果为空,将创建一个session,并设置到本地线程变量中
try {
if(session ==null&&!session.isOpen()){
if(sessionFactory==null){
rbuildSessionFactory();// 创建Hibernate的SessionFactory
}else{
session = sessionFactory.openSession();
}
}
threadLocal.set(session);
} catch (Exception e) {
// TODO: handle exception
}
return session;
}
3.2 面试题 方法A 、方法B、方法C 三个方法互相调用
如何在方法C中打印方法A的入参,不适用方法逐层传递的方式
使用ThreadLocal记录参数
3.3 微服务之间Fegin调用默认传递Token问题
原因:之前公司代码Fegin Client声明 全部都在参数中使用了@RequestHeader来强制入参中包含Token
@PostMapping(value = SaasUrlConstant.GET_ORGBYDIMPATH_URL)
Result<SysOrgResp> getOrgByDimPath(@RequestHeader(name = "Authorization", required = true) String token, @RequestBody QueryByIdAndName queryByIdAndName);
部分业务场景不是使用的登录人的Token,而是使用关联业务表单中的人员信息来生成Token,而不是从前端传递过来,所以使用下面通用的过滤器实现默认传Token就比较困难,无法统一FeignClient方法的入参形式。
@Configuration
public class FeignInterceptorConfig {
//--调用其他fegin服务放行URL--
private final static List<String> permitAllList = Lists.newArrayList();
static {
permitAllList.add("/v1/identity/verifyFactors");
permitAllList.add("/v1/identity/getIdentityStatusByMsg");
}
@Bean
public RequestInterceptor requestInterceptor() {
RequestInterceptor requestInterceptor = new RequestInterceptor() {
@Override
public void apply(RequestTemplate template) {
String url = template.url();
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && !permitAllList.contains(url)) {
if (authentication instanceof OAuth2Authentication) {
OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) authentication.getDetails();
String access_token = details.getTokenValue();
template.header("Authorization", OAuth2AccessToken.BEARER_TYPE + " " + access_token);
}
}
}
};
return requestInterceptor;
}
}
于是考虑使用ThreadLocal,代码中生成的Token存储在ThreadLocalMap中
@Slf4j
public class ThreadLocalTokenUtil {
private static final ThreadLocal<String> LOCAL = new ThreadLocal<String>();
private static CommonServiceImpl commonServiceImpl = (CommonServiceImpl) SpringContextUtil.getBean("commonServiceImpl");
/**
* @Description 根据usercode获取token 存入threadlocal 使用完后必须调用remove方法
* @author zhujun
* @date 2020/9/21 10:07
* @param userCode
* @return void
*/
public static String saveToken(String userCode){
String token = commonServiceImpl.getUserToken(userCode);
String threadName = Thread.currentThread().getName();
log.info("ThreadLocalTokenUtil_SAVE-threadName:{},usercode:{},token:{}",threadName,userCode,token);
LOCAL.set(token);
return token;
}
/**
* @Description 从ThreadLocal中获取 token
* @author zhujun
* @date 2020/9/21 10:08
* @param
* @return java.lang.String
*/
public static String getToken(){
String token = LOCAL.get();
return token;
}
/**
* @Description 清除ThreadLocal
* @author zhujun
* @date 2020/9/21 10:08
* @param
* @return void
*/
public static void remove(){
String token = LOCAL.get();
String threadName = Thread.currentThread().getName();
log.info("ThreadLocalTokenUtil_REMOVE_threadName:{},token:{}",threadName,token);
LOCAL.remove();
}
}
修改过滤器中的逻辑,默认先从TheadLocal中取Token,如果没用再从SecurityContext中获取
@Configuration
public class FeignInterceptorConfig {
//--调用其他fegin服务放行URL--
private final static List<String> permitAllList = Lists.newArrayList();
static {
permitAllList.add("/v1/identity/verifyFactors");
permitAllList.add("/v1/identity/getIdentityStatusByMsg");
}
@Bean
public RequestInterceptor requestInterceptor() {
RequestInterceptor requestInterceptor = new RequestInterceptor() {
@Override
public void apply(RequestTemplate template) {
String url = template.url();
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
//优先取ThreadLocal中的
String access_token = ThreadLocalTokenUtil.getToken();
if(StringUtils.isNotEmpty(access_token)){
template.header("Authorization", OAuth2AccessToken.BEARER_TYPE + " " + access_token);
}else if (authentication != null && !permitAllList.contains(url)) {
if (authentication instanceof OAuth2Authentication) {
OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) authentication.getDetails();
access_token = details.getTokenValue();
template.header("Authorization", OAuth2AccessToken.BEARER_TYPE + " " + access_token);
}
}
}
};
return requestInterceptor;
}
}
因为Tomcat容器中的线程也是循环使用的所以
注意使用完之后需要清除ThreadLocal,1是防止出现内存溢出,2是如果部分不需要登录的操作线程中存在用户Token也安全
起初考虑在业务代码的最末尾手动调用ThreadLocal.remove(),需要try catch + finally对所有代码改动较大,考虑使用过滤器实现
@WebFilter(urlPatterns = "/*", filterName = "threadLocalnterceptor")
@Order(2)
@Slf4j
public class ThreadLocalnterceptor implements HandlerInterceptor {
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
if(StringUtils.isNotEmpty(ThreadLocalTokenUtil.getToken())){
ThreadLocalTokenUtil.remove();
}
}
}