InheritableThreadlocal使用问题排查
背景
在做一个微服务系统的时候,我们的参数一般都是接在通过方法定义来进行传递的,类似这样
public void xxx(Param p, ...){
	// do something
}
然后这时有个模块,因为之前的设计原因,没有预留传递参数的形式,在本着尽可能不修改原来代码的情况下,决定通过InhertableThreadLocal来进行参数传递
InhertableThreadLocal
对于InhertableThreadLocal我们不陌生,其实它的思想是以空间来换取线性安全,对每个线程保留一份线程内私有的变量。
这个类一般是用于存在父子线程的情况下,那么在父子线程中,是怎么工作的?结合源码来简单认识下
下面这段代码是从jdk的Thread中摘取的,我们可以看到,每个被创建出来的线程,都有2个threadlocal,分别对应同名的类
    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;
    /*
     * InheritableThreadLocal values pertaining to this thread. This map is
     * maintained by the InheritableThreadLocal class.
     */
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
一开始的时候inheritableThreadLocals是null的,需要在InhertableThreadLocal调用createMap的时候来初始化。
createMap在setInitialValue()当中会被调用,而setInitialValue被get调用
// ThreadLocal.java
    private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return 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();
    }
// InheritableThreadLocal.java
    /**
     * Get the map associated with a ThreadLocal.
     *
     * @param t the current thread
     */
    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }
    /**
     * Create the map associated with a ThreadLocal.
     *
     * @param t the current thread
     * @param firstValue value for the initial entry of the table.
     */
    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
一般我们创建InheritableThreadLocal会重写初始化的方法,类似如下
ThreadLocal<Map<String,Integer>> context = new InheritableThreadLocal<Map<String,Integer>>(){
            @Override
            protected Map<String,Integer> initialValue() {
                System.out.println(Thread.currentThread().getName() + " init value");
                return new HashMap<>();
            }
        };
看到这里估计开始迷糊了,但是只要记住,父子线程的传递是通过ThreadLocal.ThreadLocalMap inheritableThreadLocals这个关键的成员变量来实现的。
上面讲的其实是父线程怎么创建这个成员变量,那么子线程怎么获取呢?
从线程池中创建线程,或者普通的创建线程,最终都会调用到这个方法
   private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
   		//前面省略
   		        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
 ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
   }
   		// 后面忽略
注意到这个变量了吗boolean inheritThreadLocals 这个就是决定是否是要继承父线程中的inheritableThreadLocals,前提自然是不能为null。
一般的线程new Thread()这个变量是true,也就是继承父线程中存放的变量。而线程池,默认使用DefaultThreadFactory的newThread(Runnable r)方法,也是如此
到这里就完成了传递,解释了为什么子线程可以得到父线程上set的变量了
回到问题开始
在简单的介绍完了如何实现变量的传递后,我们来看看一开始的问题,测试的代码如下
   @Test
    public void ParentChildThread(){
        ThreadLocal<Map<String,Integer>> context = new InheritableThreadLocal<Map<String,Integer>>(){
            @Override
            protected Map<String,Integer> initialValue() {
                System.out.println(Thread.currentThread().getName() + " init value");
                return new HashMap<>();
            }
        };
        final String TEST_KEY = "tt";
        class ChildThread implements Runnable{
            @Override
            public void run() {
                try{
                    System.out.println(Thread.currentThread().getName());
                    int a = context.get().get(TEST_KEY);;
                    System.out.println(a);
                }
                finally {
                    // 注意这里
                    context.remove();
                }
            }
        }
        ExecutorService executorService = Executors.newFixedThreadPool(1);
        String tname = Thread.currentThread().getName();
        int c = 0;
        try {
            while(c++ < 2) {
                System.out.printf("%s ======== %d ========\n", tname, c);
                System.out.println(Thread.currentThread().getName() + " set");
                // 第一次这里会触发createMap
                // 这里这里存放的是c
                context.get().put(TEST_KEY, c);
                executorService.execute(new ChildThread());
                System.out.println(Thread.currentThread().getName() + " remove");
                TimeUnit.MILLISECONDS.sleep(5000L);
                context.remove();
            }
            // 验证在线程池中remove会不会影响父线程的值,以此来判断是否需要在父线程中remove
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
main线程来模拟spring的线程池,因此需要放在一个循环中,重复的set和remove,子线程来模拟我在多线程环境下获取参数,因为在线程池中,所以需要记得remove,避免因为线程池复用的关系,而导致参数不对。
让我们来调试一下,输出的信息如下
Connected to the target VM, address: '127.0.0.1:46617', transport: 'socket'
main ======== 1 ========
main set
main init value
main remove
pool-1-thread-1
0
main ======== 2 ========
main set
main init value
main remove
pool-1-thread-1
pool-1-thread-1 init value
Exception in thread "pool-1-thread-1" java.lang.NullPointerException
	at com.cnc.core.utils.CommonUtilTest$1ChildThread.run(CommonUtilTest.java:43)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at java.lang.Thread.run(Thread.java:748)
从第一次的使用来看,ok,似乎没有问题,看第二次,怎么报错了,比较第一次和第二次,我们发现,因为在子线程中使用了remove,因此第二次需要重新进行初始化pool-1-thread-1 init value,毕竟我们已经remove了,所以肯定是需要重新初始化的,这个没有问题
注意到没这里线程池只有1个线程,这么做的原因是简化情景,因为实际的情况是32个线程,NPE的错误是在一定请求之后发生的
这个错误的发生,其实是在复用了之前的线程才出现的,也就是之前线程使用了remove后,就会出现这样的问题。why?
因为我们InheritableThreadLocal中存的是map,这个是父线程变量的拷贝
        class ChildThread implements Runnable{
            @Override
            public void run() {
                try{
                    System.out.println(Thread.currentThread().getName());
                    int a = context.get().get(TEST_KEY);;
                    System.out.println(a);
                }
                finally {
                    // 把这里注释掉
//                    context.remove();
                }
            }
        }
注释上面是保证不再出现异常,我们看看控制台输出
main ======== 1 ========
main set
main init value
pool-1-thread-1
1
main remove
main ======== 2 ========
main set
main init value
pool-1-thread-1
1
main remove
发现了没有,输出的始终是1,我们注意看main线程也有在remove,这其实是切断了与子线程的联系
解决措施
根据上面的分析我们知道了,父子线程通过inheritableThreadLocals来进行变量的共享,根据我们设置的容器是map,其实不需要调用remove,而只要把map的内容清空即可,效果是一样的,因此,下面这个可以实现我们的需求
context.remove(); --> context.get().clear()
运行测试,,这里我多测试了几个
main ======== 1 ========
main set
main init value
pool-1-thread-1
1
main remove
main ======== 2 ========
main set
pool-1-thread-1
2
main remove
main ======== 3 ========
main set
pool-1-thread-1
3
main remove
main ======== 4 ========
main set
pool-1-thread-1
4
main remove
main ======== 5 ========
main set
pool-1-thread-1
5
main remove
main ======== 6 ========
main set
pool-1-thread-1
6
main remove
InheritableThreadlocal使用问题排查的更多相关文章
- Tomcat shutdown执行后无法退出进程问题排查及解决
		问题定位及排查 上周无意中调试程序在Linux上ps -ef|grep tomcat发现有许多tomcat的进程,当时因为没有影响系统运行就没当回事.而且我内心总觉得这可能是tomcat像nginx一 ... 
- myrocks复制中断问题排查
		背景 mysql可以支持多种不同的存储引擎,innodb由于其高效的读写性能,并且支持事务特性,使得它成为mysql存储引擎的代名词,使用非常广泛.随着SSD逐渐普及,硬件存储成本越来越高,面向写优化 ... 
- Java线上应用故障排查之一:高CPU占用
		一个应用占用CPU很高,除了确实是计算密集型应用之外,通常原因都是出现了死循环. 以我们最近出现的一个实际故障为例,介绍怎么定位和解决这类问题. 根据top命令,发现PID为28555的Java进程占 ... 
- wordpress插件bug排查后记(记一次由于开启memecached引起的插件bug)
		这篇文章是写给自己的. 周三的时候我在维护公司的一个wordpress项目页面时发现了一个非常奇怪的情况:当我尝试更新网站上的一个页面后,在wordpress后台的编辑器中发现其内容并没有按我预期的将 ... 
- [AlwaysOn Availability Groups]AG排查和监控指南
		AG排查和监控指南 1. 排查场景 如下表包含了常用排查的场景.根据被分为几个场景类型,比如Configuration,client connectivity,failover和performance ... 
- mysql半同步复制问题排查
		1.问题背景 默认情况下,线上的mysql复制都是异步复制,因此在极端情况下,主备切换时,会有一定的概率备库比主库数据少,因此切换后,我们会通过工具进行回滚回补,确保数据不丢失.半同步复制则 ... 
- 数据库实战案例—————记一次TempDB暴增的问题排查
		前言 很多时候数据库的TempDB.日志等文件的暴增可能导致磁盘空间被占满,如果日常配置不到位,往往会导致数据库故障,业务被迫中断. 这种文件暴增很难排查,经验不足的一些运维人员可能更是无法排查具体原 ... 
- 一次xbuild编译失败的排查
		今天一个待上线服务测试完毕,需要构建CI,按照模板配置好包还原,xbuild编译,报错,错误信息如下: EtcdRegister.cs(8,15): error CS0234: The type or ... 
- 一次kibana服务失败的排查过程
		公司在kubernetes集群上稳定运行数月的kibana服务于昨天下午突然无法正常提供服务,访问kibana地址后提示如下信息: 排查过程: 看到提示后,第一反应肯定是检查elasticsearch ... 
随机推荐
- linux文件实时同步
			参考博客:https://www.cnblogs.com/MacoLee/p/5633650.html 一.文件同步很简单 服务端:被动的接收传输过来的数据 客户端:主动提供数据给服务端 安装思路:服 ... 
- 安卓qq视频动态名片制作器
			本软件来自互联网,仅供个人参考,严禁商业用途! 非常炫酷的diy动态名片教程,B格绝对高,内含软件教程代码,包会! 
- WindowsPhone8.1 开发-- 二维码扫描
			随着 WinRT 8.1 API 的发布,Windows 8.1 和 Windows Phone 8.1 (基于 WinRT) 应用程序的开发模型经历了戏剧性的收敛性.与一些特定于平台的考虑,我们现在 ... 
- abp.zero 9.0框架的前端Angular使用说明
			abp.zero 9.0框架的前端Angular使用说明 目录 abp.zero 9.0框架的前端Angular使用说明 摘要 1 部署及启动 1.1 依赖包安装 1.2 使用yarn安装依赖包 1. ... 
- [打基础]OI/ACM基本功&一些小功能的实现&一些错误(持续更新)
			基本功 前导0 如题,有时候需要把3输出成03这样子,可以调用 cout.width(x); ,x表示以几位,用 cout.fill(x); 来给出前导填充的内容,一般x以char的形式给出 例如可以 ... 
- spark踩坑--WARN ProcfsMetricsGetter: Exception when trying to compute pagesize的最全解法
			spark踩坑--WARN ProcfsMetricsGetter: Exception when trying to compute pagesize的最全解法 问题描述 大概是今年上半年的时候装了 ... 
- UWP ListView添加分割线
			先看效果: 我并没有找到有设置ListView分割线的属性 下面是一个比较简单的实现,如果有同学有更好的实现,欢迎留言,让我们共同进步.我的叙述不一定准确 实现的方法就是在DataTemplate里包 ... 
- 中小学生的噩梦:怎样用Python检测抄袭行为?广大中小学生们的美梦就此结束
			本教程将介绍如何使用机器学习技术(如word2vec和余弦相似度等),在Python中用几行代码制作抄袭检测器.搭建完成后,抄袭检测器将会从文件中载入学生们的作业,然后通过计算相似度来判断学生有无相互 ... 
- IIS本地部署局域网可随时访问的项目
			原理 在本机的IIS下创建一个网站,文件目录直接指向Web项目文件夹 步骤 1.项目的启动项目为web 2.在iis中创建一个新的网站(Work_TK_EIS) 文件目录为web项目的目录(D:\Gi ... 
- C#发送腾讯企业邮箱
			腾讯企业邮箱客户端配置介绍 http://email-qq.cn/tengxun/201610303793.html?akvezc=smt0n2 POP3/SMTP协议 POP3/SMTP协议: 接收 ... 
