使用InheritableThreadLocal和ForkJoinPool踩坑分析

双十一的战场还是会炸出很多宝贝疙瘩的,出生在捡漏村的我开心坏了,内网某团队的踩坑记录,文章分析由于结合业务与技术选型等上下文,所以分析路径及内容比较复杂,我们只关心这里面的技术坑,然后学习其中的知识,把自己的理解记录并分享

需求背景

预热缓存,为了加快响应,在接口中按照租户对数据分片,然后每个分片并发执行数据预热。

代码实现

import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ForkJoinPool;

/**
 * @author 会灰翔的灰机
 * @date 2020/12/8
 */
public class ForkJoinTest {

    public static void main(String[] args) throws Exception {
        System.out.println("Main thread start");
        ForkJoinPool forkJoinPool = new ForkJoinPool(8);
        InheritableThreadLocal<String> threadLocal = new InheritableThreadLocal<>();
        // 每个租户的数据,暂定写死咯
        List<String> data = Arrays.asList("A", "B", "C", "D");
        // 3个租户
        for (String s : new String[]{"DAWN", "IS", "PIG"}) {
            System.out.println(Thread.currentThread().getName() + " " + s + "---" + threadLocal.get());
            // 1. 按照租户分片
            forkJoinPool.submit(() -> {
                System.out.println(Thread.currentThread().getName() + " start");
                try {
                    // 2. 设置租户ID,对每个并发线程共享
                    threadLocal.set(s);
                    // 3. 并发执行业务逻辑:根据租户ID获取需要预热的数据,进行缓存预热
                    data.stream().parallel().forEach(t -> {
                        System.out.println(Thread.currentThread().getName() + "---" + t + "---" + threadLocal.get());
                        try {
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    });
                } finally {
                    // 4. 清理租户ID
                    System.out.println(Thread.currentThread().getName() + " end and clear threadLocal");
                    threadLocal.remove();
                }
            }).get();
        }
        System.out.println("Main thread end");
    }
}

问题描述

代码实现看上去平平无奇,却隐藏着一个大坑,该代码运行结果出现租户乱序问题,极端情况下可能出现:共12个线程,有10个线程处理一个租户的数据,两个线程分别处理剩余两个租户的数据。糟糕透顶,如果剩余的两个租户是大客户,那岂不是要预热到天荒地老

问题分析

先看代码执行结果

Main thread start
main DAWN---null
ForkJoinPool-1-worker-1 start
ForkJoinPool-1-worker-1---C---DAWN
ForkJoinPool-1-worker-3---D---DAWN
ForkJoinPool-1-worker-2---B---DAWN
ForkJoinPool-1-worker-4---A---DAWN
ForkJoinPool-1-worker-1 end and clear threadLocal
main IS---null
ForkJoinPool-1-worker-1 start
ForkJoinPool-1-worker-1---C---IS
ForkJoinPool-1-worker-3---A---DAWN
ForkJoinPool-1-worker-2---D---DAWN
ForkJoinPool-1-worker-4---B---DAWN
ForkJoinPool-1-worker-1 end and clear threadLocal
main PIG---null
ForkJoinPool-1-worker-1 start
ForkJoinPool-1-worker-1---C---PIG
ForkJoinPool-1-worker-4---B---DAWN
ForkJoinPool-1-worker-3---A---DAWN
ForkJoinPool-1-worker-2---D---DAWN
ForkJoinPool-1-worker-1 end and clear threadLocal
Main thread end

Process finished with exit code 0

根据执行结果可以得出执行顺序如下

DAWN分片

  1. worker-1开始执行
  2. 设置ThreadLocal=DAWN,当前线程为worker-1
  3. 并发流开始执行,共四份数据,并发4个线程,当前线程+3个新线程
  4. 构建3个新线程执行:worker-2,worker-3,worker-4
  5. 3个新线程ThreadLocal均继承自父线程worker-1=DAWN
  6. 等待3个新线程执行完毕
  7. worker-1执行完成
  8. 清除ThreadLocal,当前线程为worker-1

IS分片

  1. worker-1开始执行
  2. 设置ThreadLocal=IS,当前线程为worker-1
  3. 并发流开始执行,共四份数据,并发4个线程,当前线程+3个新线程
  4. 线程池中获取3个线程执行:worker-2,worker-3,worker-4,3个线程ThreadLocal未发生变更依然为DAWN
  5. 等待3个线程执行完毕
  6. worker-1执行完成
  7. 清除ThreadLocal,当前线程为worker-1

PIG分片

同IS分片

线程与ThreadLocal层级

1

线程与ThreadLocal映射

2

解决方案

问题定位后解决方案也就简单了

  1. parallel流中进行手工设置ThreadLocal
  2. 直接不需要使用ThreadLocal,通过构建线程任务,在线程任务构造器中透传分片ID(例如:DAWN)

总结

java stream流并发线程池

java stream流并发执行任务会提交至当前线程所在的线程池中。如果当前线程没有线程池,则使用默认的内部线程池:ForkJoinPool.commonPool,可以看到并发流底层线程池使用的也是ForkJoinPool模型。验证代码如下:

public static void main(String[] args) throws Exception {
    System.out.println("Main thread start");
    ForkJoinPool forkJoinPool = new ForkJoinPool(8);
    InheritableThreadLocal<String> threadLocal = new InheritableThreadLocal<>();
    // 每个租户的数据,暂定写死咯
    List<String> data = Arrays.asList("A", "B", "C", "D");
    // 3个租户
    for (String s : new String[]{"DAWN", "IS", "PIG"}) {
        System.out.println(Thread.currentThread().getName() + " " + s + "---" + threadLocal.get());
        // 1. 按照租户分片
                data.stream().parallel().forEach(t -> {
                    System.out.println(Thread.currentThread().getName() + "---" + t + "---" + threadLocal.get());
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                });
    }
    System.out.println("Main thread end");
}

执行结果

Main thread start
main DAWN---null
main---C---null
ForkJoinPool.commonPool-worker-2---B---null
ForkJoinPool.commonPool-worker-1---A---null
ForkJoinPool.commonPool-worker-3---D---null
main IS---null
main---C---null
ForkJoinPool.commonPool-worker-2---B---null
ForkJoinPool.commonPool-worker-1---D---null
ForkJoinPool.commonPool-worker-3---A---null
main PIG---null
main---C---null
ForkJoinPool.commonPool-worker-2---B---null
ForkJoinPool.commonPool-worker-1---A---null
ForkJoinPool.commonPool-worker-3---D---null
Main thread end

Process finished with exit code 0

java stream流并发执行线程分配

  1. 自定义线程池,直接将所有数据丢到自定义线程池中执行。
  2. 默认线程池,首次执行任务时会复用主线程main,(所有数据数量-1)个任务丢到线程池中执行。之后会全部丢到线程池中执行
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值