【050】Thread.interrupt () 是杀线程?3 层底层逻辑扒光它的真面目

在这里插入图片描述


📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌

📙 作者: 编程技术圈(哇哥面试陪跑)
👉 欢迎关注、分享、评论
✔️ 持续分享更多干货内容
🌐🌏🌎➕tcmeta, 欢迎沟通交流

📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌


零、引入

王二的额角沁出一层汗,手指抖着点向屏幕 —— 他写的文件下载线程占着 IO 资源不肯停,调用 Thread.interrupt () 想 “一刀砍死” 它,结果线程没断,反而抛出 InterruptedException,下载的文件成了残缺的碎片。

“这 interrupt 到底是个什么妖物!说好的中断线程,结果把我代码搅得稀烂!” 王二的怒吼震得桌上的搪瓷缸嗡嗡响。

隔壁的哇哥正用旧报纸包炒花生,闻言慢悠悠转过头,花生壳的碎屑落在桌上:“你把 interrupt 当铡刀,可它不过是递了张‘停工通知’—— 工人(线程)接不接,停不停,全看它自己;你硬逼它停,自然要闹脾气。”

在这里插入图片描述

点赞 + 关注,跟着哇哥把 Thread.interrupt () 的底层扒得底朝天,3 层逻辑戳破 “中断 = 杀死” 的谎言,下次再用这方法,保准让线程服服帖帖。

一、王二的 “铡刀代码”:把 interrupt 用成 “夺命符”

王二写的文件下载线程,循环读取网络流,他觉得线程 “不听话”,就直接调用 interrupt (),结果全乱了套。代码如下,透着股 “一刀切” 的蛮横:

package cn.tcmeta.interrupt;


import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import java.util.concurrent.TimeUnit;

/**
 * @author: laoren
 * @description: 王二的坑:把interrupt当“杀死线程”的铡刀
 * @version: 1.0.0
 */
public class InterruptMisuseSample {

    // 下载线程:从网络读取文件
    private static class DownloadThread extends Thread {
        private final String url;
        private ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

        public DownloadThread(String url) {
            super("Download-Thread");
            this.url = url;
        }

        @Override
        public void run() {
            try {
                URLConnection conn = new URL(url).openConnection();
                try (InputStream in = conn.getInputStream()) {
                    byte[] buffer = new byte[1024];
                    int len;
                    // 循环读取数据(王二想让这里立刻停止)
                    while ((len = in.read(buffer)) != -1) {
                        outputStream.write(buffer, 0, len);
                        // 模拟下载耗时
                        TimeUnit.MILLISECONDS.sleep(10);
                    }
                    System.out.println("下载完成:文件大小=" + outputStream.size() + "字节");
                }
            } catch (IOException e) {
                e.printStackTrace();
            } catch (InterruptedException e) {
                // 王二以为捕获异常就完事,结果文件没写完
                System.out.println("线程被中断,下载失败!");
                e.printStackTrace();
            }
        }

        // 获取下载的字节数据
        public byte[] getContent() {
            return outputStream.toByteArray();
        }
    }


    static void main() throws InterruptedException {
        // 下载一个大文件(示例用百度首页,实际可换大文件地址)
        DownloadThread downloadThread = new DownloadThread("https://www.baidu.com");
        downloadThread.start();

        // 下载200ms后,王二觉得太慢,强行中断
        TimeUnit.MILLISECONDS.sleep(200);
        System.out.println("主线程:强制中断下载线程!");
        downloadThread.interrupt();

        // 等待线程结束,查看文件大小
        downloadThread.join();
        System.out.println("最终文件大小:" + downloadThread.getContent().length + "字节");
    }
}

运行结果:线程没 “死”,文件残缺
在这里插入图片描述
王二瘫在椅子上,百思不解:“interrupt 不是中断线程吗?怎么只打断了 sleep,读取数据的循环还跑了半截?”

哇哥把包好的花生往桌上一放,纸包发出窸窣的响:“你这是犯了新手最常犯的错 —— 把 interrupt 当成‘杀死线程’的命令,可它的本质是‘设置中断标记’+‘唤醒阻塞线程’。线程在运行中时,interrupt 只给它贴个‘要中断’的标签;只有线程在 sleep、wait、join 这些阻塞状态时,才会抛异常 —— 就像你给工人递停工通知,他正干活时,顶多瞥一眼通知,继续干;只有他在歇着(阻塞)时,才会被通知吵醒,闹着要停工。”

二、用 “工厂停工” 讲透 interrupt:是 “通知” 不是 “命令”

在这里插入图片描述

哇哥拽过王二桌上的草稿纸,画了个歪歪扭扭的工厂车间,一个工人在搬货,墙上贴个 “停工通知” 的标签 —— 这是他的拿手好戏,再深的底层逻辑,到他手里都能变成车间里的家常事。

“线程就像工厂里的工人,CPU 是工位,interrupt 是车间主任递的‘停工通知’,” 哇哥指着草稿纸,“核心就三点:

  • 贴标签:调用 interrupt (),先给工人(线程)贴个‘待停工’的标签(中断标记置为 true);
  • 唤阻塞:如果工人正歇着(线程在 sleep/wait/join 等阻塞状态),就把他叫醒,还甩给他一张‘被打断’的单子(抛 InterruptedException),同时把标签撕了(中断标记置为 false);
  • 不强制:工人接不接通知,停不停工,全看他自己 —— 要是他埋头干活不看标签,就算贴满通知,照样接着干。”

他用铅笔圈出 “中断标记” 四个字:“这是 interrupt 的核心 ——Thread 类里有个 volatile 的 interruptedStatus 字段(JVM 层面的标记),interrupt () 的底层就是操作这个标记,再处理阻塞状态。”

// interrupt status (read/written by VM)
volatile boolean interrupted;

在这里插入图片描述

三、底层逻辑三层拆解(王二记在烟盒内侧)

王二掏出皱巴巴的烟盒,用铅笔歪歪扭扭地记,字里行间都是底层真相:

在这里插入图片描述

❓ 第一层:中断标记的存储(Java 层面)

Thread 类里有个private volatile boolean interrupted;(JDK 源码里实际是 JVM 维护的 interruptedStatus 字段),这是个 volatile 变量,保证多线程可见性 —— 调用 interrupt (),本质是把这个标记置为 true;调用 interrupted () 会清除标记,isInterrupted () 只读取不清除。

在这里插入图片描述

public static boolean interrupted() {
    return currentThread().getAndClearInterrupt();
}

在这里插入图片描述

public boolean isInterrupted() {
    return interrupted; // 只读取
}

➡️ 第二层:interrupt () 的核心行为(JVM 层面)

JDK 源码里,Thread.interrupt () 的核心逻辑如下(简化版):

public void interrupt() {
   // Setting the interrupt status must be done before reading nioBlocker.
   interrupted = true;
   interrupt0();  // inform VM of interrupt

   // thread may be blocked in an I/O operation
   if (this != Thread.currentThread()) {
       Interruptible blocker;
       synchronized (interruptLock) {
           blocker = nioBlocker;
           if (blocker != null) {
               blocker.interrupt(this);
           }
       }
       if (blocker != null) {
           blocker.postInterrupt();
       }
   }
}

JVM 执行 interrupt0 () 时,会做两件事:

  • 给线程设置中断标记;
  • 如果线程处于阻塞状态(比如调用了 park ()),就唤醒线程。

👉 第三层:阻塞线程的中断处理(OS 层面)

当线程在 sleep/wait/join 时,会陷入操作系统的 “阻塞态”,JVM 调用 OS 的中断机制(比如 Linux 的 pthread_kill)唤醒线程,同时清除中断标记,抛出 InterruptedException—— 这是 OS 和 JVM 协同的结果,目的是让阻塞线程能响应中断。

四、代码示例:把 interrupt 用对,让线程 “优雅停工”

哇哥拿过王二的鼠标,说:“想让线程听话,得两步走:一是调用 interrupt () 贴标签,二是让线程自己检查标签,优雅停工。这三个示例,把 interrupt 的用法扒得明明白白。”

✅ 示例 1:正确终止运行中的线程(检查中断标记)

package cn.tcmeta.interrupt;

import java.util.concurrent.TimeUnit;


/**
 * @author: laoren
 * @description: 正确用法:线程主动检查中断标记,优雅终止
 * @version: 1.0.0
 */
public class InterruptCorrectSample {
    // 任务线程:循环处理数据,主动检查中断标记
    private static class TaskThread extends Thread {
        public TaskThread() {
            super("Task-Thread");
        }

        @Override
        public void run() {
            long count = 0;
            while (!Thread.currentThread().isInterrupted()) {
                // 模拟处理数据
                count++;
                if (count % 10000000 == 0) {
                    System.out.println(this.getName() + ": 已处理" + count / 10000 + "万次");
                }
                // 每处理一定次数后才休眠,避免过早被中断
                if (count % 1000000 == 0) {
                    try {
                        TimeUnit.MICROSECONDS.sleep(1);
                    } catch (InterruptedException e) {
                        System.out.println(this.getName() + ": 阻塞时被中断,准备终止");
                        // 恢复中断状态并在退出前完成最后一次计数
                        Thread.currentThread().interrupt();
                        count++; // 补偿中断时的处理
                        break;
                    }
                }
            }
            System.out.println(this.getName() + ": 优雅终止,最终处理了" + count / 10000 + "万次");
        }
    }

    static void main() throws InterruptedException {
        TaskThread taskThread = new TaskThread();
        taskThread.start();

        // 运行3秒后,发送中断通知
        TimeUnit.SECONDS.sleep(3);
        System.out.println("主线程: 发送中断通知");
        taskThread.interrupt();

        // 等待线程终止
        taskThread.join();
        System.out.println("主线程: 任务线程已终止");
    }
}

在这里插入图片描述
哇哥解释:“关键是两点:一是循环条件检查 isInterrupted (),二是捕获 InterruptedException 后重置标记 —— 因为异常会清除标记,不重置的话,循环会继续跑。就像工人被叫醒后,把通知撕了,你得重新贴一张,他才知道要停工。”

💯 示例 2:interrupted () 和 isInterrupted () 的区别(面试必考)

王二总把这两个方法弄混,哇哥干脆写了段对比代码,让他一眼看明白:

package cn.tcmeta.interrupt;


import java.util.concurrent.TimeUnit;

/**
 * @author: laoren
 * @description:  对比:interrupted() vs isInterrupted()
 * @version: 1.0.0
 */
public class InterruptMethodCompare {
    static void main() {
        Thread thread = new Thread(() -> {
            // 循环检查标记
            while (!Thread.interrupted()) { // 注意:用的是Thread.interrupted()
                try {
                    TimeUnit.MILLISECONDS.sleep(100);
                } catch (InterruptedException e) {
                    // 异常会清除标记,interrupted()会返回false,循环继续
                    System.out.println("线程被中断,interrupted()=" + Thread.interrupted());
                }
            }
            System.out.println("线程终止");
        }, "Test-Thread");

        thread.start();

        try {
            TimeUnit.SECONDS.sleep(1);
            thread.interrupt(); // 发送中断通知
            TimeUnit.SECONDS.sleep(1);
            // 查看isInterrupted()(不清除标记)
            System.out.println("主线程查看:isInterrupted()=" + thread.isInterrupted());
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

在这里插入图片描述
核心区别(王二抄在小本本上)

在这里插入图片描述
“简单说,interrupted () 是‘看一眼,撕标签’,isInterrupted () 是‘看一眼,留标签’,” 哇哥敲着桌子,“面试时要是把这俩弄混,面试官立马知道你没真用过。”

🔔 示例 3:终止阻塞中的线程(IO 阻塞的特殊处理)

王二的下载线程卡在 IO 读取上,哇哥教他用 interrupt + 关闭流的方式终止:

package cn.tcmeta.interrupt;


import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import java.util.concurrent.TimeUnit;

/**
 * @author: laoren
 * @description: 终止IO阻塞的线程:interrupt+关闭流
 * @version: 1.0.0
 */
public class InterruptIOThreadSample {

    private static class DownloadThread extends Thread {
        private final String url;
        private final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        private InputStream in; // 保存输入流,用于手动关闭

        public DownloadThread(String url) {
            super("Download-Thread");
            this.url = url;
        }

        @Override
        public void run() {
            try {
                URLConnection conn = new URL(url).openConnection();
                in = conn.getInputStream();
                byte[] buffer = new byte[1024];
                int len;
                // 循环读取,同时检查中断标记
                while ((len = in.read(buffer)) != -1 && !Thread.currentThread().isInterrupted()) {
                    outputStream.write(buffer, 0, len);
                    TimeUnit.MILLISECONDS.sleep(10);
                }
                if (Thread.currentThread().isInterrupted()) {
                    System.out.println("线程检测到中断,主动终止下载");
                } else {
                    System.out.println("下载完成:文件大小=" + outputStream.size() + "字节");
                }
            } catch (IOException e) {
                System.out.println("IO流被关闭,下载终止");
            } catch (InterruptedException e) {
                System.out.println("线程休眠时被中断,准备终止");
                Thread.currentThread().interrupt(); // 重置标记
            } finally {
                // 终止前关闭流,释放资源
                if (in != null) {
                    try {
                        in.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }

        public byte[] getContent() {
            return outputStream.toByteArray();
        }
    }

    static void main() throws InterruptedException {
        DownloadThread downloadThread = new DownloadThread("https://www.baidu.com");
        downloadThread.start();

        // 200ms后中断线程
        TimeUnit.MILLISECONDS.sleep(200);
        System.out.println("主线程:中断下载线程并关闭流");
        downloadThread.interrupt();

        // 等待线程终止
        downloadThread.join();
        System.out.println("最终文件大小:" + downloadThread.getContent().length + "字节");
    }
}

  • 执行结果
主线程:中断下载线程并关闭流
线程检测到中断,主动终止下载
最终文件大小:0字节

Process finished with exit code 0

“IO 阻塞比较特殊,interrupt () 只能贴标记,没法直接唤醒,” 哇哥解释,“所以得手动检查标记,关闭流 —— 就像工人卡在搬货的仓库里,光贴通知没用,得把仓库门打开(关流),他才能出来。”

五、面试必问:Thread.interrupt () 核心题(附答案)

✔️ 面试题 1:Thread.interrupt () 的底层原理是什么?它会直接杀死线程吗?

用 “工厂停工” 的例子答,面试官一听就懂:

底层原理:

  • 调用 interrupt () 时,JVM 先给线程设置 “中断标记”(interruptedStatus=true);
    若线程处于 sleep/wait/join 等阻塞状态,JVM 会唤醒线程,清除中断标记,抛出 InterruptedException;
  • 底层通过 native 方法 interrupt0 () 实现,最终调用操作系统的线程中断机制(如 Linux 的 pthread_kill)。

不会直接杀死线程:

  • interrupt 只是 “中断通知”,线程是否终止,取决于是否检查中断标记、处理中断异常 —— 就像递停工通知,工人不看通知,照样干活。

👉 面试题 2:InterruptedException 抛出后,中断标记会怎样?为什么?

  • 标记会被清除(置为 false);
  • 原因:JVM 的设计初衷是让线程有 “重新开始” 的机会 —— 异常表示线程 “被打断”,但标记清除后,线程可以选择继续运行,而不是被标记绑定死;若需要保留中断状态,需在 catch 块中调用 Thread.currentThread ().interrupt () 重置标记。

📢 面试题 3:Thread.interrupted () 和 Thread.isInterrupted () 的区别?

  • 核心是 “是否清除标记”+“是否静态”:
    • Thread.interrupted():静态方法,查看当前线程的中断标记,读取后立即清除标记(置为 false);
    • Thread.isInterrupted():实例方法,查看指定线程的中断标记,只读取不清除;
      举例:线程 A 调用 interrupt () 后,isInterrupted () 返回 true;调用 interrupted () 后返回 true,再次调用则返回 false(标记已清)。

六、总结:interrupt 核心心法(王二编的顺口溜)

王二把核心知识点编成顺口溜,贴在键盘上,字丑但好记:

  • interrupt 不杀线程,只贴标记加提醒;
  • 阻塞线程抛异常,标记清零要记清;
  • 运行线程查标记,优雅终止才正经;
  • interrupted 清标记,isInterrupted 只看行;
  • IO 阻塞特殊办,关流中断两并行。

王二用优化后的代码替换了之前的下载程序,调用 interrupt () 后,线程检查标记,关闭 IO 流,优雅终止,再也没出现文件残缺的情况。领导看了测试报告,拍着他的肩膀说:“这代码,终于不像野路子了。”

王二拿着报告去找哇哥,脸上笑开了花。哇哥剥了颗花生,慢悠悠说出那句收尾的话:

哇哥说:“技术这东西,最忌望文生义。你把 interrupt 当‘杀线程’的刀,自然会被它反咬;你把它当‘递通知’的信,懂它的脾气,顺它的逻辑,它才会听你的话。这世上本没有难用的 API,只有不肯花心思懂它的人 —— 就像待人,你懂他,他才懂你。”

在这里插入图片描述
在这里插入图片描述

<think>我们正在讨论Boost线程库中的thread::interrupt()的工作原理。根据用户提供的引用和问题,我们需要详细解释Boost线程中断机制。 引用[2]提到Java线程中断机制:当目标线程在sleep时被中断,会清除中断状态并抛出异常。如果异常未被处理,则后续循环中中断状态被清除,线程继续执行。Boost线程中断机制有类似之处。 引用[3]展示了一个使用线程中断的示例:在close()方法中调用了thread.interrupt()来中断线程。 Boost线程中断机制的核心思想是:一个线程可以通过调用另一个线程interrupt()方法来请求中断。被中断的线程会在特定的中断点(interruption points)抛出boost::thread_interrupted异常。如果该异常没有被捕获,线程将终止;如果被捕获,线程可以继续执行。 关键点: 1. 中断点(Interruption Points):Boost线程库中,一些阻塞函数(如sleep, wait, join等)被设计为中断点。当线程在这些函数中阻塞时,如果收到中断请求,这些函数会抛出boost::thread_interrupted异常。 2. 中断请求:调用线程对象的interrupt()方法,设置目标线程的中断标志(interrupt flag)。 3. 中断检查:当线程运行到中断点时,会检查中断标志,如果被设置,则清除中断标志并抛出boost::thread_interrupted异常。 注意:中断请求是异步的,但实际中断发生在中断点处。 示例代码: ```cpp #include <boost/thread.hpp> #include <iostream> void worker_thread() { try { // 循环执行任务 while (true) { // 模拟工作 boost::this_thread::sleep(boost::posix_time::seconds(1)); std::cout << "Working..." << std::endl; } } catch (const boost::thread_interrupted&) { std::cout << "Thread interrupted. Exiting..." << std::endl; } } int main() { boost::thread t(worker_thread); // 主线程等待一段时间 boost::this_thread::sleep(boost::posix_time::seconds(5)); // 中断工作线程 t.interrupt(); // 等待线程结束 t.join(); return 0; } ``` 在这个例子中: 1. 工作线程在一个循环中每隔1秒打印一条消息。 2.线程在5秒后中断工作线程3. 当工作线程在sleep(这是一个中断点)时,收到中断请求,sleep函数抛出boost::thread_interrupted异常。 4. 异常被捕获,线程退出循环,然后线程结束。 注意:如果线程执行的函数中没有中断点,那么即使调用了interrupt()线程也不会被中断。因此,在长时间运行的任务中,可以适当插入中断点,或者通过调用boost::this_thread::interruption_point()手动设置中断点。 例如: ```cpp void long_running_task() { while (true) { // 执行一些计算密集型任务 // ... // 手动插入中断点 boost::this_thread::interruption_point(); } } ``` 这样,在循环中每次都会检查中断请求,如果有中断请求则抛出异常。 总结: 1. 中断请求通过调用线程对象的interrupt()方法发出。 2. 中断实际发生在中断点(包括Boost提供的阻塞函数和手动插入的中断点)。 3. 中断点会抛出boost::thread_interrupted异常,线程可以通过捕获该异常来执行清理工作并退出。 §§相关问题§§ 1. 如何在线程中安全地处理中断异常? 2. 除了sleep,Boost线程库中还有哪些函数是中断点? 3. 如何自定义中断点? 4. 中断机制与取消线程有何区别?</think>### Boost Thread Interrupt 原理详解 Boost 线程库中的 `thread.interrupt()` 提供了一种协作式中断机制,允许一个线程请求另一个线程停止执行。其工作原理基于**中断点(interruption points)**和**异常处理**机制,以下是详细分析: #### 核心原理 1. **中断请求机制** 当调用 `thread.interrupt()` 时,Boost 会为目标线程设置一个**中断标志位**(非即时中断)。这个操作本身不会立即停止线程,而是设置一个状态标记[^2]。 2. **中断点检查** 线程在执行到特定的**中断点函数**时(如 `sleep()`, `wait()`, `join()` 等),会自动检查中断标志位: ```cpp boost::this_thread::sleep_for(boost::chrono::seconds(1)); // 中断点 ``` 3. **异常触发** 若检测到中断标志位被设置,中断点函数会: - 清除中断标志位 - 抛出 `boost::thread_interrupted` 异常[^2] 4. **异常处理** 线程通过捕获异常实现优雅退出: ```cpp try { while(true) { /* 工作循环 */ } } catch (const boost::thread_interrupted&) { // 清理资源并退出 } ``` #### 关键特性 1. **协作式中断** 线程必须主动检查中断点才能响应中断请求,不同于强制终止(如 `terminate()`),这种设计能避免资源泄漏[^3]。 2. **中断点函数** | 函数类型 | 示例函数 | |-----------------------|----------------------------------| | 等待函数 | `sleep()`, `wait()`, `join()` | | 条件变量 | `condition_variable::wait()` | | 显式检查点 | `boost::this_thread::interruption_point()` | 3. **中断状态管理** - `interrupt()`:设置中断标志 - `interruption_requested()`:检查中断状态 - 抛出异常后自动清除标志 #### 完整示例 ```cpp #include <boost/thread.hpp> #include <iostream> void worker_thread() { try { while (true) { // 显式中断检查点(推荐) boost::this_thread::interruption_point(); std::cout << "Working...\n"; boost::this_thread::sleep_for(boost::chrono::milliseconds(500)); } } catch (const boost::thread_interrupted&) { std::cout << "Thread interrupted gracefully\n"; // 资源清理操作 } } int main() { boost::thread t(worker_thread); // 让工作线程运行3秒 boost::this_thread::sleep_for(boost::chrono::seconds(3)); // 发送中断请求 t.interrupt(); // 等待线程结束 t.join(); return 0; } ``` #### 注意事项 1. **非中断点代码** 纯计算型循环无法被中断: ```cpp while(true) { /* 无中断点 */ } // 无法响应中断 ``` 2. **异常处理必须** 未捕获 `thread_interrupted` 会导致线程终止: ```cpp // 危险:未处理中断异常 void unsafe_thread() { while(true) { boost::this_thread::sleep_for(boost::chrono::seconds(1)); } } ``` 3. **资源安全** 应在 catch 块中释放资源: ```cpp catch (const boost::thread_interrupted&) { db_connection.close(); // 关闭数据库连接 file_handle.release(); // 释放文件句柄 } ``` 4. **中断重置** 捕获异常后若需继续运行,需重置中断状态: ```cpp catch (const boost::thread_interrupted&) { boost::this_thread::disable_interruption di; // 临时禁用中断 // 执行恢复操作 } ``` #### 与Java线程中断对比 参考引用[2]的Java示例,Boost 的实现与其高度相似: - 都是通过标志位 + 异常机制 - 都需要在特定检查点响应中断 - 未处理的异常会导致线程无法终止[^2] > ⚠️ **重要区别**:Boost 的 `interrupt()` 不保证立即生效,线程必须主动协作才能响应中断请求。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值