滚雪球学Java(84):告别阻塞:Java并发集合带你轻松处理多线程问题

  咦咦咦,各位小可爱,我是你们的好伙伴——bug菌,今天又来给大家普及Java之数组篇啦,别躲起来啊,听我讲干货还不快点赞,赞多了我就有动力讲得更嗨啦!所以呀,养成先点赞后阅读的好习惯,别被干货淹没了哦~


🏆本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!

环境说明:Windows 10 + IntelliJ IDEA 2021.3.2 + Jdk 1.8

前言

  身为开发者,相比其他语言,Java作为一种广泛应用于企业级开发的编程语言,其强大的并发处理能力是其核心优势之一。在并发编程中,很多时候需要处理多个线程对共享数据的访问和修改,这就需要使用并发集合来保证线程安全。本文将介绍Java中的一些常用并发集合,以及它们的应用场景、优缺点分析等内容,最后通过一个示例演示,辅助同学们加深理解。

摘要

  本文我主要会介绍Java中的并发集合,包括ConcurrentHashMapCopyOnWriteArrayListBlockingQueue等集合。通过源代码解析,讲解了它们的实现原理和使用方法。同时,还给出了一些具体的应用场景案例,并对各个集合的优缺点进行了分析。最后,给出了一些类代码方法介绍和测试用例,以帮助读者更好地理解和应用这些并发集合。

概述

  众所周知,并发集合是为了解决多线程环境下共享数据的访问和修改问题而设计的,而传统的集合类在多线程环境下是线程不安全的,因为多个线程同时对集合进行操作可能会导致数据的不一致性,这不就是典型的线程不安全表征之一。而并发集合则通过加锁或其他同步机制来保证线程安全。在多线程环境下,使用并发集合可以提高程序的性能和效率,那么具体是在怎么一回事呢,同学们请继续往下看。

  对于Java本身而言,它就提供了一些常用的并发集合类,比如ConcurrentHashMap、CopyOnWriteArrayList和BlockingQueue等。这些集合类都是线程安全的,可以被多个线程同时访问和修改,如果你遇到并发问题,无妨可以将一些集合改成如上线程安全的集合之一,但是具体选择哪一种,这又是一种深度考察了。

并发集合解读

  这里,我就将如上提到的三种线程安全的集合进行逐一介绍,给大家科普并深入拓展下,以便于大家在遇到该问题能更好的择优选择。

ConcurrentHashMap

  首先,对于ConcurrentHashMap集合,这个应该是见到过最多的,也是大家最为熟悉的集合之一了吧。它之所以线程安全,在JDK1.7中,它的数据结构是由一个Segment(实现了ReentrantLock)数组和多个HashEntry组成,Segment数组的意义就是将一个大的table分割成多个小的table来进行加锁,这样能保证线程安全,每一个Segment元素存储的是HashEntry数组+链表,这个和HashMap的数据存储结构一样。而在JDK1.8中,它已经摒弃了Segment的概念,而是直接用数组+链表+红黑树的数据结构来实现,并发控制使用Synchronized和CAS来操作,这里大家就当做作为个基础了解,需要深入地同学可以去扒扒源码。所以基于ConcurrentHashMap,它的put、get、remove等方法操作都是原子的,可以被多个线程同时执行。

  那么最简单的问题,它如何使用,举个示例:

ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("key", 1);
map.get("key");
map.remove("key");

  使用上跟传统的map集合毫无区别,关键就在于底层的实现了,想深挖的同学们,可以去专研下它的源码,也不是特别难懂。

CopyOnWriteArrayList

  接下来就是CopyOnWriteArrayList,它是 Java 中的一个线程安全的列表实现,它属于 java.util.concurrent 包。这个类的行为在多线程环境中特别有用,因为它提供了一种在迭代和修改列表时避免迭代器快速失败的方法。CopyOnWriteArrayList 的工作原理是在每次修改操作(如添加、删除元素)时创建一个新的数组实例,并将修改后的元素复制到这个新数组中。这种复制机制确保了在修改操作进行时,其他线程仍然可以安全地读取列表。

  基于它的使用,也非常简单,举个示例:

CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();
list.add(1);
list.get(0);
list.remove(0);

  对于上述简单使用示例代码,这里我给同学们拓展几点:

  • 第一点:list.add(1) 向列表中添加了一个元素 1。由于这是列表中的第一个元素,它会被添加到索引 0 的位置。在内部中,CopyOnWriteArrayList会创建一个新的数组,并将元素 1 放入这个数组中。
  • 第二点:list.get(0) 从列表中获取索引为 0 的元素。由于列表中只有一个元素,并且该元素位于索引 0,所以这个方法将返回 1。在内部,CopyOnWriteArrayList 会提供一个只读视图的数组,以便线程可以安全地访问列表中的元素。
  • 第三点:list.remove(0) 从列表中移除索引为 0 的元素。在这种情况下,由于列表中只有一个元素,这个操作将导致列表变为空。CopyOnWriteArrayList 会创建一个新的空数组来表示修改后的列表,并更新内部的引用以指向这个新数组。

  所以,需要注意的是,由于 CopyOnWriteArrayList 在每次修改操作时都会创建一个新的数组并复制元素,因此在元素数量较多或者修改操作频繁的情况下,这种数据结构可能会有性能问题。此外,由于数组复制涉及到大量的内存操作,所以在内存使用方面也需要注意优化。

  在实际应用中,如果预期的修改操作不是非常频繁,或者不需要在多个线程之间共享列表实例,那么使用非线程安全的 ArrayList 可能会有更好的性能。如果需要线程安全,但是读操作远多于写操作,那么 CopyOnWriteArrayList 是一个不错的选择。所以说,任何事物都具有两面性,更好的选择择优才是关键。

BlockingQueue

  BlockingQueue 是 Java 并发包 java.util.concurrent 中的一个接口,它代表了一个线程安全的队列,可以在阻塞操作中等待队列中的元素,ArrayBlockingQueueBlockingQueue 接口的一个具体实现,它是一个有界队列,创建时需要指定容量。它提供了put、take等操作来实现线程之间的同步;当队列为空时,take操作会阻塞线程,直到队列不为空;当队列已满时,put操作会阻塞线程,直到队列有空闲位置。

BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10);
queue.put(1);
queue.take();

  对于上述简单使用BlockingQueue的示例代码,这里我给同学们拓展几点:

  • 第一点:new ArrayBlockingQueue<>(10) 创建了一个容量为 10 的有界队列。这意味着队列最多可以存储 10 个元素。
  • 第二点:queue.put(1) 将整数 1 放入队列。如果队列已满,这个操作会阻塞,直到队列中有空闲位置。在 ArrayBlockingQueue 中,put 方法是一个阻塞操作,如果队列已满,它会等待直到有空间可用。
  • 第三点:queue.take() 从队列中取出并移除头部元素。如果队列为空,这个操作会阻塞,直到队列中有元素可用。take 方法也是一个阻塞操作,它确保了即使在没有可用元素的情况下,线程也会等待直到有元素被放入队列。

  所以,ArrayBlockingQueue 的使用场景包括但不限于生产者-消费者问题,其中生产者线程使用 put 方法放入元素,而消费者线程使用 take 方法取出元素。由于 ArrayBlockingQueue 是有界的,它还可以帮助防止内存中数据的无限制增长。

  需要注意的是,put 和 take 方法都是阻塞方法,如果队列满或者空,相应的线程将会进入等待状态。在设计并发系统时,应该考虑到这种等待可能对系统性能和响应时间的影响。此外,还可以使用 offer 和 poll 等非阻塞方法作为替代,这些方法在无法立即执行操作时会立即返回,而不是进入等待状态。

应用场景案例

以下是几个使用并发集合的应用场景案例:

  1. 在多线程环境下进行缓存操作时,可以使用ConcurrentHashMap来保存和获取缓存数据,保证线程安全。

  2. 在多线程环境下进行队列操作时,可以使用BlockingQueue来实现生产者-消费者模型,保证线程之间的同步。

  3. 在多线程环境下进行列表操作时,可以使用CopyOnWriteArrayList来保证线程安全,避免数据的不一致性。

优缺点分析

ConcurrentHashMap

优点:

  • 线程安全,可以被多个线程同时访问和修改。
  • 支持高并发的读操作。
  • 内部使用锁分段技术,可以提高并发性能。

缺点:

  • 写操作的性能相对较低。
  • 无法保证某些操作的原子性,如putIfAbsent。

CopyOnWriteArrayList

优点:

  • 线程安全,可以被多个线程同时访问和修改。
  • 支持高并发的读操作。
  • 写操作的性能较好,适用于读多写少的场景。

缺点:

  • 内存占用较大,每次写操作都会复制整个数组。

BlockingQueue

优点:

  • 线程安全,可以实现生产者-消费者模型。
  • 提供了put、take等阻塞操作,可以进行线程之间的同步。

缺点:

  • 需要手动处理线程的阻塞和唤醒。

类代码方法介绍

ConcurrentHashMap的常用方法

  ConcurrentHashMap 是 Java 中提供的一个线程安全的哈希表,它在高并发场景下提供了较好的性能。以下是 ConcurrentHashMap 的一些常用方法:

  1. 构造方法

    • ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel): 创建一个具有指定初始容量、负载因子和并发级别(即预计同时访问的线程数)的 ConcurrentHashMap
    • ConcurrentHashMap(int initialCapacity): 创建一个具有指定初始容量和默认负载因子(0.75)以及默认并发级别的 ConcurrentHashMap
    • ConcurrentHashMap(Map<? extends K,? extends V> m): 使用给定的映射初始化 ConcurrentHashMap,所有键值对都直接添加到新的 ConcurrentHashMap 中。
  2. put 方法

    • V put(K key, V value): 将指定的值与此映射中的指定键关联。如果映射之前已经包含该键的映射关系,则旧值会被替换。
    • V putIfAbsent(K key, V value): 如果指定的键不存在于映射中,则将键和值添加到映射中并返回 null。如果键已存在,则不进行任何操作并返回旧值。
    • V replace(K key, V value): 如果指定的键存在于映射中,则用新值替换其对应的值,并返回旧值。如果键不存在,则不进行任何操作并返回 null
    • boolean replace(K key, V oldValue, V newValue): 如果指定的键存在于映射中,并且其值等于旧值,则用新值替换旧值并返回 true。如果键不存在或值不等于旧值,则不进行任何操作并返回 false
  3. get 方法

    • V get(Object key): 从映射中获取与指定键关联的值。如果键不存在于映射中,则返回 null
    • V getOrDefault(Object key, V defaultValue): 从映射中获取与指定键关联的值。如果键不存在于映射中,则返回默认值。
  4. remove 方法

    • V remove(Object key): 从映射中移除与指定键关联的值(如果存在)。返回被移除的值,如果键不存在于映射中,则返回 null
    • boolean remove(Object key, Object value): 从映射中移除与指定键关联的值,如果键和值都匹配。如果成功移除了键值对,则返回 true
    • V putIfAbsent(K key, V value): 如果指定的键不存在于映射中,则将键和值添加到映射中并返回 null。如果键已存在,则不进行任何操作并返回旧值。
  5. contains 方法

    • boolean containsKey(Object key): 如果映射包含指定键的映射关系,则返回 true
    • boolean containsValue(Object value): 如果映射包含指定值的键值对,则返回 true
  6. size 方法

    • int size(): 返回映射中的键值对数量。
  7. isEmpty 方法

    • boolean isEmpty(): 如果映射不包含任何键值对,则返回 true
  8. clear 方法

    • void clear(): 从映射中移除所有的键值对。
  9. keySet 方法

    • Set<K> keySet(): 返回映射中所有键的集合视图。

  这些方法提供了对 ConcurrentHashMap 进行操作的基本工具,包括添加、获取、更新和删除键值对,以及对映射进行条件查询和修改。在使用这些方法时,需要注意它们的行为和性能特点,尤其是在多线程环境下。

CopyOnWriteArrayList的常用方法

  CopyOnWriteArrayList 是 Java 中的一个线程安全的列表类,它属于 java.util.concurrent 包。这个类通过在每次修改操作时复制底层数组来提供并发安全性。以下是 CopyOnWriteArrayList 的一些常用方法:

  1. 构造方法

    • CopyOnWriteArrayList(): 创建一个空的列表。
    • CopyOnWriteArrayList(Collection<? extends E> c): 创建一个包含指定集合所有元素的列表。
  2. 添加元素

    • boolean add(E e): 将指定的元素添加到列表的尾部。
    • void add(int index, E element): 在列表的指定位置插入一个元素。
    • boolean addAll(Collection<? extends E> c): 将指定集合的所有元素添加到列表的尾部。
    • boolean addAll(int index, Collection<? extends E> c): 将指定集合的所有元素插入到列表的指定位置。
  3. 获取元素

    • E get(int index): 获取列表中指定位置的元素。
    • int indexOf(Object o): 返回指定元素在列表中第一次出现的索引,如果不存在则返回 -1
    • int lastIndexOf(Object o): 返回指定元素在列表中最后一次出现的索引,如果不存在则返回 -1
  4. 更新元素

    • E set(int index, E element): 替换列表中指定位置的元素。
    • void replaceAll(UnaryOperator<E> operator): 将列表中的每个元素都替换为操作的结果。
  5. 删除元素

    • boolean remove(Object o): 从列表中移除指定元素的第一个匹配项。
    • E remove(int index): 从列表中移除指定位置的元素并返回被移除的元素。
    • boolean removeAll(Collection<?> c): 从列表中移除指定集合中包含的所有元素。
    • boolean retainAll(Collection<?> c): 保留列表中指定集合中包含的所有元素,移除其他所有元素。
  6. 大小和容量

    • int size(): 返回列表中的元素数量。
    • boolean isEmpty(): 如果列表不包含任何元素,则返回 true

  CopyOnWriteArrayList 适用于读操作远多于写操作的场景,因为每次写操作都需要复制整个底层数组,这可能会导致性能开销。然而,对于读操作,由于迭代器不会抛出 ConcurrentModificationException 异常,所以可以安全地在迭代过程中进行读取操作。

BlockingQueue的常用方法

  BlockingQueue 是 Java 并发 API 中的一个接口,它扩展了 Queue 接口并添加了阻塞操作,使得线程在队列为空时能够等待元素的到来,或者在队列已满时能够等待空间的释放。以下是 BlockingQueue 接口的一些常用方法:

  1. 插入操作

    • put(E e): 将指定的元素插入队列尾部,如果队列已满,则等待空间可用。
    • offer(E e): 尝试将元素插入队列尾部,如果队列已满,则立即返回 false 而不等待。
    • add(E e): 将元素插入队列尾部,如果队列已满,则根据 Queue 接口的行为抛出 IllegalStateException
    • offer(E e, long timeout, TimeUnit unit): 尝试在指定的时间内将元素插入队列,如果队列已满,则等待直到超时。
  2. 移除操作

    • take(): 移除并返回队列头部的元素,如果队列为空,则等待元素的到来。
    • poll(): 移除并返回队列头部的元素,如果队列为空,则立即返回 null
    • remove(): 移除并返回队列头部的元素,如果队列为空,则根据 Queue 接口的行为抛出 NoSuchElementException
    • poll(long timeout, TimeUnit unit): 尝试在指定的时间内移除并返回队列头部的元素,如果队列为空,则等待直到超时。
  3. 检查操作

    • element(): 返回队列头部的元素但不移除,如果队列为空,则抛出 NoSuchElementException
    • peek(): 返回队列头部的元素但不移除,如果队列为空,则返回 null
    • isEmpty(): 如果队列为空,则返回 true
    • size(): 返回队列中元素的数量。
  4. 清空操作

    • clear(): 移除队列中的所有元素。
  5. 条件检查

    • contains(Object o): 如果队列包含指定的元素,则返回 true
    • remainingCapacity(): 返回队列中还可以添加的元素数量。
  6. 迭代器

    • iterator(): 返回队列的迭代器。迭代器的快照是在创建时获取的,因此迭代器不会抛出 ConcurrentModificationException
    • Spliterator (Java 8 及以上版本): 返回一个分割器,它可以用于遍历队列中的元素。

  BlockingQueue 接口的实现类(如 ArrayBlockingQueueLinkedBlockingQueuePriorityBlockingQueue 等)提供了不同的队列行为和性能特性。在使用这些方法时,需要注意阻塞操作可能会影响程序的性能和响应时间。此外,由于 BlockingQueue 是线程安全的,它可以在多线程环境中用于生产者-消费者问题的解决方案。

测试用例

以下是一些对并发集合进行测试的示例代码:

测试代码

以下是将您提供的代码段修改为 `main` 函数的写法,并包含必要的异常处理和 `assert` 语句的移除(因为在生产代码中通常不建议使用 `assert` 语句,因为它们在某些运行环境下可能被忽略)。

```java
package com.demo.javase.day84;

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ArrayBlockingQueue;

/**
 * @Author bug菌
 * @Source 公众号:猿圈奇妙屋
 * @Date 2024-04-08 23:18
 */
public class ConcurrentCollectionsTest {

    public static void main(String[] args) {
        // 测试 ConcurrentHashMap
        ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
        map.put("key", 1);
        Integer value = map.get("key");
        if (value != 1) {
            throw new RuntimeException("Expected value 1, but got " + value);
        }
        map.remove("key");
        if (map.containsKey("key")) {
            throw new RuntimeException("The key should not be in the map after removal.");
        }

        // 测试 CopyOnWriteArrayList
        CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();
        list.add(1);
        Integer element = list.get(0);
        if (element != 1) {
            throw new RuntimeException("Expected element 1, but got " + element);
        }
        list.remove(0);
        if (!list.isEmpty()) {
            throw new RuntimeException("The list should be empty after removal.");
        }

        // 测试 BlockingQueue
        BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10);
        try {
            queue.put(1);
            Integer takenValue = queue.take();
            if (takenValue != 1) {
                throw new RuntimeException("Expected value 1, but got " + takenValue);
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt(); // Reset the interrupt status
            throw new RuntimeException("Interrupted during take operation", e);
        }
    }
}

测试结果

  根据如上部分测试用例,本地执行结果如下,仅供参考:

测试代码解析

  针对如上测试代码,这里我再具体给大家讲解下,希望能够更透彻的帮助大家理解:

  上述测试案例,我演示了如何使用 ConcurrentHashMapCopyOnWriteArrayListBlockingQueue 这三个并发集合。这些集合类型是 Java 并发 API 的一部分,它们提供了在多线程环境中安全操作数据结构的能力。下面是对代码中每个部分的解析:

1. 测试 ConcurrentHashMap

  ConcurrentHashMap 是一个线程安全的哈希表,它允许多个线程同时对映射进行读写操作而不会产生并发冲突。其中上述部分代码使用步骤如下:

  • 创建一个 ConcurrentHashMap 实例。
  • 使用 put 方法向映射中添加一个键值对,键为 "key",值为 1
  • 使用 get 方法从映射中检索键 "key" 对应的值,并验证它是否为 1
  • 使用 remove 方法从映射中删除键 "key"
  • 检查映射是否不再包含键 "key"
2. 测试 CopyOnWriteArrayList

  CopyOnWriteArrayList 是一个线程安全的列表,它在每次修改操作时复制底层数组,以提供迭代器的快速失败特性。其中上述部分代码使用步骤如下:

  • 创建一个 CopyOnWriteArrayList 实例。
  • 使用 add 方法向列表中添加元素 1
  • 使用 get 方法从列表中检索索引 0 处的元素,并验证它是否为 1
  • 使用 remove 方法从列表中删除索引 0 处的元素。
  • 检查列表是否为空。
3. 测试 BlockingQueue

  BlockingQueue 是一个线程安全的队列,它提供了阻塞操作,使得生产者在队列满时等待空间,消费者在队列空时等待元素。其中上述部分代码使用步骤如下:

  • 创建一个容量为 10 的 ArrayBlockingQueue 实例。
  • 使用 put 方法向队列中添加元素 1
  • 使用 take 方法从队列中取出并删除一个元素,并验证它是否为 1
  • 如果 take 方法在等待期间被中断,捕获 InterruptedException 并重新设置中断状态,然后抛出一个包含原始异常信息的 RuntimeException

  所以,最终,它展示了如何在多线程环境中安全地使用 ConcurrentHashMapCopyOnWriteArrayListBlockingQueue。通过异常处理,代码确保了在操作不符合预期时能够提供明确的错误信息,有助于调试和错误追踪。

小结

  本文主要介绍了Java中的并发集合,包括ConcurrentHashMapCopyOnWriteArrayListBlockingQueue等。通过对源代码的解析,加深同学们对这些并发集合的理解。同时,给出了一些常见的应用场景案例和优缺点分析,帮助大家能够更好地选择和使用并发集合。最后,给出了一些类代码方法介绍和测试用例,帮助大家更好地学习和应用这些并发集合。

总结

  针对并发编程,它是Java开发中不可避免的一部分,而并发集合则是保证多线程环境下数据安全的重要手段,这点也是我单独写文的初衷。我通过本文介绍了Java中的几个常用并发集合,例如ConcurrentHashMapCopyOnWriteArrayListBlockingQueue等。通过对源代码的解析和应用场景案例的介绍,帮助大家更好地理解和应用这些并发集合。同时,给出了一些类代码方法介绍和测试用例,帮助读者更好地学习和掌握这些集合类的使用方法。通过学习并发集合,你们可以有效地提高多线程编程的效率和性能,提升自己在Java开发中的竞争力。

结尾

  通过本文的学习,相信大家对Java中的并发集合也有了更深入的了解,并能够在实际开发中灵活应用。并发集合是Java多线程编程不可或缺的一部分,对于提高程序效率和性能具有重要作用。希望大家可以继续深入学习并发编程,学习它们的源码及底层构造,不断提升自己的技术水平。

… …

  ok,以上就是我这期的全部内容啦,如果还想学习更多,你可以看看专栏的导读篇《「滚雪球学Java」教程导航帖》,每天学习一小节,日积月累下去,你一定能成为别人眼中的大佬的!功不唐捐,久久为功!

「赠人玫瑰,手留余香」,咱们下期拜拜~~

附录源码

  如上涉及所有源码均已上传同步在「Gitee」,提供给同学们一对一参考学习,辅助你更迅速的掌握。

☀️建议/推荐你

  无论你是计算机专业的学生,还是对编程感兴趣的跨专业小白,都建议直接入手「滚雪球学Java」专栏;该专栏不仅免费,bug菌还郑重承诺,只要你学习此专栏,均能入门并理解Java SE,以全网最快速掌握Java语言,每章节源码均同步「Gitee」,你真值得拥有;学习就像滚雪球一样,越滚越大,带你指数级提升。

码字不易,如果这篇文章对你有所帮助,帮忙给bug菌来个一键三连(关注、点赞、收藏) ,您的支持就是我坚持写作分享知识点传播技术的最大动力。

同时也推荐大家关注我的硬核公众号:「猿圈奇妙屋」 ;以第一手学习bug菌的首发干货,不仅能学习更多技术硬货,还可白嫖最新BAT大厂面试真题、4000G Pdf技术书籍、万份简历/PPT模板、技术文章Markdown文档等海量资料,你想要的我都有!

📣关于我

我是bug菌,CSDN | 掘金 | InfoQ | 51CTO | 华为云 | 阿里云 | 腾讯云 等社区博客专家,C站博客之星Top30,华为云2023年度十佳博主,掘金多年度人气作者Top40,51CTO年度博主Top12,掘金/InfoQ/51CTO等社区优质创作者;全网粉丝合计 20w+;硬核微信公众号「猿圈奇妙屋」,欢迎你的加入!免费白嫖最新BAT互联网公司面试真题、4000G PDF电子书籍、简历模板等海量资料,你想要的我都有,关键是你不来拿。


  • 30
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

bug菌¹

你的鼓励将是我创作的最大动力。

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值