ThreadLocal 、TransmittableThreadLocal 底层原理 (图解+秒懂+史上最全)

本文 的 原文 地址

原始的内容,请参考 本文 的 原文 地址

本文 的 原文 地址

尼恩说在前面:

在40岁老架构师 尼恩的读者交流群(50+)中,最近有小伙伴拿到了一线互联网企业如得物、阿里、滴滴、极兔、有赞、shein 希音、shopee、百度、网易的面试资格,遇到很多很重要的面试题:

ThreadLocal 底层原理是 什么?

ThreadLocal只能在同步中传递上下文信息,如果某个业务需要开启异步多线程,那么每个异步线程怎么拿到上下问信息?

你们项目怎么使用thread local的?

最近一个 34岁的专科小伙伴(L同学) 面架构 , 按照尼恩下面的 思路去做答, 拿到了5个架构offer ,爆桶了。

借着此文,尼恩给大家做L同学的作答思路, 做一下系统化、体系化的梳理,使得大家内力猛增,展示一下雄厚的 “技术肌肉、技术实力”,让面试官爱到 “不能自已、口水直流”,然后实现”offer直提,offer自由”。

当然,这道面试题,以及参考答案,也会收入咱们的 《尼恩Java面试宝典PDF》V170版本,供后面的小伙伴参考,提升大家的 3高 架构、设计、开发水平。

《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》的PDF,请到文末公号【技术自由圈】获取

一、ThreadLocal 局限性:异步传递失效

ThreadLocal 是基于线程隔离的机制,每个线程拥有自己的数据副本(通过 Thread.threadLocals 存储)。

同一个 ThreadLocal 变量在不同线程中互不共享

  • 同步场景可用:同一线程内,无论调用层级多深,都能访问到设置的值。
  • 异步场景失效:一旦开启新线程(如 new Thread()),子线程会创建自己独立的 threadLocals,无法继承父线程的上下文。

ThreadLocal 的局限性:异步调用时上下文丢失问题。 示例代码


// 主线程设置上下文
ThreadLocal<String> context = new ThreadLocal<>();
context.set("主线程上下文");

// 异步子线程尝试获取
new Thread(() -> {
    System.out.println(context.get()); // 输出:null(上下文丢失)
}).start();

结果为 null,因为子线程没有自动继承父线程的 ThreadLocal 值。

ThreadLocal 仅保证线程内部的变量可见性,不支持跨线程传递。在使用线程池或异步任务时,上下文将无法自动传递,需借助其他机制 解决。

45岁老架构师尼恩尼恩点评:

L同学 讲到这里,已经清晰指出了 ThreadLocal 在异步场景下因线程隔离导致上下文丢失的本质问题,展现了扎实的底层知识功底;但仅停留在“现象+示例”层面,未触及面试官真正想考察的跨线程上下文传递的设计思维

如何在复杂调用链中安全、自动地传递上下文?

接下来L同学引入的手动传递方案,精准命中了这一痛点:一是通过显式传递+finally清理 (手动传递) ,体现了对内存泄漏风险的警惕与防御性编程意识;二是为 框架自动传递 (弱入侵自动化方案 如TTL),展现出清晰的技术演进逻辑。

开局不错,再接再厉。L同学 讲 接下来 L同学开始 分析 异步场景下的上下文传递方案

二、异步场景下的上下文context传递

在多线程异步编程中,经常需要将主线程的上下文(如用户信息、请求ID等)传递到子线程。

由于 ThreadLocal 数据默认不会自动跨线程共享,因此必须通过特定方式传递。

常见的解决方案分为两类:手动传递框架自动传递

1. 手动传递:显式传递 ThreadLocal 数据

核心思路

在创建子线程前,从主线程读取 ThreadLocal 中的上下文数据,然后通过参数或闭包传给子线程,并在子线程中重新设置到其自己的 ThreadLocal 中。

示例代码


ThreadLocal<String> context = new ThreadLocal<>();
context.set("local value 技术自由圈");

// 主线程获取上下文并传递给子线程
String mainContext = context.get();
new Thread(() -> {
    try {
        // 子线程设置上下文
        context.set(mainContext);
        System.out.println("子线程获取到的上下文:" + context.get()); // 输出:主线程上下文
    } finally {
        // 清除子线程的ThreadLocal,避免内存泄漏
        context.remove();
    }
}).start();

优点

  • 实现简单
  • 不依赖第三方库

缺点

  • 每次异步操作都要手动传递和清理,代码重复且易遗漏
  • 若忘记调用 remove(),可能导致内存泄漏,尤其在线程池场景下更危险

适用场景:逻辑简单、异步调用少的小型项目。

线程池的场景,可以改造成使用工具类或框架(如 TransmittableThreadLocal)来自动传递上下文。

2. 线程池场景: 通过装饰器模式传递上下文

在使用线程池(如 ThreadPoolExecutor)执行异步任务时,主线程的上下文信息(例如用户身份、请求追踪ID等,通常存于 ThreadLocal无法自动传递到子线程

为解决此问题,可使用装饰器模式对任务 task 进行包装,在任务执行前后自动传递和清理上下文。

核心流程

(1) 提交任务时:捕获当前线程的上下文( 从 ThreadLocal 中读取 value)。

(2) 执行任务前:在子线程中设置该上下文。

(3) 任务执行后:恢复子线程原有上下文,避免污染线程池中的线程。

这种方式无需修改业务逻辑,只需在提交任务时做一层包装。

示例代码


public class ContextAwareRunnable implements Runnable {
    private final Runnable target;
    private final String context; // 存储主线程的上下文

    public ContextAwareRunnable(Runnable target) {
        this.target = target;
        // 捕获当前线程(主线程)的上下文
        this.context = ContextHolder.getContext();
    }

    @Override
    public void run() {
        String originalContext = ContextHolder.getContext();
        try {
            // 子线程设置主线程的上下文
            ContextHolder.setContext(context);
            target.run(); // 执行原任务
        } finally {
            // 恢复子线程原有上下文
            ContextHolder.setContext(originalContext);
        }
    }
}

// 工具类:封装ThreadLocal操作
class ContextHolder {
    private static final ThreadLocal<String> context = new ThreadLocal<>();

    public static void setContext(String value) {
        context.set(value);
    }

    public static String getContext() {
        return context.get();
    }

    public static void clear() {
        context.remove();
    }
}

// 使用方式:提交任务时用装饰器包装
ExecutorService executor = Executors.newFixedThreadPool(5);
executor.submit(new ContextAwareRunnable(() -> {
    System.out.println("子线程获取到的上下文:" + ContextHolder.getContext());
}));

优点与限制

项目说明
优点- 对业务代码无侵入
- 可统一在线程池层处理上下文传递
- 适合大量异步任务场景
缺点- 需手动包装任务(如使用自定义 submit 工具)
- 若使用第三方线程池且无法控制任务提交方式,则难以应用

该实现可作为构建更通用上下文传递工具的基础(如结合 Callable 支持返回值),也可用于日志追踪、权限校验等依赖 ThreadLocal 的场景。

在使用 ThreadPoolExecutor 等线程池执行异步任务时,ThreadLocal 上下文无法自动传递到子线程,导致日志链路追踪、用户身份信息丢失等问题。

解决方案: 自定义线程池,然后通过 装饰器模式 包装 RunnableCallable,在任务提交时“快照”主线程上下文,在子线程执行前恢复,执行后清理——实现上下文的跨线程传递。

  • ✔️ 对业务代码几乎无侵入
  • ✔️ 可统一集成在线程池层面
  • 若使用第三方不可控线程池,则难以直接应用

45岁老架构师尼恩尼恩点评:

L同学精准指出了线程池中上下文传递的痛点,并基于装饰器模式给出了可落地的技术方案,体现了扎实的并发编程功底;

但其论述止步于“如何做”,未触及问题的本质根源——这会让面试官从认可转为意犹未尽,期待更深层的洞见。殊不知,真正打动面试官的是对 ThreadLocal 设计本质的理解。

接下来 L同学开始 分析 ThreadLocal 异步场景为什么 上下文传递失败的底层原理,和 开源TTL 底层原理。开始真正 吊打面试官。

三、 原理拆解:为什么 ThreadLocal 会“断层”?

1. ThreadLocal 的本质

  • 每个线程拥有独立的变量副本。
  • 主线程设置的值,不会自动传递给它创建的子线程。

// 示例:主线程设值,子线程拿不到
ContextHolder.setContext("user123");
new Thread(() -> {
    System.out.println(ContextHolder.getContext()); // 输出 null!
}).start();

2. 线程池复用加剧问题

线程池中的线程, 是反复使用的,长期存活 的:

  • 第一次任务设置了上下文;
  • 第二次任务可能“误读”上次残留数据;
  • 正确做法:每次执行前后都要 保存 → 设置 → 清理

类比快递打包:L同学要寄一件衣服,不能只说“这是我穿的那件”,而要拍照记录特征(捕获)→ 装箱发送(传递)→ 收货后还原描述(恢复)

3. ThreadLocal 的 底层map 结构: 使用map 存储key-value

两种map结构的 版本演进总览

第一阶段:Java 1.2 - 1.7 的实现方式

一个ThreadLocal 内部一个map (globalMap ) ,thread 作为key。

第二阶段:Java 1.8+ 的现代实现

一个 Thread 内部维护 一个 map (ThreadLocalMap) ,使用ThreadLocal做key

两种设计思路 核心思想对比:

全局Map 高并发时锁竞争严重


// 所有线程操作同一个全局Map,需要同步锁
private static Map<Thread, T> globalMap = 
                 Collections.synchronizedMap(new HashMap<>()); 

4、Java8之后设计:一个 Thread 一个 Map(Java 实际实现)

这是 Java 实际采用的设计,也是面试中需要重点阐述的。其核心结构如下:

5、内部结构详解:一个 Thread 一个 Map,用 ThreadLocal 做 Key

1). 核心存储结构

每个 Java 线程(Thread对象)内部都维护了一个私有的 ThreadLocalMap


// 在 java.lang.Thread 类中
public class Thread {
    // 每个线程都有自己的 ThreadLocalMap
    ThreadLocal.ThreadLocalMap threadLocals = null;
}

这个 ThreadLocalMapThreadLocal的静态内部类,它的基本结构是:


static class ThreadLocalMap {
    // 底层是 Entry 数组,处理哈希冲突使用线性探测法
    private Entry[] table;
    
    static class Entry extends WeakReference<ThreadLocal<?>> {
        // 存储的值,是强引用
        Object value;
        
        Entry(ThreadLocal<?> k, Object v) {
            super(k);  // Key 是弱引用指向 ThreadLocal 实例
            value = v; // Value 是强引用
        }
    }
}

2). 数据存储的实际形式

假设我们在同一个线程中定义了两个不同的 ThreadLocal变量:


// 主线程中
ThreadLocal<String> userContext = new ThreadLocal<>();
ThreadLocal<Integer> transactionId = new ThreadLocal<>();

userContext.set("Alice");
transactionId.set(12345);

此时,主线程的 threadLocals中的数据是这样的:


Thread-1.threadLocals (ThreadLocalMap):
{
    [userContext 实例]  ->  "Alice",
    [transactionId 实例] ->  12345
}

6、正确设计的优势分析

1). 完美的变量隔离

//  正确设计:每个 ThreadLocal 实例都是独立的 Key
ThreadLocal<String> userName = new ThreadLocal<>();
ThreadLocal<Integer> userAge = new ThreadLocal<>();

// 在主线程中
userName.set("Alice");
userAge.set(30);

// 此时主线程的 threadLocals 中:
{
    [userName 实例] -> "Alice",
    [userAge 实例]  -> 30
}
// 两个变量完全独立,互不干扰

2). 内存管理优势

弱引用的巧妙运用

  • Key(ThreadLocal实例)是弱引用
  • 当外部没有强引用指向 ThreadLocal实例时,GC 可以回收 Key
  • 下次访问时,ThreadLocalMap会清理 Key 为 null 的 Entry,避免内存泄漏
3). 性能优势

线程本地访问

  • 每个线程操作自己内部的 Map,不需要同步锁
  • 避免了全局 Map 的并发竞争问题
  • 数据访问更快,更高效

7、源码级验证:set() 和 get() 的实际流程

1. set() 方法源码分析

public void set(T value) {
    Thread t = Thread.currentThread();  // 1. 获取当前线程
    ThreadLocalMap map = getMap(t);      // 2. 获取线程的 ThreadLocalMap
    
    if (map != null) {
        // 3. 以当前 ThreadLocal 实例为 Key 存储值
        map.set(this, value);
    } else {
        // 4. 第一次使用时创建 Map
        createMap(t, value);
    }
}

// 获取线程的 Map(就是返回 threadLocals)
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

// 创建新的 Map
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

2. get() 方法源码分析

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    
    if (map != null) {
        // 以当前 ThreadLocal 实例为 Key 获取值
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

8、设计哲学总结

为什么这是更优雅的设计?

(1) 关注点分离:Thread负责管理线程本地的存储空间ThreadLocal负责提供访问接口和作为变量标识符

(2) 扩展性:一个线程可以轻松拥有任意多个线程本地变量每个变量通过不同的 ThreadLocal实例区分

(3) 生命周期管理:变量生命周期与线程绑定,线程结束自动清理弱引用机制避免 ThreadLocal实例泄漏

(4) 性能优化:无锁设计,每个线程操作自己的数据局部性原理,数据访问更高效

面试回答要点

核心结论:Java 采用"一个 Thread 一个 Map,用 ThreadLocal 做 Key"的设计,是因为这种设计可以完美支持同一线程中多个线程本地变量的独立存储和访问,而用 Thread 做 Key 的设计会导致变量间相互覆盖,无法实现真正的线程本地变量隔离。

这种设计体现了"让每个线程管理自己的数据,让每个变量标识自己的存储空间"的架构哲学,既保证了功能完整性,又兼顾了性能和内存管理的优雅性。

45岁老架构师尼恩尼恩点评:

L同学清晰拆解了 ThreadLocalMap 的存储机制,展现了扎实的源码功底和结构化表达能力,尤其对弱引用与线性探测的解释到位; 面试官虽频频点头, 觉得“不错,但不过瘾”。

接下来, L同学开始进行高度拉升。

引入的 TransmittableThreadLocal(TTL) 工业级方案,TTL 核心价值一是通过自动快照与还原机制彻底解耦业务代码,二是借助 TtlExecutors 包装器实现对线程池的无侵入增强,解决前面的 手工包装 方案 “难以复用、易漏清理”的根本痛点。

四. 工业级方案:使用 TTL 实现线程上下文传递

使用阿里开源的 TransmittableThreadLocal(TTL),可以自动将主线程的上下文“传递”到异步子线程中,解决 ThreadLocal 在线程池场景下无法继承的问题。

  • 适用于:分布式追踪、链路日志、用户身份上下文等需要跨线程共享数据的场景。
  • 关键优势:无需手动传参,支持线程池复用和嵌套异步调用。
  • 使用前提:必须通过 TtlExecutors 包装线程池,否则失效。

原理拆解:TTL 是怎么做到“自动传递”的?

Java 原生的 ThreadLocal 只能在当前线程内有效。

一旦 把任务提交给线程池,子线程拿不到父线程设置的值 —— 因为每个线程都有自己独立的 ThreadLocal 存储。

TTL 的核心思路(通俗类比):

想象 要寄一个包裹(上下文信息),但快递员(线程池里的线程)是临时调度的。

普通方式是L同学口头告诉快递员:“记得帮我办件事。”——但新来的快递员根本不知道 你 说过啥。

TTL 的做法是:

(1) 把 要交代的事写成一张便条(快照上下文);

(2) 和包裹一起打包封好(绑定任务 Runnable);

(3) 快递员取件时自动看到这张纸条,并照做(执行前恢复上下文);

(4) 办完事后把纸条撕掉(清理现场)。

这样,即使快递员不是同一个人,也能准确完成 你 的嘱托。

TTL 本质:

  • 在任务提交时:捕获当前线程的所有 TransmittableThreadLocal 变量值,生成“快照”;
  • 在线程执行任务前:将快照中的值复制到子线程的 TTL 中;
  • 任务结束后:清除这些值,防止内存泄漏或污染后续任务。

TTL 从代码到执行流程逐步解析

下面我们结合实际代码与 Mermaid 图,一步步看 TTL 是如何工作的。

步骤一:引入依赖(Maven)

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>transmittable-thread-local</artifactId>
    <version>2.14.2</version>
</dependency>

注意:这是第三方库,需显式引入才能使用 TransmittableThreadLocalTtlExecutors

步骤二:替换原生 ThreadLocal

// 定义可传递的上下文
private static final TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>();

// 主线程设置上下文
context.set("主线程上下文");

// 使用 TtlExecutors 包装线程池(关键!)
ExecutorService executor = TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(5));

// 提交异步任务
executor.submit(() -> {
    System.out.println("子线程获取到的上下文:" + context.get()); // 输出:主线程上下文
    context.remove(); // 清理资源
});

重点说明:

代码片段对应流程阶段
context.set(...)主线程保存上下文
TtlExecutors.getTtlExecutorService(...)创建具备“上下文传递能力”的线程池包装器
executor.submit(...)提交任务 → 触发上下文快照与绑定
Lambda 内 context.get()子线程读取已被恢复的上下文
TTL 执行流程图

关键细节

以下是使用 TTL 时必须注意的技术要点,避免踩坑:

1. 必须使用 TtlExecutors 包装线程池

// 错误 :直接使用原始线程池
ExecutorService badPool = Executors.newFixedThreadPool(4);
badPool.submit(() -> System.out.println(context.get())); // null!

// 正确 :用 TtlExecutors 包装
ExecutorService goodPool = TtlExecutors.getTtlExecutorService(
    Executors.newFixedThreadPool(4)
);
goodPool.submit(() -> System.out.println(context.get())); // "主线程上下文"

原因:只有被包装后的线程池才会在 submit() 时自动做上下文快照和绑定。

2. 自动清理机制(防内存泄漏)

TTL 在任务执行前后会自动处理上下文的复制与清除,包括:

  • beforeExecute():将父线程快照注入子线程;
  • afterExecute():清空本次传递的值,恢复子线程原有状态。

开发者仍建议显式调用 remove(),尤其是在长生命周期任务中:


try {
    String value = context.get();
    // 处理业务...
} finally {
    context.remove(); // 推荐:主动清理
}

3. 支持嵌套异步与线程复用

TTL 不仅支持一级异步,还支持多层传递,比如:


context.set("一级上下文");
executor.submit(() -> {
    System.out.println("第一层子线程: " + context.get()); // 有值

    CompletableFuture.runAsync(() -> {
        System.out.println("第二层子线程: " + context.get()); // 依然有值
    }, ttlForkJoinPool).join();
});

只要每一层都使用了 TTL 包装的执行器,上下文就能逐级传递下去。

4. 兼容性说明

特性是否支持
普通线程池(newThread / fixedPool)需用 TtlExecutors 包装
ForkJoinPool / CompletableFuture提供 TtlForkJoinPool 工具类
Spring @Async 注解可配合自定义 TaskExecutor 使用
Reactor / WebFlux 异步流不适用,需用 ContextScope 机制

总结:TTL 使用口诀(便于记忆)

“三要三不要”

不要
要用 TransmittableThreadLocal 替代 ThreadLocal不要用原生线程池
要用 TtlExecutors.getTtlExecutorService() 包装不要在任务中长期持有不清理
要理解“快照+绑定+还原”机制不要期望它能跨 JVM 传递(那是分布式上下文的事)

45岁老架构师尼恩尼恩点评:

L同学在上一阶段 , 清晰阐述了 TTL 的使用和底层原理。

介绍了ttl 如何通过 快照机制解决线程池中 ThreadLocal 的传递问题,展现了扎实的中间件应用能力和场景化思维; 面试官虽频频点头表示认可,面试官会瞬间坐直身体——因为这不仅是技术选型的升级,更是从编码实现到架构思维的跨越,两眼放光只是开始,心里早已默默打上“可带团队”的标签。

L同学知道,真正的碾压 不在于复述功能.而在于解构设计本质 ——接下来介绍自己抽象的 CRER 四步范式 ,穿透TTL 源码范式;

TTL 核心源码解析: 拆解 CRER 范式与设计模式

核心结论:TTL 的工作流程 = CRER 四步法

L同步通过抽象,把TTL(TransmittableThreadLocal)的核心机制可以用四个字概括:捕获 → 复现 → 执行 → 恢复(CRER)

这不仅是一个流程模型,更是贯穿整个源码的执行范式

步骤动作发生在线程目的
C: Capture(捕获)冻结主线程当前所有 TTL 上下文主线程生成“快照”随任务一起传递
R: Replay(复现)将快照恢复到子线程中子线程让子线程拥有和主线程一样的上下文
E: Execute(执行)执行业务逻辑子线程业务代码无感知地使用上下文
R: Restore(恢复)恢复子线程原有状态子线程防止线程池复用导致上下文污染

为什么重要?

因为线程池中的线程是被复用的,如果不做“恢复”,上一个任务设置的上下文可能会“泄露”给下一个任务——这就是典型的上下文污染

而 TTL 通过这套标准流程,完美解决了这个问题。

原理拆解:每一步背后的技术实现

… 略5000字+

…由于平台篇幅限制, 剩下的内容(5000字+),请参参见原文地址

原始的内容,请参考 本文 的 原文 地址

本文 的 原文 地址

### ThreadLocal底层实现机制 ThreadLocalJava 中提供的一种能够让线程内部存储变量副本的工具,使得不同线程能够独立访问自己的变量副本而不受其他线程的影响。这种特性对于多线程环境下的资源管理非常重要。 #### 底层数据结构 Thread 维护了一个名为 `threadLocals` 的成员变量,它是一个自定义类型的数组 `ThreadLocal.ThreadLocalMap[]`[^1]。每当调用 `set()` 或 `get()` 方法时,实际上是在操作这个 Map 对象。具体来说: - **ThreadLocalMap**: 这个类是 ThreadLocal 实现的核心部分之一,它是保存键值对的数据结构,其中 key 就是指向当前使用的 ThreadLocal 变量对象本身,而 value 则是我们通过 set() 存入的具体值。 ```java static class ThreadLocalMap { static class Entry extends WeakReference<ThreadLocal<?>> { Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } private Entry[] table; ... } ``` - **弱引用 (Weak Reference)**: Key 使用的是弱引用来指向 ThreadLocal 实例,这意味着如果某个 ThreadLocal 没有被任何强引用持有,则可以在垃圾回收期间被清除掉,从而防止内存泄漏问题的发生。 #### 主要方法解析 - **初始化 (`initialValue`)** 当第一次调用 get() 方法获取尚未赋过初值的 ThreadLocal 值时会触发此方法,默认返回 null;子类可以根据需求重写该方法来指定默认初始值。 - **存取 (`set`, `get`)** 调用 set(T value) get() 方法实际上是针对当前线程所持有的 ThreadLocalMap 执行 put get 操作。每次都会先尝试定位到对应的 entry 如果不存在则新建一个 entry 放置进去。 - **移除 (`remove`)** remove() 方法用于清理不再需要的 ThreadLocal 数据项,这有助于减少潜在的内存泄露风险。 #### 图解说明 为了更好地理解上述过程,下面给出了一张简化版的工作流程图: ![ThreadLocal 工作原理](https://img-blog.csdnimg.cn/20210718194536.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0LnRoYW5rYXNoYW4=,size_16,color_FFFFFF,t_70) 在这个过程中可以看到,当多个线程同时访问同一个 ThreadLocal 类型的对象时,它们各自拥有自己的一份独立拷贝,并不会相互干扰。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值