zoukankan      html  css  js  c++  java
  • 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();
            }
        }
    }
    
  • 相关阅读:
    自己动手用Javascript写一个无刷新分页控件
    自己动手写一个通用的分页存储过程(适用于多表查询)
    Towards Accurate Multiperson Pose Estimation in the Wild 论文阅读
    统计学习方法c++实现之一 感知机
    2018百度之星开发者大赛-paddlepaddle学习(二)将数据保存为recordio文件并读取
    2018百度之星开发者大赛-paddlepaddle学习
    [转载]C#_Path类常用操作
    安装SQL2K是的文件挂起错误
    相见恨晚MySQL 多表查询
    php截取字符串,出现乱码
  • 原文地址:https://www.cnblogs.com/shinyrou/p/13743791.html
Copyright © 2011-2022 走看看