TransmittableThreadLocal使用踩坑

news2024/11/20 4:33:07

背景:为了获取相关字段方便,项目里使用了TransmittableThreadLocal上下文,在异步逻辑中get值时发现并非当前请求的值,且是偶发状况(并发问题)。

发现:TransmittableThreadLocal是阿里开源的可以实现父子线程值传递的工具,其子线程必须使用TtlRunnable\TtlCallable修饰或者线程池使用TtlExecutors修饰(防止数据“污染”),如果没有使用装饰后的线程池,那么使用TransmittableThreadLocal上下文,就有可能出现线程不安全的问题。

参考代码:

封装的上下文,成员变量RequestHeader

package org.example.ttl.threadLocal;

import com.alibaba.ttl.TransmittableThreadLocal;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.apache.commons.lang3.ObjectUtils;

/**
 * description:
 * author: JohnsonLiu
 * create at:  2021/12/24  23:19
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class RequestContext {

    private static final ThreadLocal<RequestContext> transmittableThreadLocal = new TransmittableThreadLocal();


    private static final RequestContext INSTANCE = new RequestContext();
    private RequestHeader requestHeader;


    public static void create(RequestHeader requestHeader) {
        transmittableThreadLocal.set(new RequestContext(requestHeader));
    }

    public static RequestContext current() {
        return ObjectUtils.defaultIfNull(transmittableThreadLocal.get(), INSTANCE);
    }

    public static RequestHeader get() {
        return current().getRequestHeader();
    }

    public static void remove() {
        transmittableThreadLocal.set(null);
    }


    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    @ToString
    static class RequestHeader {
        private String requestUrl;
        private String requestType;
    }
}
获取上下文内容的case:
package org.example.ttl.threadLocal;

import com.alibaba.ttl.threadpool.TtlExecutors;

import java.util.concurrent.Executor;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * description: TransmittableThreadLocal正确使用
 * author: JohnsonLiu
 * create at:  2021/12/24  22:24
 * 验证结论:
 * 1.线程池必须使用TtlExecutors修饰,或者Runnable\Callable必须使用TtlRunnable\TtlCallable修饰
 * ---->原因:子线程复用,子线程拥有的上下文内容会对下次使用造成“污染”,而修饰后的子线程在执行run方法后会进行“回放”,防止污染
 */
public class TransmittableThreadLocalCase2 {
  
  // 为达到线程100%复用便于测试,线程池核心数1
  
  private static final Executor TTL_EXECUTOR = TtlExecutors.getTtlExecutor(new ThreadPoolExecutor(1, 1, 1000, TimeUnit.MICROSECONDS, new LinkedBlockingQueue<>(1000)));
  
  // 如果使用一般的线程池或者Runnable\Callable时,会存在线程“污染”,比如线程池中线程会复用,复用的线程会“污染”该线程执行下一次任务
  private static final Executor EXECUTOR = new ThreadPoolExecutor(1, 1, 1000, TimeUnit.MICROSECONDS, new LinkedBlockingQueue<>(1000));
  
  
  public static void main(String[] args) {
    
    RequestContext.create(new RequestContext.RequestHeader("url", "get"));
    System.out.println(Thread.currentThread().getName() + " 子线程(rm之前 同步):" + RequestContext.get());
    // 模拟另一个线程修改上下文内容
    EXECUTOR.execute(() -> {
      RequestContext.create(new RequestContext.RequestHeader("url", "put"));
    });
    
    // 保证上面子线程修改成功
    try {
      Thread.sleep(2000);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    
    // 异步获取上下文内容
    TTL_EXECUTOR.execute(() -> {
      try {
        Thread.sleep(2000);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      System.out.println(Thread.currentThread().getName() + " 子线程(rm之前 异步):" + RequestContext.get());
    });
    
    // 主线程修改上下文内容
    RequestContext.create(new RequestContext.RequestHeader("url", "post"));
    System.out.println(Thread.currentThread().getName() + " 子线程(rm之前 同步<reCreate>):" + RequestContext.get());
    
    // 主线程remove
    RequestContext.remove();
    
    // 子线程获取remove后的上下文内容
    TTL_EXECUTOR.execute(() -> {
      try {
        Thread.sleep(3000);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      System.out.println(Thread.currentThread().getName() + " 子线程(rm之后 异步):" + RequestContext.get());
    });
  }
}
使用一般线程池结果:

使用修饰后的线程池结果:

这种问题的解决办法:

如果大家跟我一样存在这样的使用,那么也会低概率存在这样的问题,正确的使用方式是:

子线程必须使用TtlRunnable\TtlCallable修饰或者线程池使用TtlExecutors修饰,这一点很容易被遗漏,比如上下文和异步逻辑不是同一个人开发的,那么异步逻辑的开发者就很可能直接在异步逻辑中使用上下文,而忽略装饰线程池,造成线程复用时的“数据污染”。

另外还有一种不同于上面的上下文用法,同样使用不当也会存在线程安全问题:

上代码样例

package org.example.ttl.threadLocal;

import com.alibaba.ttl.TransmittableThreadLocal;

import java.util.LinkedHashMap;
import java.util.Map;

/**
 * description: TransmittableThreadLocal正确使用
 * author: JohnsonLiu
 * create at:  2021/12/24  23:19
 */
public class ServiceContext {

    private static final ThreadLocal<Map<Integer, Integer>> transmittableThreadLocal = new TransmittableThreadLocal() {
        /**
         * 如果使用的是TtlExecutors装饰的线程池或者TtlRunnable、TtlCallable装饰的任务
         * 重写copy方法且重新赋值给新的LinkedHashMap,不然会导致父子线程都是持有同一个引用,只要有修改取值都会变化。引用值线程不安全
         * parentValue是父线程执行子任务那个时刻的快照值,后续父线程再次set值也不会影响子线程get,因为已经不是同一个引用
         * @param parentValue
         * @return
         */
        @Override
        public Object copy(Object parentValue) {
            if (parentValue instanceof Map) {
                System.out.println("copy");
                return new LinkedHashMap<Integer, Integer>((Map) parentValue);
            }
            return null;
        }

        /**
         * 如果使用普通线程池执行异步任务,重写childValue即可实现子线程获取的是父线程执行任务那个时刻的快照值,重新赋值给新的LinkedHashMap,父线程修改不会影响子线程(非共享)
         * 但是如果使用的是TtlExecutors装饰的线程池或者TtlRunnable、TtlCallable装饰的任务,此时就会变成引用共享,必须得重写copy方法才能实现非共享
         * @param parentValue
         * @return
         */
        @Override
        protected Object childValue(Object parentValue) {
            if (parentValue instanceof Map) {
                System.out.println("childValue");
                return new LinkedHashMap<Integer, Integer>((Map) parentValue);
            }
            return null;
        }

        /**
         * 初始化,每次get时都会进行初始化
         * @return
         */
        @Override
        protected Object initialValue() {
            System.out.println("initialValue");
            return new LinkedHashMap<Integer, Integer>();
        }
    };

    public static void set(Integer key, Integer value) {
        transmittableThreadLocal.get().put(key, value);
    }

    public static Map<Integer, Integer> get() {
        return transmittableThreadLocal.get();
    }

    public static void remove() {
        transmittableThreadLocal.remove();
    }
}

使用case:

package org.example.ttl.threadLocal;

import com.alibaba.ttl.threadpool.TtlExecutors;

import java.util.concurrent.Executor;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * description: TransmittableThreadLocal正确使用
 * author: JohnsonLiu
 * create at:  2021/12/24  22:24
 */
public class TransmittableThreadLocalCase {


    private static final Executor executor = TtlExecutors.getTtlExecutor(new ThreadPoolExecutor(1, 1, 1000, TimeUnit.MICROSECONDS, new LinkedBlockingQueue<>(1000)));
//    private static final Executor executor = new ThreadPoolExecutor(1, 1, 1000, TimeUnit.MICROSECONDS, new LinkedBlockingQueue<>(1000));

    static int i = 0;

    public static void main(String[] args) {

        ServiceContext.set(++i, i);

        executor.execute(() -> {
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " 子线程(rm之前):" + ServiceContext.get());
        });

        ServiceContext.set(++i, i);
        ServiceContext.remove();

        executor.execute(() -> {
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " 子线程(rm之后):" + ServiceContext.get());
        });
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1358489.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

鸿蒙系列--动态共享包的依赖与使用

一、前言 HarmonyOS的共享包相当于Android的Library&#xff0c;在HarmonyOS中&#xff0c;给开发者提供了两种共享包&#xff0c;HAR&#xff08;Harmony Archive&#xff09;静态共享包&#xff0c;和HSP&#xff08;Harmony Shared Package&#xff09;动态共享包 区别&…

记一次 .NET 某新能源材料检测系统 崩溃分析

一&#xff1a;背景 1. 讲故事 上周有位朋友找到我&#xff0c;说他的程序经常会偶发性崩溃&#xff0c;一直没找到原因&#xff0c;自己也抓了dump 也没分析出个所以然&#xff0c;让我帮忙看下怎么回事&#xff0c;那既然有 dump&#xff0c;那就开始分析呗。 二&#xff…

计算机创新协会冬令营——暴力枚举题目05

这道题挺基础但是挺多坑的。(•́へ•́╬) 题目 204. 计数质数 - 力扣&#xff08;LeetCode&#xff09; 给定整数 n &#xff0c;返回 所有小于非负整数 n 的质数的数量 。 示例 示例 1&#xff1a; 输入&#xff1a;n 10 输出&#xff1a;4 解释&#xff1a;小于 10 的质…

具有大电流,双通道 12V,短地短电源保护等功能的国产芯片GC8549 可替代ONSEMI的LV8548/LV8549

GC8549 可以工作在 3.8~12V 的电源电压上&#xff0c;每 通道能提供高达 1.5A 持续输出电流或者 2.5A 峰值 电流&#xff0c;睡眠模式下功耗小于 1uA。具有 PWM&#xff08;IN/EN&#xff09;输入接口,与行业标 准器件兼容&#xff0c;并具有过温保护&#xff0c;欠压保护&…

信息系统项目管理师好考吗?知识点分析与讲解,码住!

科目一&#xff1a;综合知识考试 科目一考试是由选择题组成的&#xff0c;共有75道题目。考试时间为早上9点到11点半&#xff0c;可以提前交卷&#xff0c;通常11点左右就能离开考场。对于会做的题目&#xff0c;要及时解答&#xff0c;对于不会做的题目&#xff0c;花费过多时…

QC/PD快充电源产品MOS选型分析

• 原边650-700V SJ MOSFET采用低FOM值的ESM 技术&#xff0c;有利于提高系统效 率&#xff0c; 以及更佳的EAS和EMI等特性&#xff0c;对于一些不含PFC电路的系统更友好。 • 副边采用低FOM值的SGT同步整流电路&#xff0c;相比肖特基二极管整流能有更低的 损耗&#xff0c;有…

pinia 给 state 指定变量类型

pinia 给 state 指定变量类型 问题描述 自从用 vitetsvue3 以来&#xff0c;我一直有一个很大的疑问&#xff0c;就是 pinia 中的 state 变量类型该从哪定义&#xff0c;如何定义它&#xff1f; 因为我在使用未定义类型的 state 变量的时候一直会有一个提示&#xff0c;提示说…

JAVA集合框架总结

集合框架概述 1.1 生活中的容器 1.2 数组的特点与弊端 一方面&#xff0c;面向对象语言对事物的体现都是以对象的形式&#xff0c;为了方便对多个对象的操作&#xff0c;就要对对象进行存储。另一方面&#xff0c;使用数组存储对象方面具有一些弊端&#xff0c;而Java 集合就…

Unity之预制体与变体

PS:不用说了&#xff0c;我在写博客就是在摸鱼 一、预制体 不知道大家小时候有没有看过火影&#xff0c;记得剧情最开始的时候水木哄骗鸣人去偷封印之书&#xff0c;反而让鸣人学会了多重影分身之术&#xff1a; 好了&#xff0c;小编绞尽脑子终于想好怎么向大家介绍预制体了&a…

element中Tree 树形控件实现多选、展开折叠、全选全不选、父子联动、默认展开、默认选中、默认禁用、自定义节点内容、可拖拽节点、手风琴模式

目录 1.代码实现2. 效果图3. 使用到的部分属性说明4. 更多属性配置查看element官网 1.代码实现 <template><div class"TreePage"><el-checkboxv-model"menuExpand"change"handleCheckedTreeExpand($event, menu)">展开/折叠&l…

亚马逊店铺遇到账号申诉模版分享

1.表达诚意&#xff0c;先认错再说&#xff1a;我知道&#xff0c;最近我们在Amazon.com上作为卖家的表现已经低于亚马逊和我们自己的质量标准。 2.清楚分明的格式&#xff1a;我们库存管理的混乱导致了延迟发货&#xff0c;更糟糕的是&#xff0c;物品无法使用。当延迟发货和…

00 项目结构

文章目录 后端 后端 后端 - sky-common包 公共类&#xff0c;工具类&#xff0c;常量类- constant 常量类- context 上下文有关的- enumenation 枚举- exception 自定义异常类- json json处理类- properties boot相关的配置属性类- result 结果类- uti…

【React系列】React中的CSS

本文来自#React系列教程&#xff1a;https://mp.weixin.qq.com/mp/appmsgalbum?__bizMzg5MDAzNzkwNA&actiongetalbum&album_id1566025152667107329) 一. React中的css方案 1.1. react 中的 css 事实上&#xff0c;css 一直是 React 的痛点&#xff0c;也是被很多开发…

gitlab高级功能之Kubernetes Agent介绍

文章目录 1. 前置条件2. 简介3. GitLab Kubernetes Agent 的部署3.1 启用 Agent 服务端3.2 创建 Agent 配置和清单仓库 4. 安装agent4.1 连接k8s集群4.2 在集群中部署4.3 修改资源清淡&#xff0c;调整pod的副本数 5. 思考 1. 前置条件 gitlab 14.5 专业版k8s集群helm客户端工…

风车模型与代码

这个模型使用NetLogo乌龟来重复绘制圆圈&#xff0c;定期转动&#xff0c;以便显示出类似万花筒或风车的效果。这是一个演示&#xff0c;展示了一组简单的代理规则如何产生复杂而美丽的图案。 内部工作原理非常简单。创建了许多乌龟&#xff0c;它们的笔都是放下的&#xff08…

一文全面了解 LSM BPF (含实战,强烈建议收藏)

本文地址&#xff1a;https://www.ebpf.top/post/lsm_bpf_intro 文章目录 1. 安全背景知识2. 内核安全策略模块通用框架 LSM2.1 LSM 框架介绍2.2 LSM 架构2.3 LSM 中的钩子函数 3. LSM BPF3.1 BCC 实践3.2 libbpf-bootstrap 框架实践 4. 总结5. 附录&#xff1a;LSM 热修内核漏…

Java 支持表情包存储 Incorrect string value: ‘\\xF0\\x9F\\x98\\x8A\\xF0\\x9F...‘

一&#xff0c;前言 最近测试提出了一个比较刁钻的bug 在提交表单数据的时候&#xff0c;支持表情输入&#xff0c;如下 看了一下前端参数&#xff0c;也是正常传递 但是调用接口的时候&#xff0c;后端却报错 Cause: java.sql.SQLException: Incorrect string value: \\xF0…

【python】使用fitz包读取PDF文件报错“ModuleNotFoundError: No module named ‘frontend‘”

【python】使用fitz包读取PDF文件报错“ModuleNotFoundError: No module named ‘frontend’” 正确解决过程 在读取PDF文件时&#xff0c;我使用了fitz包&#xff0c;当使用代码import fitz导入该包时&#xff0c;出现了报错&#xff1a; 于是我直接使用以下代码安装fronten…

技术查漏补缺(1)Logback

一、下定义&#xff1a;Logback是一个开源的日志组件 二、Logback的maven <!--这个依赖直接包含了 logback-core 以及 slf4j-api的依赖--> <dependency><groupId>ch.qos.logback</groupId><artifactId>logback-classic</artifactId><v…

基于Kettle开发的web版数据集成开源工具(data-integration)-介绍篇

目录 &#x1f4da;第一章 官网介绍&#x1f4d7;目标实现&#xff1a;让kettle使用更简单&#x1f4d7;架构及组成 &#x1f4da;第二章 核心功能&#x1f4da;第三章 对比Kettle&#x1f4d7;工具栏位比对&#x1f4d7;工具栏组件内容比对&#x1f4d7;扩展&#xff1a;WebSp…