学习系列之并发编程(三)

『死磕Java并发编程系列』并发编程工具类之CountDownLatch 《死磕 Java 并发编程》系列连载中,大家可以关注一波: ????????『死磕Java并发编程系列』 01 十张图告诉你多线程那些破事 『死磕Java并发编程系列』 02 面试官:说说什么是Java内存模型? 『死磕Java并发编程系列』 03 面试必问的CAS原理你会了吗? 『死磕Java并发编程系列』 04 面试官:说说Atomic原子类的实现原理? ????????『死磕Java并发编程系列』 05 图解Java中那18 把锁 在日常编码中,Java 并发编程可是少不了,试试下面这些并 阅读详情

多线程队列

在 Java 多线程应用中,特别是在线程池中,队列的使用率非常高。Java 提供的线程安全队列又分为了阻塞队列和非阻塞队列。

1. 阻塞队列

在 Java 线程池中,用到了阻塞队列。当创建的线程数量超过核心线程数时,新建的任务将会被放到阻塞队列中。阻塞队列有以下四种API。操作都是一样的,以ArrayBlockingQueue为例:
在这里插入图片描述
add和remove测试:

public static void test1(){ 
        // 队列的大小 
        ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3); 
        System.out.println(blockingQueue.add("a")); 
        System.out.println(blockingQueue.add("b")); 
        System.out.println(blockingQueue.add("c")); 
        // IllegalStateException: Queue full 抛出异常! 
        System.out.println(blockingQueue.add("d")); 
        System.out.println("=-==========="); 
        System.out.println(blockingQueue.remove());
        System.out.println(blockingQueue.remove()); 
        System.out.println(blockingQueue.remove()); 
        // java.util.NoSuchElementException 抛出异常!
        System.out.println(blockingQueue.remove()); 
    }

offer和poll测试:

   //有返回值,不抛出异常的情况
    public static void test2(){ 
        // 队列的大小 
        ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3); 
        System.out.println(blockingQueue.offer("a")); 
        System.out.println(blockingQueue.offer("b")); 
        System.out.println(blockingQueue.offer("c")); 
         System.out.println(blockingQueue.offer("d")); 
         // false 不抛出异常! 
        System.out.println("============================"); 
        System.out.println(blockingQueue.poll()); 
        System.out.println(blockingQueue.poll());
        System.out.println(blockingQueue.poll()); 
        System.out.println(blockingQueue.poll()); // null 不抛出异常! 
         }

     //等待,阻塞(等待超时)
    public static void test4() throws InterruptedException {
        // 队列的大小
        ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3);
        blockingQueue.offer("a");
        blockingQueue.offer("b");
        blockingQueue.offer("c");
        // blockingQueue.offer("d",2,TimeUnit.SECONDS); // 等待超过2秒就退出
        System.out.println("===============");
        System.out.println(blockingQueue.poll());
        System.out.println(blockingQueue.poll());
        System.out.println(blockingQueue.poll());
        blockingQueue.poll(2, TimeUnit.SECONDS);
        // 等待超过2秒就退出
        }

put和take测试:

/**
     *
     * 等待,阻塞(一直阻塞)
     */
    public static void test3() throws InterruptedException {
        // 队列的大小
        ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3);
        // 一直阻塞
        blockingQueue.put("a");
        blockingQueue.put("b");
        blockingQueue.put("c");
        // blockingQueue.put("d"); // 队列没有位置了,一直阻塞
        System.out.println(blockingQueue.take());
        System.out.println(blockingQueue.take());
        System.out.println(blockingQueue.take());
        System.out.println(blockingQueue.take());
        // 没有这个元素,一直阻塞
    }

我们可以根据自己的业务需求来选择使用哪一种阻塞队列,阻塞队列通常包括以下几种:
ArrayBlockingQueue:一个基于数组结构实现的有界阻塞队列,按 FIFO(先进先出)原则对元素进行排序,使用 ReentrantLock、Condition 来实现线程安全。根据源码,需要注意创建ArrayBlockingQueue队列的时候,必须指定队列的大小。源码如下:capacity指定队列的大小,fair指定锁是公平锁还是非公平锁。

 /** Condition for waiting takes */
    private final Condition notEmpty;

    /** Condition for waiting puts */
    private final Condition notFull;
    //锁是没有分离的,即生产和消费用的是同一个锁。
   
   ......
    public ArrayBlockingQueue(int capacity) {
        this(capacity, false);
    }
    public ArrayBlockingQueue(int capacity, boolean fair) {
        if (capacity <= 0)
            throw new IllegalArgumentException();
        this.items = new Object[capacity];
        lock = new ReentrantLock(fair);
        notEmpty = lock.newCondition();
        notFull =  lock.newCondition();
    }
    public ArrayBlockingQueue(int capacity, boolean fair,
                              Collection<? extends E> c) {
        this(capacity, fair);

        final ReentrantLock lock = this.lock;
        lock.lock(); // Lock only for visibility, not mutual exclusion
        try {
            int i = 0;
            try {
                for (E e : c) {
                    checkNotNull(e);
                    items[i++] = e;
                }
            } catch (ArrayIndexOutOfBoundsException ex) {
                throw new IllegalArgumentException();
            }
            count = i;
            putIndex = (i == capacity) ? 0 : i;
        } finally {
            lock.unlock();
        }
    }

LinkedBlockingQueue:一个基于链表结构实现的阻塞队列,同样按 FIFO (先进先出) 原则对元素进行排序,使用 ReentrantLock、Condition 来实现线程安全,吞吐量通常要高于 ArrayBlockingQueue;通过源码发现,由于LinkedBlockingQueue的读写锁分离,所以效率会高于ArrayBlockingQueue。


		/** Current number of elements */
		private final AtomicInteger count = new AtomicInteger();
		
	    /** Lock held by take, poll, etc */
	    private final ReentrantLock takeLock = new ReentrantLock();
	
	    /** Wait queue for waiting takes */
	    private final Condition notEmpty = takeLock.newCondition();
	 
	     /** Lock held by put, offer, etc */
	    private final ReentrantLock putLock = new ReentrantLock();
	
	    /** Wait queue for waiting puts */
	    private final Condition notFull = putLock.newCondition();
	   
	   /**
	     * Creates a {@code LinkedBlockingQueue} with the given (fixed) capacity.
	     *
	     * @param capacity the capacity of this queue
	     * @throws IllegalArgumentException if {@code capacity} is not greater than
	     *             zero
	     */
	    public LinkedBlockingQueue(int capacity) {
	        if (capacity <= 0)
	            throw new IllegalArgumentException();
	        this.capacity = capacity;
	        last = head = new Node<E>(null);//构造链表的头尾结点,链表的初始化
	    }

以下两种作为理解
PriorityBlockingQueue:一个具有优先级的无限阻塞队列,基于二叉堆结构实现的无界限(最大值 Integer.MAX_VALUE - 8)阻塞队列,队列没有实现排序,但每当有数据变更时,都会将最小或最大的数据放在堆最上面的节点上,该队列也是使用了 ReentrantLock、Condition 实现的线程安全;
DelayQueue:一个支持延时获取元素的无界阻塞队列,基于 PriorityBlockingQueue 扩展实现,与其不同的是实现了 Delay 延时接口;
SynchronousQueue:一个不存储多个元素的阻塞队列,每次进行放入数据时, 必须等待相应的消费者取走数据后,才可以再次放入数据,该队列使用了两种模式来管理元素,一种是使用先进先出的队列,一种是使用后进先出的栈,使用哪种模式可以通过构造函数来指定。
Java 线程池 Executors 还实现了以下四种类型的 ThreadPoolExecutor,分别对应以上队列,详情如下:
在这里插入图片描述

2. 非阻塞队列

我们常用的线程安全的非阻塞队列是 ConcurrentLinkedQueue,它是一种无界线程安全队列 (FIFO),基于链表结构实现,利用 CAS 乐观锁来保证线程安全。ConcurrentLinkedQueue内部持有2个节点:head头结点,负责出列, tail尾节点,负责入列。而元素节点Node,使用item存储入列元素,next指向下一个元素节点。

    private static class Node<E> {
        volatile E item;
        volatile Node<E> next;
        //....
    }

public class ConcurrentLinkedQueue<E> extends AbstractQueue<E>
        implements Queue<E>, java.io.Serializable {
        private transient volatile Node<E> head;
        private transient volatile Node<E> tail;  
        //....
}

入列:

   public boolean offer(E e) {
        checkNotNull(e);   //为空判断,e为null是抛异常
        final Node<E> newNode = new Node<E>(e); //将e包装成newNode
        for (Node<E> t = tail, p = t;;) {  //循环cas,直至加入成功
            //t = p = tail 
            Node<E> q = p.next;
            if (q == null) {   //判断p是否为尾节点
                //如果是,p.next = newNode
                if (p.casNext(null, newNode)) {
                    //首次添加时,p 等于t,不进行尾节点更新,所以所尾节点存在滞后性  
                    //并发环境,可能存添加/删除,tail就更难保证正确指向最后节点。
                    if (p != t) 
                        //更新尾节点为最新元素
                        casTail(t, newNode);  
                    return true;
                }
            }
            else if (p == q)
                //当tail不执行最后节点时,如果执行出列操作,很有可能将tail也给移除了    
                //此时需要对tail节点进行复位,复位到head节点
                p = (t != (t = tail)) ? t : head;
            else
                //推动tail尾节点往队尾移动
                p = (p != t && t != (t = tail)) ? t : q;
        }
    }

出列:

    public E poll() {
        restartFromHead:
        for (;;) {
            for (Node<E> h = head, p = h, q;;) {
                //入列折腾的tail,那出列折腾的就是head
                E item = p.item;
                //出列判断依据是节点的item=null
                //item != null, 并且能将操作节点的item设置null, 表示出列成功
                if (item != null && p.casItem(item, null)) {
                    if (p != h) 
                        //一旦出列成功需要对head进行移动
                        updateHead(h, ((q = p.next) != null) ? q : p);
                    return item;
                }
                else if ((q = p.next) == null) {
                    updateHead(h, p);
                    return null;
                }
                else if (p == q)
                    //第一轮操作失败,下一轮继续,调回到循环前
                    continue restartFromHead;
                else
                    //推动head节点移动
                    p = q;
            }
        }
    }

ConcurrentLinkedQueue 的非阻塞算法实现可概括为下面 5 点:
1.使用 CAS 原子指令来处理对数据的并发访问,这是非阻塞算法得以实现的基础。
2.head/tail 并非总是指向队列的头 / 尾节点,也就是说允许队列处于不一致状态。 这个特性把入队 / 出队时,原本需要一起原子化执行的两个步骤分离开来,从而缩小了入队 / 出队时需要原子化更新值的范围到唯一变量。这是非阻塞算法得以实现的关键。
3.由于队列有时会处于不一致状态。为此,ConcurrentLinkedQueue 使用三个不变式来维护非阻塞算法的正确性。
4.以批处理方式来更新 head/tail,从整体上减少入队 / 出队操作的开销。
5.为了有利于垃圾收集,队列使用特有的 head 更新机制;为了确保从已删除节点向后遍历,可到达所有的非删除节点,队列使用了特有的向后推进策略。

学习《Java并发编程》目录索引 (持续更新中)      写在前面:此系列为本人学习黑马《Java并发编程》所整理的笔记 ! 不局限于应用层开发, 更加深入理解多线程, 并发编程, 更有利于我们长久发展 ! 学习Java并发编程 阅读详情

相关推荐

ESP32-S3驱动PCA9685模块:I2C扩展16路舵机控制实战指南

本文提供了ESP32-S3通过I2C总线驱动PCA9685模块扩展16路PWM输出的完整实战指南。详细解析了PCA9685模块原理、硬件连接、I2C地址配置,并给出了核心驱动代码与角度控制函数,帮助开发者解决微控制器PWM引脚不足的问题,轻松实现多路舵机控制。

weixin_32005771的博客 515

【多线程】线程池中的队列有哪些,应该如何选择

线程池中的队列是线程池的重要组成部分,用于存放待执行的任务。在Java中,线程池常用的队列类型包括无界队列、有界队列、直接提交队列(SynchronousQueue)、优先级队列等。

qq_39666711的博客 4782

matlab 三维绘图 抛光,瓷砖抛光过程建模与仿真

:瓷砖抛 光过程 建模与仿真朱 成 就(广 东工业大学 机 电工程 学院, 广东广州 )摘要 :瓷砖的抛磨影响 因素众 多 ,实际磨削加工时容易出现漏抛 、返抛现象 。对陶瓷抛光机磨头进行运动学分析 ,并基于不 同粒度号 的磨 粒在一定压力下的加工深度模型 ,探讨抛光机磨 头转 速 、横梁摆动频率 、传送速度对瓷砖抛光加工表 面均匀性 的影响,然 后结合 磨削机理使 用 对陶瓷抛光过程进行仿真 ...

weixin_36331058的博客 747

简析SynchronousQueue,LinkedBlockingQueue,ArrayBlockingQueue

SynchronousQueue SynchronousQueue是无界的,是一种无缓冲的等待队列,但是由于该Queue本身的特性,在某次添加元素后必须等待其他线程取走后才能继续添加;可以认为SynchronousQueue是一个缓存值为1的阻塞队列,但是 isEmpty()方法永远返回是true,remainingCapacity() 方法永远返回是0,remove()和removeAll() 方法永远返回是false,iterator()方法永远返回空,peek()方法永远返回null。

qq_40603010的博客 1365

VC Spyglass CDC实战:从TCL脚本到SDC约束的完整避坑指南

本文是一份针对数字IC验证工程师的VC Spyglass CDC实战指南。文章详细阐述了从TCL脚本环境搭建、SDC约束编写到典型CDC违规(如SETUP_PORT_UNCONSTRAINED)诊断与修复的完整流程,旨在帮助读者高效避坑,深入理解工具逻辑,从而完成可靠的时钟域交叉验证。

go5gopher的博客 382

Codex 100个真实案例 - 用AI做英语单词计划学习工具(记忆曲线+测验)

背单词是英语学习的基本功,但市面上的背单词 APP 要么收费、要么广告太多。本案例用 Codex 从零打造一个 **英语单词计划学习工具**,实现 SM-2 间隔重复算法(Anki 的核心算法)、单词本增删改查、多种测验模式(选择题/拼写/听写)、TTS 发音播放,以及学习数据的可视化统计图表。全程用自然语言驱动 Codex,用 Python 写后端逻辑 + 终端 UI,打造你的私人背单词助手!

yueyeguzhuo的专栏 197

Java多线程--队列(Queue)--使用/教程/实例

原文网址: 简介 本文用示例来介绍Java中的队列的用法。包括:DelayQueue,ConcurrentLinkedQueue,BlockingQueue。 文章最后有一个常见的面试题代码实例:机器要对手机按顺序做如下任务:生产、打包、发货。消费者等待收货,如何使用BlockingQueue完成? DelayQueue 简介 DelayQueue:只有当其指定的延迟时间到了,才能够从队列中获取到该元素。DelayQueue就是基于Priority...

IT利刃出鞘的博客 7365

YOLOv11损失函数原理与源码解读:从调参血泪史到源码级优化

本文深入解析YOLOv11目标检测模型的损失函数设计,揭示其相比前代的关键改进。重点剖析了回归损失中动态权重的小目标优化、分类损失的带温度系数softmax、以及目标损失的自适应阈值机制等创新点。作者通过源码分析指出常见实现陷阱,如梯度回传的正确处理方式、标签分配的匹配策略等。结合实战经验,给出针对小目标检测、类别误判等典型问题的调参建议,并分享实用的调试工具代码片段。最后强调损失函数调参需结合具体任务,指出后处理对实际部署效果的重要影响。全文以工程实践为导向,为开发者提供可直接落地的优化思路。

as1225120794的博客 439

Java_自己写的线程队列类

此类可用单态获取 然后用postThread(Runnable runnable);不停的压入线程. ThreadQueue会不停的一个一个的执行. 简单使用方法: ThreadQueue threadQueue=ThreadQueue.getThreadQueue();threadQueue.postThread(new Runnable(){ publi

taoyong1234的专栏 375

【Unity 反向运动学插件】Final IK 一套完整的IK解决方案

Final IK是Unity中一套完整的反向运动学解决方案,提供多种IK算法(CCD、FABRIK等)和功能模块,包括全身IK、VR驱动、武器瞄准、视线跟随等。其核心是通过数学方法计算关节旋转,使末端骨骼精确到达目标位置。系统支持多约束求解、旋转限制、动画烘焙及交互系统,采用模块化设计,兼顾性能与扩展性,适用于动作游戏、VR等需要高质量角色控制的场景,是Unity生态中最成熟的IK工具链之一。

2403_88403568的博客 1002

AI漫剧全套万能提示词合集

今天给大家整理一套已经跑通爆款漫剧的万能提示词库!覆盖:豆包剧本人设词 + 分镜生成词 + 即梦绘图词 + 动态优化词 + 防翻车固定词直接复制粘贴,新手也能做出稳定、高清、统一、有质感的AI漫剧!你是资深短视频漫剧编剧,擅长制作60秒竖屏爆款AI漫剧,请为我创作一集完整漫剧内容。要求:开头3秒高能抓眼,中段有冲突转折,结尾留强悬念钩子,节奏紧凑、适合短视频流量。首先生成固定统一人物人设:包含脸型、五官、发色、发型、穿搭、身材、气质、画风、光影细节,要求足够精细,保证全程角色不变脸。

欢迎来到鸾姝淡月博客,专为程序员和编程爱好者打造的技术博客。我们分享编程语言趋势、开发工具、编程哲学、职业发展等,助你掌握新技术,优化技能,激发编程热情。 1867

java多线程:线程池常用的三种队列详解!

开篇:我们在初始化线程池的时候常常会使用到如下三种队列,他们分别是: LinkedBlockingDeque(链表同步阻塞队列)、ArrayBlockingQueue(数组同步阻塞队列)、SynchronousQueue(同步阻塞队列) 他们与线程池的的maximumPoolSize参数结合使用时会有不同的效果。 通过实验来说明: 1.使用ArrayBlockingQueue (1)....

zhaoliubao1的博客 1万+

VMware系列:此平台不支持虚拟化 Intel-VT-x/EPT 或 AMD-V/RVI

VMware系列:此平台不支持虚拟化 Intel-VT-x/EPT 或 AMD-V/RVI

weixin_54626591的博客 1万+

【Go语言学习系列30】并发编程(三):select语句

详解Go语言中select语句的用法、工作原理及实际应用,包括超时处理、非阻塞通信和常见并发模式,掌握这一Go并发编程的关键组件

GopherTribe的博客 1336

IMSL CNL 7.0.0 (x86-64)Crack

IMSL CNL 7.0.0 (x86-64)Crack intel的算法库破解文件,这是C++版本的破解文件

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

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

Java并发编程学习系列三:辅助类与阻塞队列

辅助类 CountDownLatch 减法计数器,位于 java.util.concurrent 包下,我们看一下关于它的定义。 主要方法有: 首先我们看一个简单的案例: public class CountDownLatchDemo { public static void main(String[] args) throws InterruptedException { CountDownLatch countDownLatch = new CountDownLatch(6

Herishwater的博客 340

efm32 入门.pdf

本PDF共计22页,介绍的是操作驱动API,下载与否自己斟酌。

深入学习掌握JUC并发编程系列(三) -- 深入浅出管程-悲观锁

深入学习掌握JUC并发编程系列(三) -- 深入浅出管程-悲观锁

snapepotter的博客 170

汉书HE8,好用的汇编代码编写工具

汉书HE8,好用的汇编代码编写工具早期编写汇编代码所用功能强大

博客推荐

博客推荐 爱看书也爱写博客的coder,值得学习 xingoo 并发编程网 ifeve 待读文章列表 《阿里感悟》如何在三年内成长为一名技术专家 redis 特性分析 芋道源码 Java技术驿站--各种系列很全 酷壳网 《左耳听风》作者,资深程序员 转载于:https://www.cnblogs.com/paxing/p/10437106.html...

weixin_30682127的博客 120

【Go语言学习系列31】并发编程(四):sync包

详细介绍Go语言sync包提供的各种同步原语,包括Mutex、RWMutex、WaitGroup等工具的使用方法、最佳实践及常见问题,帮助你有效控制并发程序中的共享资源访问

GopherTribe的博客 1068

我用10张图总结出了这份并发编程最佳学习路线!!(建议收藏)

冰河用10张图总结出了这份并发编程最佳学习路线,强烈建议大家收藏!!

冰河的专栏 9800

【Go语言学习系列29】并发编程(二):channel基础

深入讲解Go语言channel的核心概念、使用方法与最佳实践,包括缓冲与非缓冲通道、发送接收操作、关闭通道等重要知识点,助您全面掌握Go并发编程的核心机制

GopherTribe的博客 997

如何学习Java并发编程

2012年我刚转行到互联网,那年夏天我从传统通信行业跳槽到互联网行业。那是我成年后看的第一部国产动画片,可谓惊为天人。里面有所谓的八奇技,其中有一门绝技称为:术之尽头,炁体源流。虽然目前作者还没有揭开谜底,但从定义上来说,炁体源流大概有“追本溯源,回归本质”的含义。为什么扯这些呢?很多人学不好Java并发编程,恰恰是因为没有抓住本质。

smart_an的专栏 1484

并发编程之学习总结

前言楼主自1月14号就停更了,这段时间一直在看JVM,因此没有更新任何文章,但楼主觉得有必要总结一下之前学习并发的过程,因为这一次的总结其实就是下一次的开始。主要时总结一下并发的学习路线,因为在楼主的工作环境中,并不是每天都在编写高并发的程序。然而,即使是这样,我们也要知道并发的每个知识点,这样才算一个合格的Java 程序员。那么如何系统的学习并发呢?学习路线是怎么样的呢?1. 学会开始就成功了一半

ignore 2018

【Python系列】Python 并发编程概述

💝💝💝欢迎来到我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。非常期待和您一起在这个小小的网络世界里共同探索、学习和成长。💝💝💝 ✨✨ 欢迎订阅本专栏 ✨✨。

优快云站内信: https://i.youkuaiyun.com/#/msg/chat/qyj19920704 3万+

并发编程系列之并发编程的认识

前言 今天我们开始接触并发编程的讲解,首先我们要知道并发编程的目的是什么?一个目的,为了让程序员运行的更快,但是有一点我们要知道,并不是启动越多的线程就能让程序更大限度的并发执行,在并发编程的开发中,如果希望使用更多的线程来加快程序的运行,一般我们会面临下面几种挑战:上下文切换问题,死锁、以及受硬件和软件资源的限制问题,那么我们一个个来说下吧,让我们开始今天的并发编程之旅吧。 上下文切换 什么是上下文切换? 即使是单核处理器,也支持多线程执行代码,CPU通过给每个线程分配CPU时间片来实现这个机

吭哧吭哧快乐的小牛码 2万+

【Go语言学习系列33】并发编程(六):原子操作与内存模型

深入探讨Go语言的原子操作和内存模型,包括sync/atomic包的应用、happens-before关系、内存重排以及并发陷阱的避免方法,帮助你构建高效、正确的并发程序

GopherTribe的博客 1208

Java并发编程面经大全——双非上岸阿里巴巴系列

双非本科,四面成功上岸阿里巴巴,在这里把自己整理的面经分享出来,欢迎大家阅读。 序号 文章名 超链接 1 操作系统面经大全——双非上岸阿里巴巴系列 2021最新版面经——>传送门1 2 计算机网络面经大全——双非上岸阿里巴巴系列 2021最新版面经——>传送门2 3 Java并发编程面经大全——双非上岸阿里巴巴系列 2021最新版面经——>传送门3 4 面试阿里,你必须知道的背景知识——双非上岸阿里巴巴系列 2021最新版面经——>传送门4 如有疏漏

weixin_43899069的博客 3639
上一篇: 学习系列之并发编程(二)
下一篇: 学习系列之并发编程(四)
CodersCoder
博客等级 码龄10年 90粉丝 258原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值