InheritableThreadLocal线程复用
引自:http://www.cnblogs.com/sweetchildomine/p/6575666.html
虽然使用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(壳)的时候,没有办法可以保证.