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也是无法被回收的,从而导致内存泄露。

PS:Java中的强、弱、软、虚引用

解决方法:

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();
        }
    }
}
posted @ 2020-09-28 11:19  ShinyRou  阅读(717)  评论(0编辑  收藏  举报