TransmittableThreadLocal实现原理

TransmittableThreadLocal(TTL)底层实现原理 (TTL)是阿里开源的一个增强版 ,用于解决 线程池环境下的线程上下文传递问题。它继承自 ,并在其基础上增加了 跨线程池传递数据 的能力。TTL 的实现主要依赖 3 个关键组件:TTL 继承自 ,并维护了一个 全局的 集合()来跟踪所有 TTL 实例: 的作用是让 TTL 能 主动感知所有需要传递的变量,而不是像 那样仅依赖线程创建时的快照。 (2) 任务包装( / ) TTL 通过装饰器模式 包装 /,在任务执行前恢复上下文: 的底层逻辑: 是 TTL 最核心的类,负责 捕获(capture 阅读详情

TransmittableThreadLocal实现父线程值传递变更值给线程池子线程

InheritableThreadLocal只会在线程池中的线程初次创建的时候,从父线程拷贝属性,而父线程再次修改这个属性时,线程池中的这个线程是无法再次感知到的。

我们在日常的开发中,很少主动new线程,都是使用线程池,而线程池中,不会频繁的创建线程,更多的场景是线程创建一次,重复使用。

如果线程中的属性在上次使用后被修改,下次使用后,子线程中保存的属性值依然是首次使用时的值

TransmittableThreadLocal实现父线程值传递变更值给线程池子线程

import com.alibaba.ttl.TransmittableThreadLocal;
import com.alibaba.ttl.threadpool.TtlExecutors;
 
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
 
/**
 * @author : mazhen
 * Date    : 2021/11/12
 * Time    : 4:11 下午
 * ---------------------------------------
 * Desc    :
 */
public class ITL {
 
    public static TransmittableThreadLocal<String> param = new TransmittableThreadLocal<>();
 
    public static void main(String[] args) {
        param.set("main");
 
        ExecutorService pool = TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(1));
 
        pool.submit(() -> {
            System.out.println(param.get());
        });
        param.set("main2");
        pool.submit(() -> {
            System.out.println(param.get());
        });
        pool.submit(() -> {
            System.out.println(param.get());
        });
        pool.shutdown();
    }
 
}



----------------------
main
main2
main2

可以看到,使用TransmittableThreadLocal并配合TtlExecutors.getTtlExecutorService()可以让子线程感知到父线程对共享变量的实时变化。

子线程对父线程值的修改只会影响当前运行时子线程,不影响父线程和其他子线程,也不影响下次运行

import com.alibaba.ttl.TransmittableThreadLocal;
import com.alibaba.ttl.threadpool.TtlExecutors;
 
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
 
/**
 * @author : mazhen
 * Date    : 2021/11/12
 * Time    : 4:11 下午
 * ---------------------------------------
 * Desc    :
 */
public class ITL {
 
    public static TransmittableThreadLocal<String> param = new TransmittableThreadLocal<>();
 
    public static void main(String[] args) {
        param.set("main");
 
        ExecutorService pool = TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(1));
 
        pool.submit(() -> {
            System.out.println(param.get());
        });
        param.set("main2");
        pool.submit(() -> {
            param.set("child value only use in this");
            System.out.println(param.get());
        });
        pool.submit(() -> {
            System.out.println(param.get());
        });
        pool.shutdown();
    }
 
}


--------------
main
child value only use in this
main2

TransmittableThreadLocal实现原理

与ThreadLocal保存在Thread中不同,holder就保存在TransmittableThreadLocal中:

//holder就保存在TransmittableThreadLocal
private static InheritableThreadLocal<Map<TransmittableThreadLocal<?>, ?>> holder = new InheritableThreadLocal<Map<TransmittableThreadLocal<?>, ?>>() {
        protected Map<TransmittableThreadLocal<?>, ?> initialValue() {
            return new WeakHashMap();
        }
 
        protected Map<TransmittableThreadLocal<?>, ?> childValue(Map<TransmittableThreadLocal<?>, ?> parentValue) {
            return new WeakHashMap(parentValue);
        }
    };



//看设置值的特点
public final void set(T value) {
     // InheritableThreadLocal.set(),这里保留了父类的特性,即会把值保存到ThreadLocal
     super.set(value);
     if (null == value) {
         this.removeValue();
     } else {
         this.addValue();
     }
 
}
 
 
 
private void addValue() {
    if (!((Map)holder.get()).containsKey(this)) {
       ((Map)holder.get()).put(this, (Object)null);
    }
 
}

set时依然会像thread本身特点一样设置ThreadLocal,但是更重要的是TTL自己也创建了一个holder去保存值,而holder就保存在TransmittableThreadLocal中

TtlExecutors对普通的ExecutorService进行了封装

//封装ExecutorService
public static ExecutorService getTtlExecutorService(@Nullable ExecutorService executorService) {
        return (ExecutorService)(!TtlAgent.isTtlAgentLoaded() && executorService != null && !(executorService instanceof TtlEnhanced) ? new ExecutorServiceTtlWrapper(executorService) : executorService);
    }




///ExecutorServiceTtlWrapper的实现
public <T> Future<T> submit(@Nonnull Runnable task, T result) {
    return this.executorService.submit(TtlRunnable.get(task), result);
}
 
//核心就是TtlRunnable
public static TtlRunnable get(@Nullable Runnable runnable) {
    return get(runnable, false, false);
}
 
 
public static TtlRunnable get(@Nullable Runnable runnable, boolean releaseTtlValueReferenceAfterRun, boolean idempotent) {
    if (null == runnable) {
         return null;
    } else if (runnable instanceof TtlEnhanced) {
         if (idempotent) {
              return (TtlRunnable)runnable;
         } else {
             throw new IllegalStateException("Already TtlRunnable!");
         }
    } else {
         return new TtlRunnable(runnable, releaseTtlValueReferenceAfterRun);
    }
}




//TtlRunnable实现了runable,可以看到运行业务前后它会做子线程前后的现场备份和恢复工作
@Override
public void run() {
        /**
         * capturedRef是主线程传递下来的ThreadLocal的值。
         */
        Object captured = capturedRef.get();
        if (captured == null || releaseTtlValueReferenceAfterRun && !capturedRef.compareAndSet(captured, null)) {
        throw new IllegalStateException("TTL value reference is released after run!");
        }
        /**
         * 1.  backup(备份)是子线程已经存在的ThreadLocal变量;
         * 2. 将captured的ThreadLocal值在子线程中set进去;
         */
        Object backup = replay(captured);
        try {
        /**
         * 待执行的线程方法;
         */
        runnable.run();
        } finally {
        /**
         *  在子线程任务中,ThreadLocal可能发生变化,该步骤的目的是
         *  回滚{@code runnable.run()}进入前的ThreadLocal的线程
         */
        restore(backup);
        }
        }
/**
 * 将快照重做到执行线程
 * @param captured 快照
 */
public static Object replay(Object captured) {
// 获取父线程ThreadLocal快照
final Snapshot capturedSnapshot = (Snapshot) captured;
        return new Snapshot(replayTtlValues(capturedSnapshot.ttl2Value), replayThreadLocalValues(capturedSnapshot.threadLocal2Value));
        }

/*****************************************************
 * 重放TransmittableThreadLocal,并保存执行线程的原值
 ****************************************************/
private static WeakHashMap<TransmittableThreadLocalCode<Object>, Object> replayTtlValues(WeakHashMap<TransmittableThreadLocalCode<Object>, Object> captured) {
        WeakHashMap<TransmittableThreadLocalCode<Object>, Object> backup = new WeakHashMap<TransmittableThreadLocalCode<Object>, Object>();

        for (final Iterator<TransmittableThreadLocalCode<Object>> iterator = holder.get().keySet().iterator(); iterator.hasNext(); ) {
        TransmittableThreadLocalCode<Object> threadLocal = iterator.next();

        // backup
        // 遍历 holder,从 父线程继承过来的,或者之前注册进来的
        backup.put(threadLocal, threadLocal.get());

        // clear the TTL values that is not in captured
        // avoid the extra TTL values after replay when run task
        // 清除本次没有传递过来的 ThreadLocal,和对应值
        //  -- 第一点:可能会有因为 InheritableThreadLocal 而传递并保留的值
        //  -- 第二点:保证主线程set过的ThreadLocal不被传递过来。明确其传递是由业务代码控制,就是明确 set 过值的
        if (!captured.containsKey(threadLocal)) {
        iterator.remove();
        threadLocal.superRemove();
        }
        }

        // set TTL values to captured
        // 将 map 中的值,设置到快照
        // 内部调用了 beforeExecute 和 afterExecute 方法。默认不做任何处理
        setTtlValuesTo(captured);

        // call beforeExecute callback
        // TransmittableThreadLocal 的回调方法,在任务执行前执行
        doExecuteCallback(true);

        return backup;
        }

private static WeakHashMap<ThreadLocal<Object>, Object> replayThreadLocalValues( WeakHashMap<ThreadLocal<Object>, Object> captured) {
final WeakHashMap<ThreadLocal<Object>, Object> backup = new WeakHashMap<ThreadLocal<Object>, Object>();

        for (Map.Entry<ThreadLocal<Object>, Object> entry : captured.entrySet()) {
final ThreadLocal<Object> threadLocal = entry.getKey();
        backup.put(threadLocal, threadLocal.get());

final Object value = entry.getValue();
        // 如果值是标记已删除,则清除
        if (value == threadLocalClearMark) threadLocal.remove();
        else threadLocal.set(value);
        }

        return backup;
        }
/*********************************************
 * 恢复备份的原快照
 *********************************************/
public static void restore( Object backup) {
// 将之前保存的TTL和threadLocal原来的数据覆盖回去
final Snapshot backupSnapshot = (Snapshot) backup;
        restoreTtlValues(backupSnapshot.ttl2Value);
        restoreThreadLocalValues(backupSnapshot.threadLocal2Value);
        }

private static void restoreTtlValues( WeakHashMap<TransmittableThreadLocalCode<Object>, Object> backup) {
        // call afterExecute callback
        // 调用执行完后回调接口
        doExecuteCallback(false);

        // 移除子线程新增的TTL
        for (final Iterator<TransmittableThreadLocalCode<Object>> iterator = holder.get().keySet().iterator(); iterator.hasNext(); ) {
        TransmittableThreadLocalCode<Object> threadLocal = iterator.next();
        // 恢复快照时,清除本次传递注册进来,但是原先不存在的 TransmittableThreadLocal
        // 移除掉所有不在备份里面的TTL数据,应该是为了避免内存泄漏吧
        // clear the TTL values that is not in backup
        // avoid the extra TTL values after restore
        if (!backup.containsKey(threadLocal)) {
        iterator.remove();
        threadLocal.superRemove();
        }
        }

        // 重置为原来的数据(就是恢复回备份前的值)
        // restore TTL values
        setTtlValuesTo(backup);
        }

private static void setTtlValuesTo( WeakHashMap<TransmittableThreadLocalCode<Object>, Object> ttlValues) {
        for (Map.Entry<TransmittableThreadLocalCode<Object>, Object> entry : ttlValues.entrySet()) {
        TransmittableThreadLocalCode<Object> threadLocal = entry.getKey();
        // set 的同时,也就将 TransmittableThreadLocal 注册到当前线程的注册表了
        threadLocal.set(entry.getValue());
        }
        }

步骤:
1装饰Runnable,将主线程的TTL传入到TtlRunnable的构造方法中
2将子线程的TTL的值进行备份,将主线程的TTL设置到子线程中(value是对象引用,可能存在线程安全问题);
3执行子线程逻辑
4删除子线程新增的TTL,将备份还原重新设置到子线程的TTL中

其他注意点

TTL为什么不直接继承ThreadLocal?
因为有些业务需要用到ITL特性,如果直接继承ThreadLocal,就会丢失ITL的父拷贝到子线程数据的特性(子线程创建时拷贝)
为什么需要在run执行完之后调用restore()?
restore里面会主动调用remove()回收,避免内存泄露(会删除子线程新增的TTL)
不调用restore()的话,就会覆盖之前backup备份部分子线程的数据,这样可能在业务上有隐患
TTL存在线程安全问题?
存在的,因为默认都是引用类型拷贝,如果子线程修改了数据,主线程是可以感知到的
TTL是否存在内存泄露问题?
TTL维护的holder本身是一个static来的,使用的时候会调用restore(),然后里面显式调用remove()清楚子线程新增TTL,所以正确使用下是没有内存泄露问题的

参考原文链接:https://blog.youkuaiyun.com/qq_28666081/article/details/119834058

深入理解TransmittableThreadLocal:原理、使用与避坑指南 TransmittableThreadLocal(TTL)是阿里开源的一个线程间数据传递解决方案,解决了InheritableThreadLocal在线程池场景下的问题。核心特性- 支持线程池场景下的值传递- 支持任务执行前的自定义逻辑- 支持任务执行后的自定义逻辑- 兼容InheritableThreadLocalTransmittableThreadLocal是解决线程池环境下上下文传递的强大工具,合理使用可以简化编程模型,但需要注意内存管理和性能影响。 阅读详情

相关推荐

TransmittableThreadLocal详解

ThreadLocal和InheritableThreadLocal学习资料:二刷Java多线程:ThreadLocal详解 线程池学习资料:二刷Java多线程:线程池详解 1、InheritableThreadLocal+线程池局限性 InheritableThreadLocal支持子线程访问在父线程中设置的线程上下文环境,其实现原理是在创建子线程时将父线程中的本地变量值复制到子线程,即复制的时机为创建子线程时 当提交一个新任务到线程池时,线程池的处理流程如下: 线程池判断线程数是否达到核心线程数且线程

hxt的博客 2258

TransmittableThreadLocal原理

TransmittableThreadLocal(简称TTL)是阿里巴巴开源的一个Java库,用于解决线程池中线程本地变量传递的问题。其底层原理主要是基于Java的机制并对其进行扩展,以支持在父子线程间以及线程池中任务切换时,能够传递和继承变量的值。: TTL继承自但与不同的是,TTL解决了在已经存在的线程(如线程池中的线程)之间传递值的问题。: 对于异步任务,TTL确保了即使在异步执行的上下文中,父线程的状态也能被正确地传递给处理该任务的子线程。

主要作为个人的笔记用,Not For Everyone。 1591

InheritableThreadLocal与阿里的TransmittableThreadLocal设计思路解析

前言 参考文章: 《全链路跟踪(压测)必备基础组件之线程上下文“三剑客”》-- 原创: 丁威 中间件兴趣圈 https://mp.weixin.qq.com/s/a6IGrOtn1mi0r05355L5Ng 《阿里巴巴Transmittable ThreadLocal(TTL) github》 https://github.com/alibaba/transmittable-thread-loc...

度窝-笔记 8442

TransmitableThreadLocal 实现原理

TransmittableThreadLocal(TTL)是阿里巴巴开源的一个Java工具类库,用于解决线程池或异步任务场景下,子线程无法获取父线程中ThreadLocal变量值的问题。TTL通过在线程池或异步任务的边界传递ThreadLocal变量的值,确保在子线程中可以访问到父线程中的相同值。TTL的实现原理如下:基于ThreadLocal:TTL利用Java的ThreadLocal机制实现。

Primary_wind的专栏 1386

二维otsu算法python_OpenCV-Python系列之OTSU算法

上一个教程中,我们谈到了关于图像二值化的两种方法,一种是固定阈值法,另一种是自适应阈值法,总的来说,自适应阈值法在某些方面要由于固定阈值法,但还没完,这次我们将隆重介绍我们的重量级选手,也就是OTSU算法(又称为大津算法和最大类间方差法)。最大类间方差法是1979年由日本学者大津提出的,是一种自适应阈值确定的方法,又叫大津法,简称OTSU,是一种基于全局的二值化算法,它是根据图像的灰度特性,将图像...

weixin_28834169的博客 1426

在VM虚拟机的MobaXterm下安装JDK17

本文介绍了在Linux系统下安装配置JDK17的完整步骤:1)从Oracle或OpenJDK官网下载Linux版JDK17压缩包;2)上传至/usr/local目录;3)解压安装包;4)通过编辑/etc/profile文件配置环境变量,添加JAVA_HOME和PATH路径;5)使用source命令使配置生效;6)通过javac -version验证安装是否成功。整个过程涵盖了从下载到环境配置的完整流程,确保Java开发环境正确设置。

qq_2937655951的博客 441

谈谈TransmittableThreadLocal实现原理和在日志收集记录系统上下文实战应用

JDK的InheritableThreadLocal(ITL)在父子线程间传递值时,无法解决线程池复用线程导致的值传递问题。阿里开源的TransmittableThreadLocal(TTL)通过TtlRunnable修饰任务或TtlExecutors修饰线程池,实现了线程池场景下的值传递。其核心原理是装饰器模式,在任务提交时抓取当前线程上下文,确保每次任务执行都能获取正确的上下文值。相比ITL,TTL更适用于线程池等复用线程的场景。

weixin_41821642的博客 1033

AI对嵌入式的冲击分三波,大多数人只看到了第一波

摘要:AI对嵌入式行业的冲击将分三波递进:第一波(已发生)是代码生成,AI能完成底层驱动配置;第二波(1-2年内)是架构设计,AI将参与系统方案制定;第三波(2-3年)是需求验证,AI能穷举测试用例但无法替代安全决策。每波冲击都将重新定义工程师的价值标准,经验型工程师若不能将隐性知识转化为显性规则将面临淘汰。作者指出,完全不用AI的人第一波就会被淘汰,而仅满足于第一波应用的人将在后续冲击中落后。

u010898524的博客 960

009-ThreadLocal、InheritableThreadLocal和TransmittableThreadLocal的实现原理

ThreadLocal介绍threadLocal的特点就是与线程绑定,一般通过这种隐式传参的方式来传递上下文。用户登录和获取用户相关信息,这时候如果在每个需要用户信息的方法入参上加入用户信息参数就先的非常冗余,不够优雅。日志的链路信息等等。根据上面的问题我们知道ThreadLocal存在的局限性,InheritableThreadLocal也就是可继承的ThreadLocal,这里的可继承就是指的ThreadLocalMap。

mythsmyths的博客 256

ros1 ubuntu22.04 安装教程

ROS1 ubuntu22.04 安装教程

autolabor的博客 9195

05-SA8155 QNX Display框架及代码分析(1)

显示是非常重要的一个模块,人机交互的桥梁。那么SA8155 Hypervisor显示是如何处理的呢。对处理流程非常的好奇O(∩_∩)O哈哈~。TODO。

静虚待令,有物混成 1万+

TransmittableThreadLocal(TTL)实现线程变量传递的原理分析

TransmittableThreadLocal(TTL)是阿里开源的,用于解决异步执行时上下文传递的问题的组件,在InheritableThreadLocal基础上,实现了线程复用场景下的线程变量传递功能。 TTL做的实际上就是将原本与Thread绑定的线程变量,缓存一份到TtlRunnable对象中,在执行子线程任务前,将对象中缓存的变量值设置到子线程的ThreadLocal中以供run()方法的代码使用,然后执行完后,又恢复现场,保证不会对复用线程产生影响。

未完成的空间 2万+

Habitat-v0.1.7安装全网最详细教程(解决habitat-sim下载时网络问题)

Habitat-v0.1.7安装全网最详细教程(解决habitat-sim下载时网络问题)

shannanshouyin的博客 1325

java传递父线程对象,TransmittableThreadLocal(TTL)实现线程变量传递的原理分析

使用版本:release - v2.11.5最近在准备双十一压测相关的调整工作,需要将线程池改为都使用TTL包装过的,用于辅助pinpoint插件实现Trace信息传递。之前没怎么关注过TTL这个库,因此在改用TTL的同时,学习下其实现原理。一、背景InheritableThreadLocal及其局限性Jdk提供了InheritableThreadLocal类,用于在父子线程间传递线程变量(Thr...

weixin_36307549的博客 1160

行人重识别(ReID)基础知识入门

行人重识别(Person re-identification)主要目的是针对出现在监控摄像头内的某个目标行人,准确快速地从监控网络其他摄像头内的大量行人中将这个目标行人标识出来。如图所示一个区域有多个摄像头拍摄视频序列,ReID的要求对一个摄像头下感兴趣的行人,检索到该行人在其他摄像头下出现的所有图片。

用于记录 1万+

Stable-Baselines3性能优化:多环境训练对比指南(DummyVecEnv vs SubprocVecEnv)

本文深入对比了Stable-Baselines3中DummyVecEnv与SubprocVecEnv两种向量化环境的性能差异与应用场景。DummyVecEnv适用于轻量级、步进快的环境,调试友好且开销低;而SubprocVecEnv则通过真正的多进程并行,能显著加速计算密集型环境的训练。文章通过基准测试与决策树,指导开发者根据环境计算成本、CPU核心数等关键因素做出最优选择,以实现训练效率最大化。

weixin_29063979的博客 145

【2025】关于日志系统的一些理解。为什么要有日志系统?有哪些日志框架?以及InheritableThreadLocal和TransmittableThreadLocal的实现原理?

现在,很多应用都是集群部署的,一次请求会因为负载均衡而被路由到不同的服务器上面,这就导致一个应用的日志会分散在不同的服务器上面。当我们要向通过日志做数据分析,问题排查的时候,就需要分别到每台机器上去查看日志,这样就太麻烦了。于是就有了分布式日志系统,他可以做分布式系统中的。

gwndjsh的博客 1785

Probability and Statistics - 4th edition

Probability and Statistics - 4th edition

TransmittableThreadLocal源码分析

666

weixin_70280523的博客 1218

文档:亲测可用解决vnc灰屏,黑屏,鼠标大黑叉情况

主要解决vnc-viewer灰屏,黑屏,鼠标大黑叉等现象,详细见文档。

假人有限元模型

假人模型,车辆碰撞假人有限元模型,适合做乘员安全的研究者利用

从ThreadLocal谈到TransmittableThreadLocal,从使用到原理

谈到这个其实还是蛮有意思的,因为我最近有在看SpringCloud相关的知识点,在玩那个链路追踪(sleuth+zipkin)的时候,看博客看着看着,就变成了看关于怎么自己手动实现链路追踪的文章去啦因为小小的好奇心吧,然后在掘金看到了这篇:自实现分布式链路追踪 方案&实践-作者:蝎子莱莱爱打怪也就是这篇文章中提到了我今天想谈的这个知识点,顺着文章提供的链接,我又去了github上溜达。至此才有了我笔下的这篇东拼西凑的博文~ 学过一些多线程的知识,但我可以说这是我第一次接触吗 🤕 哈哈。

竹林幽深 384

SAP B1中文培訓教材_SDK

公司内部培训资料,对开发工具 ,DIAPI, UIAPI,Add-ON有详细解说

TransmittableThreadLocal 源码分析

一. TransmittableThreadLocal 简介 传统的jdk的ThreadLocal只能解决线程或者父,子线程之间的数据传递。 而 TransmittableThreadLocal 是为了解决 线程池里面的线程之间传递ThreadLocal。 二. 实例带入 public static void main(String[] args) throws Exception { ThreadLocal<String> THREAD_LOCAL = new Transmit

吉他程序员的博客 786

ThreadLocal 实现原理,如何防止内存泄露,TransmittableThreadLocal(ThreadLocal线程池线程重用会导致数据污染)

ThreadLocal实现的原理 首先,我们来看一下ThreadLocal的set()方法,因为我们一般使用都是new完对象,就往里边set对象了 public void set(T value) { // 得到当前线程对象 Thread t = Thread.currentThread(); // 这里获取ThreadLocalMap ThreadLocalMap map = getMap(t); // 如果m......

Be_insighted的博客 1477

一窥TransmittableThreadLocal

ThreadLocal在我们平时的开发中很常见,拥有线程级别的变量共享,但是现在的项目都是跨线程的调用,如果主线程创建了另一个线程(父子线程),另一个线程还能拿到主线程的数据吗?这时候ThreadLocal就力不从心了,还好jdk提供了InheritableThreadLocal类,我们稍微讲下InheritableThreadLocal在跨线程间变量传递的原理。在Thread类里,除了threadLocals 变量,还有一个inheritableThreadLocals变量,两者类型一模一样。 inher

致橡树 1244

何为TransmittableThreadLocal

一、示例 线程池内的线程并没有父子关系,所以不适合InheritableThreadLocal的使用场景public class ThreadPoolInheritableThreadLocalDemo { // static ThreadLocal&lt;String&gt; threadLocal = new InheritableThreadLocal&lt;&gt;(); // ...

hellozhxy的博客 1259
上一篇: ThreadLocal和InheritableThreadLocal实现原理
下一篇: Loki+Grafana查询语句
牧竹子
博客等级 码龄17年 572粉丝 233原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值