多线程相关知识点

197 阅读3分钟

一:线程之间数据隔离

  • 在使用多线程的情况下,我们可能有这样的需求,每个线程之间的数据不共享,来了请求之后我开一个线程,准备好处理的数据放到一个地方,当前线程的后续操作都可以用到准备好的数据。 这个时候我们就可以使用ThreadLoacl来保存数据
public class ContextHolder {

    private static final ThreadLocal<ContextData> THREAD_LOCAL =
            new ThreadLocal<>();

    public static ContextData getContext() {
        return THREAD_LOCAL.get();
    }

    public static void setContext(ContextData value) {
        THREAD_LOCAL.set(value);
    }

    public static void clear() {
        THREAD_LOCAL.remove();
    }
}
  • 那么父子线程直接要传递数据怎么用呢,比如在一个线程中又开一个线程处理业务,这时候可以使用InheritableThreadLocal
public class ContextHolder {

    private static final InheritableThreadLocal<ContextData> THREAD_LOCAL =
            new InheritableThreadLocal<>();

    public static ContextData getContext() {
        return THREAD_LOCAL.get();
    }

    public static void setContext(ContextData value) {
        THREAD_LOCAL.set(value);
    }

    public static void clear() {
        THREAD_LOCAL.remove();
    }
}
  • 示例1 :但是InheritableThreadLocal 在使用线程池处理的时候会有坑,如下所示:
public static void main(String[] args) {
       ThreadPoolExecutor executor = new ThreadPoolExecutor(1,1,1,
               TimeUnit.MINUTES,new ArrayBlockingQueue<>(1));

       ThreadLocal local = new InheritableThreadLocal();
       local.set(1);

       executor.execute(()->{
           System.out.println("打印1:"+local.get());
       });
       local.set(2);

       System.out.println("打印2:"+local.get());

       executor.execute(()->{
           System.out.println("打印3:"+local.get());
       });
       new Thread(new Runnable() {
           @Override
           public void run() {
               System.out.println("打印4:"+local.get());
           }
       }).start();
   }

运行结果如下:

打印2:2
打印1:1
打印3:1
打印4:2

分析: 分析打印3为什么是1,InheritableThreadLocal的继承性是在new Thread创建子线程时候在构造函数内把父线程内线程变量拷贝到子线程内部的。 为了不在创建新线程耗费资源,我们一般会用线程池,线程池的线程会复用,那么线程中的ThreadLocal便不对了,可能是旧的,因为线程是旧的。

  • 示例2: InheritableThreadLocal 在使用线程池时失效的问题:
public class InheritableThreadLocalTest {
    
    private static final AtomicInteger ID_SEQ = new AtomicInteger();
    private static final ExecutorService THREAD_POOL = Executors.newFixedThreadPool(1, r -> new Thread(r, "thread-" + ID_SEQ.getAndIncrement()));
    private static Integer i = 0;
    private static final ThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();

    public static void main(String[] args) throws Exception {
        // 模拟接口调用
        IntStream.range(0, 10).forEach(InheritableThreadLocalTest::testInheritableThreadLocal);
        THREAD_POOL.shutdown();
    }

    public static void testInheritableThreadLocal(int s) {
        inheritableThreadLocal.set("小蚂蚁" + i++);
        Future<?> submit = THREAD_POOL.submit(new YypTask("任务" + s));
        try {
            submit.get();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static class YypTask implements Runnable{

        String taskName;

        public YypTask(String taskName) {
            this.taskName = taskName;
        }

        @Override
        public void run() {
            System.out.println(taskName + "线程:" + Thread.currentThread().getName() + "获取到的值: " + inheritableThreadLocal.get());
            inheritableThreadLocal.remove();
        }
    }

结果如下: image.png

可以看到我们在获取到第一次的值后就再也获取不到后面的InheritableThreadLocal的值了。正确的运行结果应该是如下的:

image.png

为什么会出现这种错误呢?原因很简单,InheritableThreadLocal只有在创建子线程的时候才会传递值。而线程池由于线程创建后会一直复用,所以导致后面获取不到值。这里不妨上边线程池的最大线程数改成10试下,会有意想不到的结果。

  • 这里我们要实现线程池上线文的传递(值)就不得不引出我们今天的主角TransmittableThreadLocalTransmittableThreadLocal是阿里开源的线程池异步上线文传递的一个组件,并不是JDK自带的
public class TransmittableThreadLocalTest {
    private static final AtomicInteger ID_SEQ = new AtomicInteger();
    private static final ExecutorService EXECUTOR = Executors.newFixedThreadPool(1, r -> new Thread(r, "thread-" + ID_SEQ.getAndIncrement()));
    private static final TransmittableThreadLocal<String> ttl = new TransmittableThreadLocal<>();
    private static Integer i = 0;



    public static void main(String[] args) throws Exception {
        // 模拟接口调用
        IntStream.range(0, 10).forEach(TransmittableThreadLocalTest::testInheritableThreadLocal);
        EXECUTOR.shutdown();
    }


    public static void testInheritableThreadLocal(int s) {
        ttl.set("小蚂蚁技术" + i++);
        Future<?> submit = EXECUTOR.submit(TtlRunnable.get(new YypTask("任务" + s)));
        try {
            submit.get();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }

    public static class YypTask implements Runnable{

        String taskName;

        public YypTask(String taskName) {
            this.taskName = taskName;
        }

        @Override
        public void run() {
            System.out.println(taskName + "线程:" + Thread.currentThread().getName() + "获取到的值: " + ttl.get());
            ttl.remove();
        }
    }

}

这里我们要实现线程池上线文的传递(值)就不得不引出我们今天的主角TransmittableThreadLocalTransmittableThreadLocal是阿里开源的线程池异步上线文传递的一个组件,并不是JDK自带的

github地址

功能介绍(官网介绍):

TransmittableThreadLocal(TTL):在使用线程池等会池化复用线程的执行组件情况下,提供ThreadLocal值的传递功能,解决异步执行时上下文传递的问题。一个Java标准库本应为框架/中间件设施开发提供的标配能力,本库功能聚焦 & 0依赖,支持Java 17/16/15/14/13/12/11/10/9/8/7/6。     JDK的InheritableThreadLocal类可以完成父线程到子线程的值传递。但对于使用线程池等会池化复用线程的执行组件的情况,线程由线程池创建好,并且线程是池化起来反复使用的;这时父子线程关系的ThreadLocal值传递已经没有意义,应用需要的实际上是把 任务提交给线程池时的ThreadLocal值传递到 任务执行时     整个TransmittableThreadLocal库的核心功能(用户API与框架/中间件的集成API、线程池ExecutorService/ForkJoinPool/TimerTask及其线程工厂的Wrapper),只有 ~1000 SLOC代码行,非常精小

这里面有几个细节:

  1. TransmittableThreadLocal替换InheritableThreadLocal
  2. TtlRunnable增强JDK原始的Runnable接口

TransmittableThreadLocal使用方式大致如下。一些高级用法可能比如无侵入Agent接入等