3 ThreadLocal-手写TransmittableThreadLocal

TTL的原理

为了解决InheritableThreadLocal无法处池内线程ThreadLocal传递问题。

线程池ThreadLocal的特殊性

池线程中的ThreadLocal,和父子线程间的ThreadLocal传递有一定的特殊性:

  1. 池内线程有自己的ThreadLocal。
    a. 线程池可以指定ThreadFactory,通过ThreadFactory创建的Thread,可以设置自己的ThreadLocal。
    b. 池内线程创建时,同样是业务线程触发,因此业务线程可以看做池线程的父线程,因此业务线程的InheritableThreadLocal,同样会被复制到池线程中。
  2. 不同任务的ThreadLocal存在不一致性。例如:业务线程A的CurrentUserThreadLocal存储的是用户甲,业务线程B的CurrentUserThreadLocal存储的事用户乙。

ThreadLocal的传递时机

线程池中线程的执行过程分析,唯一可以变动ThreadLocal的时机,只有run方法,因为run方法是业务唯一可以控制的代码。当然这也是一句废话,因为池内线程只处理run方法。

因此大致逻辑如下:

  1. 提交业务线程时,备份该业务线程的ThreadLocal。
  2. 池线程执行时,备份池线程的ThreadLocal。
  3. 根据步骤一的备份,恢复备份数据到池线程中,完成业务ThreadLocal到池线程ThreadLocal的传递。
  4. 根据步骤二的备份,复原池线程的ThreadLocal。

为什么备份业务线程的ThreadLocal?
因为业务线程可能会消亡,消亡后,是无法获取ThreadLocal对应的值,因此要备份。

为什么要备份池线程的ThreadLocal,执行完成后,还有复原,而不是直接移除掉。
因为线程池的一个拒绝策略是:callerRun,当触发后,其实执行run方法的不再是池线程,而是业务线程本身,如果移除掉,就会导致业务线程后续无法再次获取。

自定义TTL

单一ThreadLocal传递

只需要传递CurrentUser,那么可以自定义如下。

public class CurrentUserRunnable implements Runnable {
    private String currentUser;
    private Runnable runnable;

    @Override
    public void run() {
        try {
            //设置池线程的currentUser
            CurrentUserUtil.set(currentUser);
            //真正的执行run方法
            runnable.run();
        } finally {
            //移除池线程的CurrentUser
            CurrentUserUtil.remove();
        }
    }

    public CurrentUserRunnable(Runnable runnable) {
        this.runnable = runnable;
        //备份 业务线程的CurrentUser
        currentUser = CurrentUserUtil.get();
    }

}

CurrentUserUtil就是对    
private static ThreadLocal<String> currentUserTL = new ThreadLocal<>();
做了包装,提供了静态get、set、remove方法,就不做展示。



运行Demo
public class CurrentUserDemo {
    public static void main(String[] args) {
        CurrentUserUtil.set("jkf");
        ExecutorService executorService = Executors.newFixedThreadPool(1);
        executorService.execute(new CurrentUserRunnable(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + ":" + CurrentUserUtil.get());
            }
        }));
    }
}
输出结果:
pool-1-thread-1:jkf

当然上述也存在问题:run方法的finally移除了currentUser,如果触发calllerRun拒绝策略,就会导致业务线程再也无法获取CurrentUser。

稍微的优化

添加ExecutorThreadUtil,包装:

private static ThreadLocal<Boolean> executorThreadTL= new ThreadLocal() {
        protected Boolean initialValue() {
            return false;
        }
    };



修改CurrentUserDemo,创建executorService后,添加ThreadFactory
public class CurrentUserDemo {
    public static void main(String[] args) {
       
        //设置创建ThreadFactory,表明当前线程为池线程。
        ((ThreadPoolExecutor) executorService).setThreadFactory(new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                Thread t = new Thread(r);
                //表明当前线程是池线程
                ExecutorThreadUtil.set();
                return t;
            }
        });
    }
}

修改CurrentUserRunnable的finall
//当前线程是池线程
            if(ExecutorThreadUtil.isExecutorThread()){
                //移除池线程的CurrentUser
                CurrentUserUtil.remove();
            }

通过ExecutorThreadUtil标明当前线程是否是池线程,当触发callerRun时,会给当前线程的ThreadLocalMap中,添加executorThreadTL,value=false,会稍微占用一些内存。

缺点:上述方案只能针对特定的ThreadLocal,进行处理,例如:需要传递CurrentOrg,那么就需要有CurrentOrgRunnable;如果要传递CurrentUser和CurrentOrg,那么还需要特殊处理,当然上述情况可以提取一个ThreadLocalRunnable,把ThreadLocal数组作为入参。

多ThreadLocal传递

刚才说了单一ThreadLocal传递的方案,但是针对多ThreadLoal传递的情况,需要抽象一个ThreadLocalRunnable,具体优化如下:

public class ThreadLocalRunnable implements Runnable {
	//存储业务的ThreadLocal对应的值。
    private Map<ThreadLocal, Object> tls;
    private Runnable runnable;

    /**
     * 创建任务时,把需要的ThreadLocal全部传递进来
     */
    public ThreadLocalRunnable(Runnable run, List<ThreadLocal> allThreadLocal) {
        this.runnable = run;
        //备份业务的ThreadLocal
        backupThreadLocal(allThreadLocal);
    }

    @Override
    public void run() {
        try {
        	//恢复业务的ThreadLocal值到池线程中
            recoveryThreadLocal();
            //真正的执行run方法
            runnable.run();
        } finally {
        	//移除恢复的ThreadLocal
            removeThreadLocal();
        }
    }

    /**
     * 备份传递进来的ThreadLocal
     */
    private void backupThreadLocal(List<ThreadLocal> allThreadLocal) {
        if (!CollectionUtils.isEmpty(allThreadLocal)) {
            for (ThreadLocal tl : allThreadLocal) {
                //备份 业务线程的Tl的value
                tls.put(tl, tl.get());
            }
        }
    }

    /**
     * 恢复备份的业务ThreadLocal到池线程内
     */
    private void recoveryThreadLocal() {
        if (ExecutorThreadUtil.isExecutorThread()) {
            //池线程,恢复业务线程的ThreadLocal
            for (Map.Entry<ThreadLocal, Object> entry : tls.entrySet()) {
                //此时的Thread,是池线程
                entry.getKey().set(entry.getValue());
            }
        }
    }

    /**
     * 移除池线程中的业务ThreadLocal,防止ThreadLocal数据泄漏
     */
    private void removeThreadLocal() {
        if (ExecutorThreadUtil.isExecutorThread()) {
            //池线程,移除恢复的业务ThreadLocal
            for (Map.Entry<ThreadLocal, Object> entry : tls.entrySet()) {
                entry.getKey().remove();
            }
        }
    } 
}

通过上述优化,解决了多个ThreadLocal传递的问题,同时优化了代码结构,但是最严重的一个问题没有解决,即:run方法执行期间,你不知道需要业务线程的哪些ThreadLocal,尤其是当run方法内部,调用了其他服务,不确定用到哪些,就不确定构造函数的入参,run方法存在获取ThreadLocal对应值为null的风险。

最好是可以直接把业务线程的所有ThreadLocal全部备份,这样就不需要考虑run方法中需要哪些业务线程的ThreadLocal。
即修改ThreadLocalRunnable的backupThreadLocal方法。

private void backupThreadLocal() {
        Map<ThreadLocal, Object> allThreadLocals = Thread.currentThread().threadLocalMap;
        if (!CollectionUtils.isEmpty(allThreadLocals)) {
            for (Map.Entry<ThreadLocal, Object> entry : allThreadLocals.entrySet()) {
                //备份 业务线程的Tl的value
                tls.put(entry.getKey(), entry.getValue());
            }
        }
    }

即创建任务时,直接获取当前线程的所有ThreadLocal,这样就不需要创建任务时,把业务的ThreadLocal作为入参了。

上述backupThreadLocal方法是理想中的好方法,但是Thread的ThreadLocalMap不允许跳过ThreadLocal获取值,因此可以把优化点放在:获取当前线程的所有ThreadLocal

ThreadLocalMap暴露访问

暴露ThreadLoalMap,

  1. 继承Thread(直接访问)
    通过ThreadLocalMap的源码查看,ThreadLocalMap的访问权限是:default,只允许包内类访问,不允许子类访问,因此通过继承Thread访问ThreadLocalMap行不通。
  2. 继承ThreadLocal(间接访问)
    通过继承ThreadLocal,重写set、get、remove方法,保存ThreadLocal变量,通过变量访问值,间接的获取当前线程的所有ThreadLocal。

自定义MyTTL

通过上述分析,只能通过继承ThreadLocal,持有所有的ThreadLocal才能实现间接的访问ThreadLocalMap。

public class MyTTL<V> extends ThreadLocal<V> {

    //1. 继承ThreadLocal,当有set/get/remove操作时,更新MyTTL记录(增删)。
    //2. 为了持有所有MyTTL变量,因此需要是静态变量,保证记录是唯一一份。
    //3. Set集合:保证MyTTL唯一,MyTTL变量可能多次set/get/remove操作
    //4. 通过ThreadLocalsHolder持有线程的所有TTL变量,这样可以通过访问threadLocalsHolder
    // 获取线程所有的MyTTL变量。
    //5. 因为MyTTL继承ThreadLocal支持多线程,因此通过ThreadLocal区分不同线程的所有ThreadLocal
    //6. threadLocalsHolder持有了所有线程的所有MyTTL变量
    private static ThreadLocal<Set<MyTTL>> threadLocalsHolder = new ThreadLocal() {
        /**初始值*/
        protected Set<MyTTL> initialValue() {
            return new HashSet<>();
        }
    };

    /**
     * 设置数据,因为是继承ThreadLocal,因此需要先设置父类
     *
     * @param v
     */
    public void set(V v) {
        super.set(v);
        if (v == null) {
            //MyTTL值设置为null,相当于移除MyTTL。
            removeHolder();
        }
        //添加
        add();
    }

    /**
     * 记录ThreadLocal
     */
    private void add() {
        threadLocalsHolder.get().add(this);
    }

    /**
     * 获取数据
     */
    public V get() {
        V v = super.get();
        if (v == null) {
            removeHolder();
        }
        return v;
    }

    private void removeHolder() {
        threadLocalsHolder.get().remove(this);
    }

    /**
     * 生成当前Thread的所有ThreadLocal的备份(快照)
     */
    public static Map<MyTTL, Object> backup() {
        Set<MyTTL> currentTls = threadLocalsHolder.get();
        if (currentTls.isEmpty()) {
            return new HashMap<>();
        }
        Map<MyTTL, Object> backup = new HashMap<>();
        for (MyTTL ttl : currentTls) {
            backup.put(ttl, ttl.get());
        }
        return backup;
    }

    /**
     * 根据ThreadLocals,恢复当前线程
     */
    public static void resetThreadLocal(Map<MyTTL, Object> allThreadLocals) {
        //重建MyTTL
        for (Map.Entry<MyTTL, Object> entry : allThreadLocals.entrySet()) {
            MyTTL tl = entry.getKey();
            //当前线程添加TTL
            tl.set(entry.getValue());
        }
        //移除不存在的MyTTL
        for (MyTTL myTTL : threadLocalsHolder.get()) {
            if (!allThreadLocals.containsKey(myTTL)) {
                threadLocalsHolder.get().remove(myTTL);
            }
        }
    }
}

上述代码还存在一定的缺陷:threadLocalsHolder强应用MyTTL,在ThreadLocal章节中,就已经提到ThreadLocalMap的key是弱引用,防止强引用导致ThreadLocal对象无法回收,而threadLocalsHolder现在就是强引用MyTTL对象,也存在上述问题,因此threadLocalsHolder的Set中key要改为弱引用,但是没有WeakHashSet,只有WeakHashMap,又因为WeakHashMap也能实现set效果,因此直接使用WeakHashMap代替。

优化MyTTL的强引用

public class MyTTL<V> extends ThreadLocal<V> {

    //1. 继承ThreadLocal,当有set/get/remove操作时,更新MyTTL记录(增删)。
    //2. 为了持有所有MyTTL变量,因此需要是静态变量,保证记录是唯一一份。
    //3. Set集合:保证MyTTL唯一,MyTTL变量可能多次set/get/remove操作
    //4. 通过ThreadLocalsHolder持有线程的所有TTL变量,这样可以通过访问threadLocalsHolder
    // 获取线程所有的MyTTL变量。
    //5. 因为MyTTL继承ThreadLocal支持多线程,因此通过ThreadLocal区分不同线程的所有ThreadLocal
    //6. threadLocalsHolder持有了所有线程的所有MyTTL变量
    //7. 为了解决threadLocalsHolder强引用MyTTL对象,导致MyTTL对象无法回收的问题,
    //   修改为弱引用,又因为不存在WeakHashSet,只好用WeakHashMap代替,但是相关的value设置为null
    private static ThreadLocal<Map<MyTTL<?>, ?>> threadLocalsHolder = new ThreadLocal() {
        /**初始值*/
        protected WeakHashMap<MyTTL<?>, ?> initialValue() {
            return new WeakHashMap();
        }
    };

    /**
     * 设置数据,因为是继承ThreadLocal,因此需要先设置父类
     *
     * @param v
     */
    public void set(V v) {
        super.set(v);
        if (v == null) {
            //MyTTL值设置为null,相当于移除MyTTL。
            removeHolder();
        }
        //添加
        add();
    }

    /**
     * 记录ThreadLocal
     */
    private void add() {
        //实现Set效果
        if (!threadLocalsHolder.get().containsKey(this)) {
            threadLocalsHolder.get().put(this, null);
        }
    }

    /**
     * 获取数据
     */
    public V get() {
        V v = super.get();
        if (v == null) {
            removeHolder();
        }
        return v;
    }
    
	 /**
     * 移除
     */
    public void remove() {
        super.remove();
        removeHolder();
    }

    private void removeHolder() {
        threadLocalsHolder.get().remove(this);
    }

    /**
     * 生成当前Thread的所有ThreadLocal的备份(快照)
     */
    public static Map<MyTTL, Object> backup() {
        Map<MyTTL<?>, ?> currentTls = threadLocalsHolder.get();
        if (currentTls.isEmpty()) {
            return new HashMap<>();
        }
        Map<MyTTL, Object> backup = new HashMap<>();
        for (Map.Entry<MyTTL<?>, ?> entry : currentTls.entrySet()) {
            backup.put(entry.getKey(), entry.getKey().get());
        }
        return backup;
    }

    /**
     * 根据ThreadLocals,恢复当前线程
     */
    public static void resetThreadLocal(Map<MyTTL, Object> allThreadLocals) {
        //重建MyTTL
        for (Map.Entry<MyTTL, Object> entry : allThreadLocals.entrySet()) {
            MyTTL tl = entry.getKey();
            //当前线程添加TTL
            tl.set(entry.getValue());
        }
        //移除不存在的MyTTL
        for (MyTTL myTTL : threadLocalsHolder.get().keySet()) {
            if (!allThreadLocals.containsKey(myTTL)) {
                threadLocalsHolder.get().remove(myTTL);
            }
        }
    }
}

优化MyTTL的继承

MyTTL继承自ThreadLocal,ThreadLocal本身是不能处理父子线程的传递问题,而InheritableThreadLocal可以实现父子线程间的传递,因为MyTTL继承修改为InheritableThreadLocal。

public class MyTTL<V> extends InheritableThreadLocal<V> {}

这样TTL既支持父子线程的传递,也支持线程池的线程复用。

MyTTLRunnbale

在ThreadLocal的传递时机中,描述了如何传递的问题,因此还需要特殊的Runnable接口配合MyTTL才能实现传递功能。

public class MyTTLRunnable implements Runnable {
    //实际的run方法
    private Runnable runnable;
    //存储业务线程的所有MyTTL,此时可以看做是快照
    //业务线程后续再添加的MyTTL,run方法方位时,仍然为null
    private Map<MyTTL, Object> bizThreadLocals= new HashMap<>();

    public MyTTLRunnable(Runnable runnable) {
        this.runnable = runnable;
        //备份业务线程的MyTTL
        bizThreadLocals= MyTTL.backup();
    }

    @Override
    public void run() {
        //备份池线程的MyTTL
        Map<MyTTL, Object> currentThreadBackup = MyTTL.backup();
        try {
            //恢复业务MyTTL到池线程中
            MyTTL.resetThreadLocal(bizThreadLocals);
            runnable.run();
        } finally {
            //恢复池线程的MyTTL
            MyTTL.resetThreadLocal(currentThreadBackup);
        }
    }

    public static MyTTLRunnable of(Runnable runnable) {
        return new MyTTLRunnable(runnable);
    }
}

测试

public class MyTTLDemo {
    private static MyTTL<String> currentUser = new MyTTL<>();
    private static ExecutorService executorService = Executors.newFixedThreadPool(1);

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            //模拟10个请求
            String user = "user_" + i;
            currentUser.set(user);
            executorService.execute(MyTTLRunnable.of(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + ":" + currentUser.get());
                }
            }));
        }
        executorService.shutdown();
    }
}

输出:
pool-1-thread-1:user_0
pool-1-thread-1:user_1
pool-1-thread-1:user_2
pool-1-thread-1:user_3
pool-1-thread-1:user_4
pool-1-thread-1:user_5
pool-1-thread-1:user_6
pool-1-thread-1:user_7
pool-1-thread-1:user_8
pool-1-thread-1:user_9

启动只有一个线程的线程池,输出了不同的请求的用户信息,这就表明MyTTL支持线程池的复用。

优化

上述测试是普通线程池运行MyTTLRunnable,如果需要使用MyTTL,就需要变动普通的Runnable为MyTTLRunnable,改动可能很大,因此可以换个角度:MyTTL线程池,运行普通Runnable。

public class TTLExecutorService extends ThreadPoolExecutor {

    public TTLExecutorService(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
    }

    /**
     * 重写execute,内部把Runnable替换为MyTTLRunnable
     */
    public void execute(Runnable command) {
        super.execute(MyTTLRunnable.of(command));
    }
}

继承ThreadPoolExecutor,重新execute方法,这样使用就比较方便。

个人思考

池线程的MyTTL恢复,有两种方案:1 备份恢复。2 用完删除。

  1. 备份恢复
    先备份池线程的MyTTL,恢复业务MyTTL到池线程,然后再根据备份的MyTTL,恢复池线程的TTL。这样可以完整的保证池线程的MyTTL正确性。
  2. 用完删除:恢复业务MyTTL到池线程,完成后,移除池线程的业务MyTTL。
    方案一:存在数据copy和替换的问题,不过都是浅拷贝,速度还可控。
    方案二存在严重的后果:线程池存在callerRun拒绝策略,如果触发,此时的池线程却是业务线程本身,如果移除了业务MyTTL,就导致业务线程后续无法继续使用。

但是如果能区分出池线程和业务线程,那么方案二是否可行?

业务TTL用完删除

其他与MyTTLRunnable一致
public class MyTTLRunnableTwo implements Runnable {
    @Override
    public void run() {
        try {
            //恢复业务MyTTL到池线程中
            MyTTL.resetThreadLocal(bizThreadLocals);
            runnable.run();
        } finally {
            //删除业务TTL
            removeBizMyTTL(bizThreadLocals);
        }
    }

    private void removeBizMyTTL(Map<MyTTL, Object> bizThreadLocals) {
        if (ExecutorThreadUtil.isExecutorThread()) {
            //只有池线程才会移除
            for (MyTTL myTTL : bizThreadLocals.keySet()) {
                myTTL.remove();
            }
        }
    }   
}

核心是如何ExecutorThreadUtil.isExecutorThread()。线程池是支持ThreadFactory,那么是否可以通过ThreadFactory创建线程时标明。

public class MyTTLRunnableTwoDemo {
    private static MyTTL<String> currentUser = new MyTTL<>();
    private static ExecutorService executorService;

    static {
        executorService = Executors.newFixedThreadPool(1);
        //线程池指定ThreadFactory
        ((ThreadPoolExecutor) executorService).setThreadFactory(new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                Thread t = new Thread(r);
                //当前线程设置为池线程
                ExecutorThreadUtil.set();
                t.setName("pool_" + t.getName());
                return t;
            }
        });
    }

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            //模拟10个请求
            String user = "user_" + i;
            currentUser.set(user);
            executorService.execute(MyTTLRunnableTwo.of(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + ":" + currentUser.get());
                }
            }));
        }
        executorService.shutdown();
    }
}

输出:
pool_Thread-0:user_0
pool_Thread-0:user_1
pool_Thread-0:user_2
pool_Thread-0:user_3
pool_Thread-0:user_4
pool_Thread-0:user_5
pool_Thread-0:user_6
pool_Thread-0:user_7
pool_Thread-0:user_8
pool_Thread-0:user_9

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值