ThreadLoacl,InheritableThreadLocal,原理,以及配合线程池使用的一些坑

Posted 高因咖啡

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ThreadLoacl,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修改以后的壳的属性.

 

加大线程池,以满足每个线程方法独立使用一个线程可以解决以上问题.但是谁能预测一共会有多少个线程同时执行呢?

 

以上是关于ThreadLoacl,InheritableThreadLocal,原理,以及配合线程池使用的一些坑的主要内容,如果未能解决你的问题,请参考以下文章

Threadlocal

多租户数据库实现解决方案汇总

当InheritableThreadLocal遇到线程池:主线程本地变量修改后,子线程无法读取到新值