zoukankan      html  css  js  c++  java
  • ThreadLoacl,InheritableThreadLocal,原理,以及配合线程池使用的一些坑

    虽然使用AOP可以获取方法签名,但是如果要获取方法中计算得出的数据,那么就得使用ThreadLocal,如果还涉及父线程,那么可以选择InheritableThreadLocal.

    注意:理解一些原理能够减少很多不可控问题,最简单的使用方式就是不要交给线程池处理.为了提高一点性能,而导致数据错误得不偿失.

    2018年4月12日 12:44:41更新 关于InheritableThreadLocal 配合线程池的问题解决方案 -> TransmittableThreadLocal 解决 线程池线程复用 无法复制 InheritableThreadLocal 的问题.

    首先看看ThreadLoacl如何做到共享变量实现为线程私有变量

    Thread源码里面,有一个ThreadLoaclMap

    ThreadLocal.ThreadLocalMap threadLocals = null;

    ThreadLoacl set方法源码

       public void set(T value) {
        //获取当前线程 Thread t
    = Thread.currentThread();
        //获取当前线程ThreadLoaclMap ThreadLocalMap map
    = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); }

    ThreadLoacl getMap方法源码

       ThreadLocalMap getMap(Thread t) {
            return t.threadLocals;
        }

    测试TreadLocal线程私有

    public class A {
        static final ThreadLocal<String> threadParam = new ThreadLocal<>();
    
        public static void main(String[] args) throws InterruptedException {
            //死循环,测多几次看结果
            while (true) {
                //线程1
                new Thread(() -> {
                    //设置参数
                    threadParam.set("abc");
                    //输出参数
                    System.out.println("t1:" + threadParam.get());
                    //看起来像是多余操作
    //                threadParam.remove();
                }).start();
                TimeUnit.SECONDS.sleep(1);
                new Thread(() -> {
                    //线程二,测试是否能获取abc
                    System.out.println("t2:" + threadParam.get());
                }).start();
            }
        }
    }

    测试结果

    线程1永远输出abc

    线程2永远输出null

    看起来很美好.但是也有需要注意的地方

    如果使用线程池,以下把线程交给线程池处理

    /**
     * 
     * @author ZhenWeiLai
     *
     */
    public class B {
        static final ThreadLocal<String> threadParam = new ThreadLocal<>();
    
        public static void main(String[] args) throws InterruptedException {
            //固定池内只有存活3个线程
            ExecutorService execService = Executors.newFixedThreadPool(3);
            //死循环几次才能看出效果
            while (true) {
                Thread t = new Thread(()->{
                        threadParam.set("abc");
                        System.out.println("t1:" + threadParam.get());
                        //如果不调用remove,将引发问题
    //                    threadParam.remove();
                });
                execService.execute(t);
                TimeUnit.SECONDS.sleep(1);
                Thread t2 = new Thread(()-> {
                        System.out.println("t2:" + threadParam.get());
                });
                execService.execute(t2);
            }
        }
    }

    测试结果:

    t1:abc
    t1:abc
    t2:null
    t2:abc  //因复用线程而导致问题
    t1:abc

    原因:线程池把线程提交到队列,当被调用的时候如果存在空闲线程就直接复用线程,仅仅是调用了用户提交的run方法.

    所以当ThreadLocal参数使用完,记得调用remove方法

    除了ThreadLocal 还有 InheritableThreadLocal,子线程可以共享父线程的InheritableThreadLocal

    InheritableThreadLocal实现的关键源码

     //初始化一个线程时,获取当前线程,作为父线程
     Thread parent = currentThread();
    //如果父线程inheritableThreadLocals 不为空时,子线程复制一份inheritableThreadLocals 
     if (parent.inheritableThreadLocals != null)
                this.inheritableThreadLocals =
                    ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

    测试代码

    /**
     * 
     * @author ZhenWeiLai
     *
     */
    public class A {
        static final InheritableThreadLocal<String> threadParam = new InheritableThreadLocal<>();
        public static void main(String[] args) throws InterruptedException {
            //死循环,测多几次看结果
            while (true) {
                //线程1,测试是否能获取父线程参数
                new Thread(() -> {
                    //设置参数
                    threadParam.set("abc");
                    //输出参数
                    System.out.println("t1:" + threadParam.get());
                    
                    //线程2,测试是否能获取线程1参数
                    new Thread(() -> {
                        System.out.println("t2:" + threadParam.get());
                        //为了测试线程三能否获得,这里先不删除
    //                    threadParam.remove();
                    }).start();
                }).start();
                
                TimeUnit.SECONDS.sleep(1);
                
                //线程3,测试是否能获取线程1参数
                new Thread(() -> {
                    System.out.println("t3:" + threadParam.get());
                }).start();
            }
        }
    }

     输出结果:自线程可以获取参数,非自线程不能获取.

    t1:abc
    t2:abc
    t1:abc
    t3:null
    t2:abc
    t3:null
    t1:abc
    t2:abc
    t3:null
    t1:abc
    t2:abc

    再一次看似很美好,以下写一个复杂点的,交给线程池执行

    package thread.base.threadloacl;
    
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.TimeUnit;
    
    /**
     * 
     * @author ZhenWeiLai
     *
     */
    public class B {
        static final InheritableThreadLocal<String> threadParam = new InheritableThreadLocal<>();
    
        public static void main(String[] args) throws InterruptedException {
            //固定池内只有存活3个线程
            ExecutorService execService = Executors.newFixedThreadPool(3);
            //死循环几次才能看出效果
            while (true) {
                //线程1,里面有两个子线程
                Thread t = new Thread(()->{
                        threadParam.set("abc");
                        System.out.println("t1:" + threadParam.get());
                        Thread t2 = new Thread(()->{
                            System.out.println("t2:" + threadParam.get());
    //                        threadParam.remove();
                        });
                        execService.execute(t2);
                        
                        Thread t3 = new Thread(()->{
                            System.out.println("t3:" + threadParam.get());
    //                        threadParam.remove();
                    });
                        execService.execute(t3);
                        
                });
                execService.execute(t);
                TimeUnit.SECONDS.sleep(1);
                //线程4,线程1同级
                Thread t4 = new Thread(()-> {
                    threadParam.set("CBA");
                        System.out.println("t4:" + threadParam.get());
                });
                execService.execute(t4);
            }
        }
    }

    输出结果:

    t1:abc
    t2:abc
    t3:abc
    t4:CBA
    t1:abc
    t2:abc
    t3:abc
    t4:CBA
    t1:abc
    t2:abc
    t3:CBA //因复用线程而导致问题
    t4:CBA

    Runnable只是线程方法,Thread才是线程,需要给Runnable加上一个线程的壳,调用start才会使用线程执行.

    这里线程池只存活3个线程,那么在线程池复用线程(壳)的时候,壳的属性只有在创建的时候才会被重新设置值(如果有操作的话,例如:InheritableThreadLocal,ThreadLocal).

    这些壳被创建好以后提交给了线程池,但是线程方法并没有马上执行,然后被其他壳修改了属性.当这个线程方法开始执行的时候,已经不是自己创建的壳了

    这里线程3,因为由于线程切换使用了被线程4修改以后的壳的属性.

    加大线程池,以满足每个线程方法独立使用一个线程只能保证第一次运行正确,因为没有涉及Thread重用的问题.但是如果涉及重用Thread(壳)的时候,没有办法可以保证.

    本篇问题还未结束...使用线程池如何Remove ThreadLocal参数...使用线程池其他提交方法结果也不一样等等,有空再补充.

     

  • 相关阅读:
    依赖反转Ioc和unity,autofac,castle框架教程及比较
    webform非表单提交时防xss攻击
    tfs分支操作
    防火墙入站出站规则配置
    前端流程图jsplumb学习笔记
    Js闭包学习笔记
    word中加入endnote
    Rest概念学习
    DRF的版本、认证、权限
    博客园自动生成目录
  • 原文地址:https://www.cnblogs.com/sweetchildomine/p/6575666.html
Copyright © 2011-2022 走看看