细说ConcurrentHashMap扩容规则

ConcurrentHashMap 扩容机制(源码解析) ConcurrentHashMap 扩容机制解析,炒鸡详细 阅读详情

当数组中存放的节点个数超过了阈值就可以任务当前的哈希冲突可能性较大,HashMap就会对当前数组进行扩容操作。在普通的HashMap中就会直接声明一个两倍大小的新数组,然后从旧数组中从左到右进行转化,但是在多线程下肯定会存在线程安全的问题。那么ConcurrentHashMap是如何来保证多线程下的扩容安全的呢?

transfer是进行扩容操作的方法,接收两个参数tab(原容器数组对象),nextTab(新容器数组对象,一般长度为tab的两倍)。

private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
    int n = tab.length, stride;
  	// stride : 单个线程需要处理的下标数
  	// 当有多个CPU参与扩容时,每个CPU处理的数量就是(n / 8 / NPU)
  	// 如果单个CPU处理的数量小于MIN_TRANSFER_STRIDE(16),就取最小值MIN_TRANSFER_STRIDE
    if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
        stride = MIN_TRANSFER_STRIDE; // subdivide range
  	// 当扩容刚开始时nextTab为null,进行初始化操作
    if (nextTab == null) {            // initiating
      	// 创建一个新的2倍长度的心数组
        try {
            @SuppressWarnings("unchecked")
            Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
            nextTab = nt;
        } catch (Throwable ex) {      // try to cope with OOME
          	//	如果扩容所需空间过大导致异常,就将阈值设置为最大值,停止扩容
            sizeCtl = Integer.MAX_VALUE;
            return;
        }
        nextTable = nextTab;
        transferIndex = n;
    }
    // 扩容后数组长度
    int nextn = nextTab.length;
  	// 定义ForwardingNode类并设置状态为MOVED,该类指向新的扩容数组
    ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
  	// 标识是否需要
    boolean advance = true;
  	// 标志扩容是否结束
    boolean finishing = false; // to ensure sweep before committing nextTab
    for (int i = 0, bound = 0;;) {
        Node<K,V> f; int fh;
      	// 循环查找需要进行处理的下标
        while (advance) {
            int nextIndex, nextBound;
          	// 当前所分配的数组区段还未完成或者扩容已完成,停止推进
            if (--i >= bound || finishing)
                advance = false;
          	// 没有需要分配的内容
            else if ((nextIndex = transferIndex) <= 0) {
                i = -1;
                advance = false;
            }
          	// 尝试获取某个区间段扩容任务
            else if (U.compareAndSwapInt
                     (this, TRANSFERINDEX, nextIndex,
                      nextBound = (nextIndex > stride ?
                                   nextIndex - stride : 0))) {
              	// 代表任务的结束下标
                bound = nextBound;
              	// 任务的起始下标(可以理解为数组的从右往左遍历)
                i = nextIndex - 1;
              	// 已找到要处理的下标,停止推进
                advance = false;
            }
        }
      	// 表示当前所需处理的数组下标不合法,可能已经完成了扩容操作
        if (i < 0 || i >= n || i + n >= nextn) {
            int sc;
          	// 已完成扩容,将用户访问的数组指向新数组,并重新设置阈值
            if (finishing) {
                nextTable = null;
                table = nextTab;
                sizeCtl = (n << 1) - (n >>> 1);
                return;
            }
          	// 在完成后将sizeCtl - 1
            if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
              	// (sc - 2) == resizeStamp(n) << RESIZE_STAMP_SHIFT 代表其他扩容执行的线程都已经执行完成,
              	// 符合就将扩容状态标志置为true
                if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                    return; 	
                finishing = advance = true;
                // 重置i等于原数组长度    重新检查所有的内容
                i = n; // recheck before commit
            }
        }
        // 如果i的位置为null,直接将null放入fwd中
        else if ((f = tabAt(tab, i)) == null)
            advance = casTabAt(tab, i, null, fwd);
      	// 如果MOVED说明已经处理过了
        else if ((fh = f.hash) == MOVED)
            advance = true; // already processed
        else {
            synchronized (f) {
              	// 其他线程没有修改执行下标元素
                if (tabAt(tab, i) == f) {
                    Node<K,V> ln, hn;
                    if (fh >= 0) {
                        int runBit = fh & n;
                        Node<K,V> lastRun = f;
                      	// 先将最后一部分进行划分
                        for (Node<K,V> p = f.next; p != null; p = p.next) {
                            int b = p.hash & n;
                            if (b != runBit) {
                                runBit = b;
                                lastRun = p;
                            }
                        }
                        if (runBit == 0) {
                            ln = lastRun;
                            hn = null;
                        }
                        else {
                            hn = lastRun;
                            ln = null;
                        }
                        for (Node<K,V> p = f; p != lastRun; p = p.next) {
                            int ph = p.hash; K pk = p.key; V pv = p.val;
                          	//	利用hash值通过头插法生成左右两个链表
                            if ((ph & n) == 0)
                                ln = new Node<K,V>(ph, pk, pv, ln);
                            else
                                hn = new Node<K,V>(ph, pk, pv, hn);
                        }
                        setTabAt(nextTab, i, ln);
                        setTabAt(nextTab, i + n, hn);
                        setTabAt(tab, i, fwd);
                        advance = true;
                    }
                    else if (f instanceof TreeBin) {
                        TreeBin<K,V> t = (TreeBin<K,V>)f;
                        TreeNode<K,V> lo = null, loTail = null;
                        TreeNode<K,V> hi = null, hiTail = null;
                        int lc = 0, hc = 0;
                        for (Node<K,V> e = t.first; e != null; e = e.next) {
                            int h = e.hash;
                            TreeNode<K,V> p = new TreeNode<K,V>
                                (h, e.key, e.val, null, null);
                            if ((h & n) == 0) {
                                if ((p.prev = loTail) == null)
                                    lo = p;
                                else
                                    loTail.next = p;
                                loTail = p;
                                ++lc;
                            }
                            else {
                                if ((p.prev = hiTail) == null)
                                    hi = p;
                                else
                                    hiTail.next = p;
                                hiTail = p;
                                ++hc;
                            }
                        }
                        ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
                            (hc != 0) ? new TreeBin<K,V>(lo) : t;
                        hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
                            (lc != 0) ? new TreeBin<K,V>(hi) : t;
                        setTabAt(nextTab, i, ln);
                        setTabAt(nextTab, i + n, hn);
                        setTabAt(tab, i, fwd);
                        advance = true;
                    }
                }
            }
        }
    }
}

扩容流程

首次开启扩容任务的线程:

  1. 调用transfer方法,其中nextTab参数为null;

  2. 根据CPU数及当前线程数,计算出每个线程所要负责的数组区间长度值stride(最小值为MIN_TRANSFER_STRIDE,16);

  3. 创建一个数组长度为原来两倍的新数组,将新数组放入nextTable变量中,并设置transferIndex为原数组长度值(转换由原数组从右往左执行);

    3.1 由于新数组长度可能过大导致出现OOM异常,说明已达到最大长度,直接将阈值设置为int的最大值,停止后续的扩容操作;

  4. 根据当前未转化区间的最大值以及stride,计算出转化的起始下标值和终止下标值;

  5. 按照起始下标值进行循环遍历转换;

  6. 创建ForwardingNode对象fwd,参数为新数组,同时节点的hash值为MOVED;

  7. 当前下标值非法;

    6.1 如果finishing为true,代表整个扩容操作已完成,将table指向新数组,nextTab设置为null,阈值sizeCtl为原数组长度的1.75倍;

    6.2 如果finishing为false,代表当前线程的扩容任务已完成,并且已无可分配的区间,将sizeCtl进行减1;

    ​ 6.2.2 当(sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT 为true,代表还有其他线程正在进行扩容操作,直接结束当前方法;

    ​ 6.2.3 反之为false,说明当前是完成扩容任务的最后一个线程,将finishing置为true,并完成后续的检查以及数组替换任务;

  8. 当前下标节点对象为null,通过cas操作将fwd对象放入该下标处;

  9. 当前下标节点的hash为MOVED,代表当前位置已完成转换,进行下一轮循环;

  10. 对当前下标节点对象加锁;

    10.1 如果对应节点对象为改变,按照HashMap的扩容思想(根据最高位hash值,分为左右两部分)进行扩容。

其他线程对扩容时的操作:

putVal

  1. 获取到对应的下标节点对象;

  2. 如果节点对象为null,直接进行将新的节点放入;

  3. 如果节点的hash值为MOVED代表该下标节点已完成扩容转化任务,帮助其他线程完成剩余扩容任务;

    3.1 获取到节点中存放的新数组对象,将sizeCtl的值+1,然后按照区间分配的原则进行扩容任务;

  4. 如果不满足上面两种情况,按照正常的插入操作进行。

get

  1. 返回null

1. ConcurrentHashMap如何保证扩容安全?

  • cas操作保证只有一个线程进行新数组的初始化工作;

  • 将整个数组分为若干个区间,每个区间都只对应单独的线程;

  • 转化前在当前下标节点上加synchronized锁;

  • 插入操作检测到对应的节点处于MOVED状态,先完成扩容在进行插入。

2. 如何判断扩容任务完成?

扩容任务刚开始时设置sizeCtl为 (rs << RESIZE_STAMP_SHIFT) + 2),后续有新的线程加入参与扩容任务就将sizeCtl + 1,线程当前分配的区间已转化完成且没有可以分配的区间,此时将sizeCtl - 1。如果最后sizeCtl 等于 (rs << RESIZE_STAMP_SHIFT) + 2) 说明扩容完成。

可维护的 SAP UI5 Theming 策略:从 CSS 级联与特性选择器到 UI Theme Designer 的工程化落地 摘要:SAP UI5主题化是一个涉及CSS级联、选择器特异性和主题参数的系统工程,与应用逻辑解耦以实现品牌化和无障碍适配。轻量级调整优先使用附加CSS,重大改动才考虑创建新主题。样式冲突需通过特异性原则解决,避免滥用!important。动态添加样式应使用addStyleClass而非直接操作DOM。主题可通过bootstrap参数、URL或运行时API设置,企业级主题推荐使用UI Theme Designer。自定义内容应消费主题变量以实现一致性,避免直接修改控件内部结构或内联样式。工程化落地需规范分层和 阅读详情

相关推荐

YOLOv8模型部署与测试:保姆级教程(含Ultralytics配置避坑指南)

本文提供了一份详尽的YOLOv8模型部署与测试指南,涵盖环境配置、模型加载、参数优化、批量处理及API服务封装等关键步骤。重点分享了Ultralytics框架在实际部署中的常见问题与解决方案,帮助开发者高效完成从训练到生产的过渡,确保模型在真实场景中稳定运行。

weixin_29038155的博客 293

ConcurrentHashMap的扩容

ConcurrentHashMap(以下简称CHM)扩容的实现非常精妙,可以说是CHM的精髓所在。它可以在不启用额外线程的情况下,借用已有的线程实现快速扩容,同时还可以保证线程安全。下面我们不妨带着下面这几个问题一块儿来看看CHM的源码。 ...

大数据时代的狂欢 7201

Flotherm XT学习历程

大家好,通过半年的热仿真软件学习使用,最终发现最好用,最好上手的软件就是Flotherm XT,对新手非常友好,但由于软件近几年才发布,网上教程非常少,后面我将在这里持续分析软件使用心得,和大家一起学习。

xiaoredred的博客 6264

ConcurrentHashMap的扩容方法详解

一、ConcurrentHashMap扩容 (jdk1.7) 扩容只针对ConcurrentHashMap中的hashEntry类型的table数组进行扩容。 方法在Segment类中:rehash() , 扩容倍数为:2倍扩容 源码解析: private void rehash(HashEntry<K,V> node) { //先将需要扩容的table数组进行拷贝 ...

Jewel’s blog 9751

手把手教你用Qt和DSP28335实现串口Bootloader,附赠源码和避坑指南

本文详细介绍了如何使用Qt和DSP28335实现串口Bootloader,包括开发环境搭建、通信协议设计、Qt上位机开发、DSP端Bootloader核心实现以及应用程序工程的特殊配置。通过实战案例和源码分享,帮助开发者快速掌握嵌入式固件升级技术,提升工业设备远程升级的效率和可靠性。

weixin_29176179的博客 134

LDO稳定性分析

根据补偿方式和拓扑结构的不同,三个极点之间的相互位置有可能发生变化,但 是 LDO 通常采用较大的输出电容 Cout,以及较大的调整管也会导致较大的 Cpar, 所以 p1、p2 通常位于较低的频率处,而 p3 和 z1 则由于 Resr较小会位于较高的频 率处。不过幸运的是,对于较大的 Cout来说,只要选择合适的 Resr,就可以将左半平面零点 Z1 的频率降低,甚至使 Z1 进入单位增益带宽内,左半平面零点的进入将对相位有所改善,这不仅可以消除振荡的风险,还可以在单位增益频率处得到较好的相位裕度。

模拟芯片设计 1593

ConcurrentHashMap

与HashMap类似,由Segment,HashEntry组成,是数组+链表构成; Segment是ConcurrentHashMap中的一个内部类: 原理:ConcurrentHashMap采用了分段锁技术,其中Segment(可以理解为一小段数组,里面含有n个HashEntry)继承ReentrantLock,但是put和get操作都会被同步处理——>每当一个线程占用锁访问Segment时,不会影响其他的Segment(可以理解为:有多把锁,一把锁锁住的临界区不会影响其他的地方) vo.

weixin_57128596的博客 2029

基于GEC6818开发板的C/C++贪吃蛇小游戏

f.开始游戏(进入游戏,并没有马上开游戏,需要点击播放才能开始)2.相关宏,全局变量(可以根据自己需要进行修改)k.显示文字前的相关初始化。k.自定义font.h文件。1.需要的主要头文件。l.在开发板上显示字1。m.在开发板上显示字2。a.显示固定大小图片。h.线程1:获得坐标。

2301_77273301的博客 2133

聊聊HashMap和ConcurrentHashMap的扩容机制

众所周知,JDK1.8对HashMap做了优化,1.8之前HashMap的结构是数组+链表,1.8之后是数组+链表 or 红黑树。ConcurrentHashMap和HashMap一样,1.8之前是Segment+HashEntry+链表,结构和HashMap类似,1.8之后是Node节点组成的数组+链表or红黑树,跟HashMap类似。最近突然兴起,看起了HashMap(以下简称HM)和ConcurrentHashMap(以下简称CHM。

qq_36220273的博客 2431

攻克JESD204B多ADC同步:从时钟对齐到数据对齐的实战解析

本文深入解析了JESD204B多ADC同步的系统性挑战与实战解决方案。从确保设备时钟同源、精确捕获SYSREF信号,到管理接收端弹性缓冲区与释放点,文章提供了从时钟对齐到数据对齐的完整调试路径与排查清单,是攻克高速数据采集系统中多片ADC同步难题的实用指南。

weixin_29231725的博客 197

深入解析 TDA4VM/VH R5F 核中断机制:VIC 与非 VIC 模式下的实战对比与优化

本文深入解析了TI TDA4VM/VH平台中ARMv7-R架构R5F核的两种中断处理机制:VIC模式与非VIC模式。通过对比硬件自动跳转与软件向量表分发的核心流程、实战代码示例及性能差异,为开发者提供了基于实时性、灵活性与调试需求的选择指南和优化建议,帮助构建更稳定的嵌入式系统。

weixin_29159441的博客 98

ConcurrentHashMap 扩容机制:高并发下的安全扩容实现

本文将深度解析 JDK1.8 ConcurrentHashMap 的扩容机制,从触发条件 、 核心流程 、 多线程协作到并发安全保障,带你吃透高并发场景下的扩容实现逻辑。

y245166349的专栏 1461

[QMT量化交易小白入门]-九十五、多因子选股和趋势择时股票量化交易策略,年化收益率40.61%

本策略采用“双层过滤”机制:第一层基于多维度财务指标进行股票池筛选(多因子选股),第二层运用均线系统和MACD指标实施动态择时(趋势跟踪)。这种设计既保证了组合的质量下限,又能灵活应对市场波动。因子配置科学性:涵盖估值、成长、盈利、动量四大类因子风险控制精细化:设置动态止损与仓位管理机制信号验证严格性:多重条件交叉验证提升胜率。

木头大左的博客 801

细说ConcurrentHashMap计数规则

对于ConcurrentHashMap而言,需要保证的是任何操作的线程安全,包括对集合元素个数的统计。 一般在多线程下要统计一个全局数量大小,可以通过cas+循环(或者直接用Atomic相关的类)的方式实现,但是ConcurrentHashMap作者却并不是这样实现的。 先看看如何获取集合中的元素个数,通过调用size()方法: public int size() { long n = sumCount(); return ((n < 0L) ? 0 : (n

ljcgit的博客 1929

VC中sqlite的使用

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

最新的sdio 4.0 spec 协议标准 带书签 高清完整PDF版

Part-E1-SDIO-Specification-Ver4.00-Final-120220.pdf:最新的sdio 4.0 spec 协议标准 带书签 高清完整PDF版,包括SDIO 接口的外设,SDIO 接口官方协议等。

Java8 ConcurrentHashMap 存储、扩容源码阅读

ConcurrentHashMap 是线程安全且高效的 HashMap。

学习笔记 1312

C# WinForm 基于DirectShow.dll编写的程序,可边预览边保存视频,音频和图片

基于微软的DirectShow.dll的二次开发1.自动识别连接的摄像头跟麦克风且可以对其进行选择(默认选中第一个)2.可录制wmv和avi两种格式的视频并且都带有音频,avi可以录制的分辨率和帧数进行选择,wmv默认进行了淸晰度优化如不喜欢可去除代码中有相关提示3.可在预览点开之后进行拍照,也可边录制视频边拍照4.添加了视频和音频的压缩功能(酌情使用)这次二次开发发现C#对DirectShow的相关信息太少,这次做完也不敢藏私共享出来,还望大家指正

ConcurrentHashMap如何实现扩容机制之学习笔记

1 addCount transfer扩容 首先判断是否需要扩容,也就是当更新后的键值对总数baseCount>=阀值(sizeCtl)时,则重新进行hash;扩容进行两个阶段: 1.有一个线程正在扩容,另一个线程进来协助扩容; 2.如果当前没有扩容,直接触发扩容操作; private final void addCount(long x, int check) { ... if (chec...

weixin_42022924的博客 3966

Statistics and Machine Learning Toolbox

Statistics and Machine Learning Toolbox(带书签),MATLAB统计学习工具

hashmap hashtable ConcurrentHashMap 扩容

hashmap扩容: 默认大小: 16, 增长因子:0.75 扩容点规则(什么时候扩容): 16*0.75=12 存储方式:数组+链表 扩容方法:扩容为原来的2倍 出现于:JDK1.2 扩容条件: 1、判断当前个数是否大于等于阈值 2、当前存放是否发生哈希碰撞 hashtable扩容: 默认大小: 11, 增长因子:0.75 扩容点规则(什么时候扩容): 11*0.75...

那些年的代码 203

ConcurrentHashMap在扩容的过程中又有新的数据写入是怎么处理的

在扩容过程中处理新数据写入的机制是其高并发性能的关键设计之一。它采用了一套精巧的策略,确保在扩容(rehashing)这个重量级操作进行时,读写操作依然可以高效、安全地并发执行。在扩容期间,依然能高效地处理新的读写请求,实现了“现在回答核心问题:当扩容正在进行,又有新的。(JDK 1.8+)最核心的设计。”的目标,这是其高并发性能的基石。操作(写入)发生时,

mnhpo_Q的博客 696

面试必问:ConcurrentHashMap 的扩容优化,看完这篇就懂了

核心思想:将一次性的全量扩容转化为多次的增量迁移,即 **“化整为零”**。技术手段:利用双表存储(旧表 + 新表)和写时拷贝思想,在并发操作中完成数据迁移。最终收益:解决了高并发场景下,因单次扩容导致的put操作长时间阻塞问题,保证了 ConcurrentHashMap 的性能稳定性。这是 Java 并发编程中解决 “热点操作” 的经典范式,也是面试中考察 ConcurrentHashMap 实现原理的最高频考点之一。

yy20030501的博客 497

ConcurrentHashMap1.8 多线程扩容机制

ConcurrentHashMap 1.8 彻底重构了扩容逻辑,,采用实现,把扩容性能瓶颈从 O (n) 压到极致,是高并发场景下的核心优化。我用。

2401_88060344的博客 389

JUC1.8-ConcurrentHashMap源码学习-为什么每次扩容是原来两倍?

##前言 这个问题是扩容里面的一个遗留问题,前因后果大家可以去查阅上篇博客JUC1.8-ConcurrentHashMap源码学习-扩容方法解析transfer() ,在将扩容机制时,篇幅过长,因此单独拿出来说明; 大家看过源码或者是读过笔者扩容那篇博客,就知道在每次扩容时,均在原容器的基础上,扩大2倍,贴下transfer()源码里面的位置: if (nextTab == null) { ...

盘码客 1448

扩容是元素还是数组_Java集合系列-ConcurrentHashMap-扩容机制的全面解析

本人是工作7年的老程序员,在头条分享我对Java运用和源码、各种框架运用和源码的认识和理解,如果对您有所帮助,请持续关注。声明:所有的文章都是自己工作之余一个字一个字码上去的,希望对学习Java的同学有所帮助,如果有理解不到位的地方,欢迎交流。上一篇文章我对ConcurrentHashMap的非常重要的put方法做了一个全面的解析,但是遗留了两个也非常重要的知识点:扩容和线程安全,接下来两篇文章就...

weixin_39624461的博客 146

ConcurrentHashMap 详解

前言 ConcurrentHashMap。 以下的技术点都基于JDK1.8~ 基础回顾 我们都知道,从JDK1.8起,ConcurrentHashMap底层的数据结构就已经从原来的Segment分段锁变为了数组 + 链表 + 红黑树的形态。 它是一款并发容器,一款装数据的容器在并发环境下铁定就会有各种各样的问题。你在单线程环境下玩单机,并发环境下就会有别的线程和你抢数据,抢桶位。因此编写JUC包的大神Doug Lea也都为这些场景一一做了适配,可以说是绝对的并发安全,至少运行了这么多年了也没遇

一瓶绿茶三元钱 2139

hashmap的底层设计原理以及扩容规则,是否线程安全,如何线程安全。

举个例子,当一个线程使用put方法时,另一个线程不但不可以使用put方法,连get方法都不可以。注意:链表的长度大于8 且 数组长度大于64转换为红黑树(如果数组长度小于64,即使链表长度超过 8,也不会进行树华,而是会进行扩容操作):HashMap 的初始容量是 0,这是一种懒加载机制,直到第一次 put 操作才会初始化数组大小,默认大小是 16。,但是不扩容的话会造成拉链的长度越来越长,导致散列表的时间复杂度会倾向于 O(n) 而不是 O(1)。JDK1.8 中,在多线程环境下,会发生数据覆盖的情况。

wadjdkdkdjjd的博客 1191

HashMap和ConcurrentHashMap对比源码分析

为了能让存储更加高效,尽量的避免key冲突,让数据尽量均匀的进行分布,因此采用了hash值计算的方式,hash值的范围为-2147483648 到 2147483647。因此当负载因子为1.0时,在进行扩容时,会出现更多的hash冲突,可能导致链表长度或红黑树高度会变得更长或更高,导致查询效率的降低。长度为8的时候,平均查找长度为3,如果继续使用链表,平均查找长度为8/2=4,这才有转换为树的必要。链表长度如果是小于等于6,6/2=3,虽然速度也很快的,但是转化为树结构和生成树的时间并不会太短。

XING的博客 1166
上一篇: 细说ConcurrentHashMap计数规则
下一篇: MySQL 锁机制系列(一) —— Next-Key Lock
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值