zoukankan      html  css  js  c++  java
  • 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,也就是继承父线程中存放的变量。而线程池,默认使用DefaultThreadFactorynewThread(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 ========
    ", 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
    
  • 相关阅读:
    javascript的基本语法
    javascript的初步认识
    就诊管理(数据结构小学期)
    软件工程课程总结
    每日学习(个人作业2)
    每日学习(个人作业2)
    每日学习(个人作业2)
    每日学习(个人作业2)
    Java中后端Bigdecimal传值到前端精度丢失问题
    这学期的加分项
  • 原文地址:https://www.cnblogs.com/westlin/p/11910503.html
Copyright © 2011-2022 走看看