LockSupport分析

Java多线程系列(五)—LockSupport源码分析 Java多线程系列(五)—LockSupport源码分析 LockSupport类是实现锁机制和其他同步类的基础,提供了基本线程阻塞和恢复的原语; LockSupport中的park() 和 unpark() 的作用分别是阻塞线程和解除阻塞线程,而且park()和unpark()不会遇到“Thread.suspend 和 Thread.resume所可能引发的死锁”问题 阅读详情

LockSupport

LockSupport是一个线程阻塞工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞,当然阻塞之后肯定得有唤醒的方法。源码如下:

public class LockSupport {
    private LockSupport() {} // Cannot be instantiated.

    private static void setBlocker(Thread t, Object arg) {
        UNSAFE.putObject(t, parkBlockerOffset, arg);
    }
    
    /**
     * 返回提供给最近一次尚未解除阻塞的 park 方法调用的 blocker 对象。
     * 如果该调用不受阻塞,则返回 null。
     * 返回的值只是一个瞬间快照,即由于未解除阻塞或者在不同的 blocker 对象上受阻而具有的线程。
     */
    public static Object getBlocker(Thread t) {
        if (t == null)
            throw new NullPointerException();
        return UNSAFE.getObjectVolatile(t, parkBlockerOffset);
    }
    
    /**
     * 如果给定线程的许可尚不可用,则使其可用。
     * 如果线程在 park 上受阻塞,则它将解除其阻塞状态。
     * 否则,保证下一次调用 park 不会受阻塞。
     * 如果给定线程尚未启动,则无法保证此操作有任何效果。 
     * @param thread: 要执行 unpark 操作的线程;该参数为 null 表示此操作没有任何效果。
     */
    public static void unpark(Thread thread) {
        if (thread != null)
            UNSAFE.unpark(thread);
    }

    /**
     * 为了线程调度,在许可可用之前阻塞当前线程。 
     * 如果许可可用,则使用该许可,并且该调用立即返回;
     * 否则,为线程调度禁用当前线程,并在发生以下三种情况之一以前,使其处于休眠状态:
     *  1. 其他某个线程将当前线程作为目标调用 unpark
     *  2. 其他某个线程中断当前线程
     *  3. 该调用不合逻辑地(即毫无理由地)返回
     */
    public static void park() {
        UNSAFE.park(false, 0L);
    }

    /**
     * 和park()方法类似,不过增加了等待的相对时间
     */
    public static void parkNanos(long nanos) {
        if (nanos > 0)
            UNSAFE.park(false, nanos);
    }

    /**
     * 和park()方法类似,不过增加了等待的绝对时间
     */
    public static void parkUntil(long deadline) {
        UNSAFE.park(true, deadline);
    }
    
    /**
     * 和park()方法类似,只不过增加了暂停的同步对象
     * @param blocker 导致此线程暂停的同步对象
     * @since 1.6
     */
    public static void park(Object blocker) {
        Thread t = Thread.currentThread();
        setBlocker(t, blocker);
        UNSAFE.park(false, 0L);
        setBlocker(t, null);
    }
    
    /**
     * parkNanos(long nanos)方法类似,只不过增加了暂停的同步对象
     * @param blocker 导致此线程暂停的同步对象
     * @since 1.6
     */
    public static void parkNanos(Object blocker, long nanos) {
        if (nanos > 0) {
            Thread t = Thread.currentThread();
            setBlocker(t, blocker);
            UNSAFE.park(false, nanos);
            setBlocker(t, null);
        }
    }
    
    /**
     * parkUntil(long deadline)方法类似,只不过增加了暂停的同步对象
     * @param blocker 导致此线程暂停的同步对象
     * @since 1.6
     */
    public static void parkUntil(Object blocker, long deadline) {
        Thread t = Thread.currentThread();
        setBlocker(t, blocker);
        UNSAFE.park(true, deadline);
        setBlocker(t, null);
    }

    static final int nextSecondarySeed() {
        int r;
        Thread t = Thread.currentThread();
        if ((r = UNSAFE.getInt(t, SECONDARY)) != 0) {
            r ^= r << 13;   // xorshift
            r ^= r >>> 17;
            r ^= r << 5;
        }
        else if ((r = java.util.concurrent.ThreadLocalRandom.current().nextInt()) == 0)
            r = 1; // avoid zero
        UNSAFE.putInt(t, SECONDARY, r);
        return r;
    }

    // Hotspot implementation via intrinsics API
    private static final sun.misc.Unsafe UNSAFE;
    private static final long parkBlockerOffset;
    private static final long SEED;
    private static final long PROBE;
    private static final long SECONDARY;
    static {
        try {
            UNSAFE = sun.misc.Unsafe.getUnsafe();
            Class<?> tk = Thread.class;
            parkBlockerOffset = UNSAFE.objectFieldOffset
                (tk.getDeclaredField("parkBlocker"));
            SEED = UNSAFE.objectFieldOffset
                (tk.getDeclaredField("threadLocalRandomSeed"));
            PROBE = UNSAFE.objectFieldOffset
                (tk.getDeclaredField("threadLocalRandomProbe"));
            SECONDARY = UNSAFE.objectFieldOffset
                (tk.getDeclaredField("threadLocalRandomSecondarySeed"));
        } catch (Exception ex) { throw new Error(ex); }
    }
}

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

public class LockSupportDemo {

    public static Object u = new Object();
    static ChangeObjectThread t1 = new ChangeObjectThread("t1");
    static ChangeObjectThread t2 = new ChangeObjectThread("t2");

    public static class ChangeObjectThread extends Thread {
        public ChangeObjectThread(String name) {
            super(name);
        }
        @Override public void run() {
            synchronized (u) {
                System.out.println("in " + getName());
                LockSupport.park();
                if (Thread.currentThread().isInterrupted()) {
                    System.out.println("被中断了");
                }
                System.out.println("继续执行");
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        t1.start();
        Thread.sleep(1000L);
        t2.start();
        Thread.sleep(3000L);
        t1.interrupt();// 中断也可以立即使park方法返回
        LockSupport.unpark(t2);
        t1.join();
        t2.join();
    }
}

LockSupport 提供park()和unpark()方法实现阻塞线程和解除线程阻塞,LockSupport和每个使用它的线程都与一个许可(permit)关联。permit相当于1,0的开关,默认是0,调用一次unpark就变成1,调用一次park会消费permit, 也就是将1变成0,同时park立即返回。再次调用park会变成block(因为permit为0了,会阻塞在这里,直到permit变为1), 这时调用unpark会把permit置为1。每个线程都有一个相关的permit, permit最多只有一个,重复调用unpark也不会积累。

park()和unpark()不会有 Thread.suspend 和 Thread.resume 所可能引发的死锁问题,由于许可的存在,调用 park 的线程和另一个试图将其 unpark 的线程之间的竞争将保持活性。即使先调用unpark,再调用park也不会产生问题,结果是不会产生阻塞(优于wait、notify)

如果调用线程被中断,则park方法会返回,无论中断发生在park调用之前还是之后,park方法都会返回(然而又不是简单的判断isInterrpted这个状态值,具体细节逻辑得看hotspot源码,这里不做深究)

同时park也拥有可以设置超时时间的版本。三种形式的 park 还各自支持一个 blocker 对象参数。此对象在线程受阻塞时被记录,以允许监视工具和诊断工具确定线程受阻塞的原因。(这样的工具可以使用方法 getBlocker(java.lang.Thread) 访问 blocker。)建议最好使用这些形式,而不是不带此参数的原始形式。在锁实现中提供的作为 blocker 的普通参数是 this。

看下线程dump的结果来理解blocker的作用。
在这里插入图片描述从线程dump结果可以看出:
有blocker的可以传递给开发人员更多的现场信息,通过jstack命令可以非常方便的监控具体的阻塞对象,方便定位问题。所以java6新增加带blocker入参的系列park方法,替代原有的park方法。

Unsafe的park和unpark

LockSupport实际上是调用了Unsafe类里的函数,归结到Unsafe里,只有两个函数:

/**
 * 为指定线程提供“许可(permit)”
 */
public native void unpark(Thread jthread);

/**
 * 阻塞指定时间等待“许可”。
 * @param isAbsolute: 时间是绝对的,还是相对的
 * @param time:等待许可的时间
 */
public native void park(boolean isAbsolute, long time);  

上面的这个“许可”是不能叠加的,“许可”是一次性的。

比如线程B连续调用了三次unpark函数,当线程A调用park函数就使用掉这个“许可”,如果线程A再次调用park,则进入等待状态。
注意,unpark函数可以先于park调用。比如线程B调用unpark函数,给线程A发了一个“许可”,那么当线程A调用park时,它发现已经有“许可”了,那么它会马上再继续运行。

HotSpot源码

class Parker : public os::PlatformParker {  
private:  
  volatile int _counter ;  
  ...  
public:  
  void park(bool isAbsolute, jlong time);  
  void unpark();  
  ...  
}  
class PlatformParker : public CHeapObj<mtInternal> {  
  protected:  
    pthread_mutex_t _mutex [1] ;  
    pthread_cond_t  _cond  [1] ;  
    ...  
}  

可以看到Parker类实际上用Posix的mutex,condition来实现的。在Parker类里的_counter字段,就是用来记录所谓的“许可”的。

当调用park时,先尝试直接能否直接拿到“许可”,即_counter>0时,如果成功,则把_counter设置为0,并返回:

void Parker::park(bool isAbsolute, jlong time) {  
  // Ideally we'd do something useful while spinning, such  
  // as calling unpackTime().  
  
  
  // Optional fast-path check:  
  // Return immediately if a permit is available.  
  // We depend on Atomic::xchg() having full barrier semantics  
  // since we are doing a lock-free update to _counter.  
  if (Atomic::xchg(0, &_counter) > 0) return;  

如果不成功,则构造一个ThreadBlockInVM,然后检查_counter是不是>0,如果是,则把_counter设置为0,unlock mutex并返回(这里有两段代码都检查了当前线程是否是中断,但是这应该是区别于java Thread中的判断方法的,不细究)
在这里插入图片描述否则,再判断等待的时间,然后再调用pthread_cond_wait函数等待,如果等待返回,则把_counter设置为0,unlock mutex并返回:

if (time == 0) {  
  status = pthread_cond_wait (_cond, _mutex) ;  
}  
_counter = 0 ;  
status = pthread_mutex_unlock(_mutex) ;  
assert_status(status == 0, status, "invariant") ;  
OrderAccess::fence();  

当unpark时,则简单多了,直接设置_counter为1,再unlock mutext返回。如果_counter之前的值是0,则还要调用pthread_cond_signal唤醒在park中等待的线程:

void Parker::unpark() {  
  int s, status ;  
  status = pthread_mutex_lock(_mutex);  
  assert (status == 0, "invariant") ;  
  s = _counter;  
  _counter = 1;  
  if (s < 1) {  
     if (WorkAroundNPTLTimedWaitHang) {  
        status = pthread_cond_signal (_cond) ;  
        assert (status == 0, "invariant") ;  
        status = pthread_mutex_unlock(_mutex);  
        assert (status == 0, "invariant") ;  
     } else {  
        status = pthread_mutex_unlock(_mutex);  
        assert (status == 0, "invariant") ;  
        status = pthread_cond_signal (_cond) ;  
        assert (status == 0, "invariant") ;  
     }  
  } else {  
    pthread_mutex_unlock(_mutex);  
    assert (status == 0, "invariant") ;  
  }  
}  

简而言之,是用mutex和condition保护了一个_counter的变量,当park时,这个变量置为了0,当unpark时,这个变量置为1。

对比wait/notify

  1. wait/notify是以对象为纬度,阻塞当前的线程和唤醒单个(随机)或者所有线程。LockSuport主要是针对Thread进行阻塞处理,阻塞当前线程和唤醒指定单个线程。
  2. 方法需要在synchronized块中执行,必须由锁对象调用;LockSupport.park()可以在任意地方执行
  3. wait/notify机制需要注意两个方法的调用顺序,比如线程B要用notify通知线程A,那么线程B要确保线程A已经在wait调用上等待了,否则线程A可能永远都在等待,且notify无法通知指定的单个线程。park/unpark模型真正解耦了线程之间的同步,线程之间不再需要一个Object或者其它变量来存储状态,不再需要关心对方的状态。
  4. wait会释放锁资源,而LockSuport.park()不会

对比Thread.sleep()

  1. 从功能上来说,Thread.sleep()和LockSupport.park()方法类似,都是阻塞当前线程的执行,且都不会释放当前线程占有的锁资源;
  2. Thread.sleep()必须指定时间,没法从外部唤醒,只能到时间自己醒过来;中途可能发生线程中断则会抛出中断异常

Condition.await()

Condition.await()底层是调用LockSupport.park()来实现阻塞当前线程的。

实际上,它在阻塞当前线程之前还干了两件事,一是把当前线程添加到条件队列中,二是“完全”释放锁,也就是让state状态变量变为0,然后才是调用LockSupport.park()阻塞当前线程

Tornado高并发处理方法实例代码 本文主要分享的是一则关于Tornado高并发处理方法的实例,具体如下: #!/bin/env python # -*- coding:utf-8 -*- import tornado.httpserver import tornado.ioloop import tornado.options import tornado.web import tornado.gen from tornado.concurrent import run_on_executor from concurrent.futures import ThreadPoolExecutor import time from 立即下载

相关推荐

博途S7-1200与昆仑通态MCGS纯仿真联调实战指南

本文详细介绍了博途S7-1200与昆仑通态MCGS纯仿真联调的实战指南,包括环境搭建、配置步骤、通信桥接及故障排除。通过TIA Portal和MCGS Pro搭建的仿真环境,可实现80%以上的基础功能验证,适用于工业自动化学习和开发,大幅降低硬件投入成本。

weixin_26834815的博客 439

lucky带你玩转高并发Tornado框架

Tornado是使用python编写的一个强大的、可扩展的web服务器。它在处理严峻的网络流量时表现的足够强健,但却在创建和编写时有着足够的轻量级,并且能够被用在大量的应用和工具中。 不同于那些最多只能达到10000个并发连接的传统网络服务器,Tornado在设计之初就考虑到了性能因素,旨在解决C10K问题,这样的设计使得其成为一个拥有非常高性能的框架。此外,它还拥有处理安全性、用户验证、社交网络以及外部服务(如数据库和网站API)进行异步交互的工具。很多公司都把tornado作为面试的一项必备技能

手把手教你用ADIsimCLK和GUI搞定HMC7044时钟配置(附避坑指南)

本文详细解析了HMC7044时钟芯片的配置方法,通过ADIsimCLK和HMC7044 Configuration GUI工具,从频率规划到硬件验证全流程实战演示。特别针对PLL模式选择、分频链配置等易错环节,提供量产验证的配置模板和实测数据,帮助工程师快速掌握高性能时钟解决方案。

weixin_30755393的博客 807

c++11总结11——半同步半异步线程池

线程池要需要解决的问题 传统的方式处理大并发任务时,一个请求对应一个线程来处理,大量的线程创建和销毁会消耗过多的系统资源,还增加了线程上下文切换的开销,线程池技术就是来处理这个问题的。 线程池 线程池技术通过在系统中预先创建一定数量的线程,当任务请求到来时从线程池中分配一个预先创建好的线程来处理任务,线程在处理完任务后还可以重用,不会被销毁,而是等待下一次任务的到来。 线程池的优点 1. 对于多核处理器,由于线程会被分配到多个CPU,会提高并行处理的效率; 2. 每个线程独立阻塞,可以防止主线程

www_dong的博客 544

VMware ESXi +DELL虚拟化存储磁盘容量处于紧急状态问题的处理

1、环境介绍 exsi server两台(DELL R730 ): 192.168.3.206 VMware EXSi6.5 192.168.3.207 VMware EXSi6.5 磁盘存储柜一台(DELL SC2000): 192.168.3.208 2、故障处理 由于前期规划的卷空间过大,虚拟机文件逐年增大以后,造成卷的磁盘占用过与整个磁盘空间的容量,虚拟化平台可用容量爆掉,DELL SC2000自动切换到紧急状态,禁止所有磁盘I/O,exsi主机上的虚拟机无法开机 首先,安装

wolf1105的博客 3533

⭐⭐⭐全网最详细YOLOv8教程,从环境配置到自己训练,再到数据集的制作,包的各位⭐⭐⭐

问题原因:当你“pip install ultralytics” 时,torch和torchvision会作为它的依赖一起安装了,当执行“pip install torch torchvision torchaudio --index-url。”时,会卸载torch,然后再安装torch+cuda版本,但是不会自动卸载torchvision已有版本,然后再安装torchvision+cuda版本。🎉🎉🎉恭喜各位道友,已经可以训练模型了,现在就是制作自己的数据集,所谓申神通秘境,随心所欲,万物皆可识别也

weixin_74840239的博客 7493

python tornado高并发_tornado IO并发真的很高么?

首先,你可以更详细的描述一下apache 的部署,tornado 的部署。比如,torando 默认是单线程单进程的,也就是只能利用单核,apache 多线程是可以利用多核的,如果你没使用对,这个是有问题的。其次,平台,torando 在windows 和linux下区别还是有的,epoll 和select 的区别,这个区别有多大我就不知道了。另外呢,tornado 并发很高,指的是他的事件循环,...

weixin_30670053的博客 1350

C#与海康威视工业相机结合OpenCV与WPF:打造高效的机器视觉应用程序

本文介绍了基于C#、海康威视工业相机SDK和OpenCV的机器视觉应用开发方法。首先配置开发环境,包括海康SDK、OpenCvSharp和WPF项目设置。重点阐述了如何通过SDK初始化相机、获取视频流,并将YUV数据转换为OpenCV可处理的Mat格式进行图像处理(如边缘检测)。同时展示了如何用WPF实现界面交互和实时图像显示。该方案为工业自动化、质量检测等场景提供了高效的技术实现路径,结合了海康相机的高性能采集、OpenCV的强大算法和WPF的交互优势,具有较高的实用价值。

这里只有干货:从 Modbus 通信到 MES 对接,从 YOLO 训练到工控机部署,带你搞定工业软件开发全流程。 1499

4-Tornado高并发原理

【代码】4-Tornado高并发原理。

linjun的博客 956

【LAMMPS学习】五、LAMMPS命令(1) LAMMPS 输入脚本

本部分描述了 LAMMPS 输入脚本的格式以及其中的命令如何用于定义 LAMMPS 模拟。主要包括以下内容。

荷塘阅色 2645

Unity必学:Protobuf-net预编译实战

本文深入解析Unity项目使用Protobuf-net Precompile的必要性。作为高效跨平台序列化方案,Protobuf-net解决了Unity工程在网络同步、存档系统中的性能瓶颈,特别是针对iOS等AOT平台的兼容问题。通过预编译模式,将运行时反射转为静态代码生成,显著提升移动端性能表现,支持协议平滑升级,已成为大厂项目的标配方案。文章从原理、实践到多平台适配,全面展示了这一"序列化发动机"如何支撑现代Unity项目的数据传输需求。

本博客聚焦游戏开发技巧、创业实战心得及大学热门课程干货。这里既有技术深度,也有思维广度,无论你是开发者、创业者还是高校学子,都能在这里找到适合自己的成长之路! 495

LockSupport常用方法源码分析

本文将介绍LockSupport类中的方法和部分源码,以及面试常问到的一个小问题,感兴趣的大佬可以指点下。

小威的博客 3万+

国科大《人工智能原理与算法》2021年期末试题+2024经验(2025指南)

2021年期末试题+24年复习指南

MatsumotoChrikk 2929

LockSupport 详解及源码分析

LockSupport是Java并发包中的核心工具类,提供线程阻塞(park)和唤醒(unpark)能力。相比wait/notify机制,它无需同步块、支持精准线程控制、具有许可机制(先唤醒后阻塞不卡死)和更好的中断处理。其核心是通过二元许可(0/1)控制线程状态,park消耗许可,unpark设置许可。底层使用Unsafe类实现,最终通过操作系统原语(如Linux的futex)挂起/唤醒线程。LockSupport是构建高级同步器的基础,具有更灵活、更可靠的线程控制能力。

csdn_tom_168的博客 1260

android 蓝牙通讯demo

android 下蓝牙通讯demo,实现了发现蓝牙,使能蓝牙,通过蓝牙发送消息。

廖雪峰Python教程[完整版]

廖雪峰的python教程整理的,已经排好版了,而且还是完整的,适合小白

LockSupport源码分析(JDK1.8)

1、LockSupport 介绍 LockSupport 和 CAS 是Java并发包中很多并发工具控制机制的基础,它们底层其实都是依赖Unsafe实现。 LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。LockSupport 提供park()和unpark()方法实现阻塞线程和解除线程阻塞,LockSupport和每个使用它的线程都与一个许可(permit)关联。permi...

啦卡的博客 255

win10 跳过U8环境检测的IE WebControls.rar

win10 安装u8的时候 提示IE WebControls 不通过,这个工具可以欺骗u8的检查。 跳过U8环境检测的IE WebControls。提示导入失败后,重新检测通过。

jvm源码分析之LockSupport

概述 LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。LockSupport 提供park()和unpark()方法实现阻塞线程和解除线程阻塞,LockSupport和每个使用它的线程都与一个许可(permit)关联。permit相当于1,0的开关,默认是0,调用一次unpark就加1变成1,调用一次park会消费permit, 也就是将1变成0,同时park立即返回。再次调用...

lzf的博客 4873

VC中sqlite的使用

在VC中对sqlite的使用,实现数据库大批数据的快速插入,读取数据库中的数据在ListCtrl中显示

LockSupport实现线程挂起和唤醒——深入浅出原码分析

面试题 (1)LockSupport比Object的wait/notify有两大优势,分别是什么? (2)LockSupport源码是如何实现的,具体说说你的看法? (1)LockSupport比Object的wait/notify有两大优势,分别是什么? LockSu...

chuanyingcao2675的博客 448

LockSupport源码详细分析

一. LockSupport类介绍 前面分析中,阻塞和唤醒线程都会使用到LockSupport工具来完成相应工作,LockSupport定义了一组公共静态方法,这些方法提供了最基本的线程阻塞和唤醒公共,而LockSupport也成为构建同步组件的基础工具。 LockSupport定义了一组以park开头的方法用来阻塞当前线程,以及upark方法用来唤醒线程。这些方法如下所示: 方法名称...

UckyK的专栏 370

LockSupport源码阅读与分析

LockSupport底层实现

红尘之一骑的专栏 1787

LockSupport的park/unpark分析

https://blog.youkuaiyun.com/u013978512/article/details/120011860?spm=1001.2014.3001.5501这篇文章我们讲了AQS的实现过程,其中线程的阻塞和唤醒是通过LockSupport的park和unpark实现的,结尾抛出了个问题:唤醒在阻塞之前,会出现什么情况。答案是unpark可以在park之前,这样,被park的线程就不会阻塞,为了查看其中的缘由,发现https://www.cnblogs.com/yonghengzh/p/14280..

u013978512的博客 647

LockSupport的一则问题(群友问题分析)

群友的问题是想写个锁,他的代码如下: public class TestLockSupport implements Runnable{ private AtomicReference sign = new AtomicReference<>(); // 获取锁 public void lock() { Thread current = Thread

大别山放牛娃的编程二三事 552

LockSupport(park/unpark)源码分析

concurrent包是基于AQS (AbstractQueuedSynchronizer)框架的,AQS框架借助于两个类: Unsafe(提供CAS操作) LockSupport(提供park/unpark操作) 因此,LockSupport非常重要。 两个重点 (1)操作对象 归根结底,LockSupport.park()和LockSupport.unpark(Thread thre...

lyf_ldh的博客 499

LockSupport原理分析

目录 一. LockSupport类介绍 二. LockSupport类示例 三. LockSupport源码注释 一. LockSupport类介绍 LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程。主要是通过park()和unpark(thread)方法来实现阻塞和唤醒线程的操作的。 每个线程都有一个许可(permit),permit只有两个值1和0,默认是0。 ...

凡是过往,皆为序章 282
上一篇: git 常用命令记录
下一篇: 栈和队列
MrMoving
博客等级 码龄10年 54粉丝 145原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值