26、Java并发编程中的数据结构与同步工具

Java并发编程中的数据结构与同步工具

1. 累加器类

1.1 DoubleAdder类

DoubleAdder类没有 increment() decrement() 方法。

1.2 LongAccumulator和DoubleAccumulator类

这两个类相似,但有一个重要区别。它们的构造函数需要指定两个参数:
- 内部计数器的初始值。
- 用于将新值累加到累加器的函数。

注意,该函数不能依赖于累加顺序。这两个类最重要的方法有:
- accumulate() :接收一个长整型值作为参数,将该函数应用于当前值和参数,以增加或减少计数器。
- get() :返回计数器的当前值。

示例代码如下:

LongAccumulator accumulator=new LongAccumulator((x,y) -> x*y, 1);
IntStream.range(1, 10).parallel().forEach(x -> accumulator.accumulate(x));
System.out.println(accumulator.get());

在累加器中使用了可交换操作,因此任何输入顺序的结果都相同。

2. 同步机制

2.1 同步类型

在并发应用程序中,有两种同步类型:
- 进程同步:用于控制任务的执行顺序。例如,一个任务必须等待其他任务完成后才能开始执行。
- 数据同步:当两个或多个任务访问同一个内存对象时使用。在这种情况下,必须保护对该对象的写操作,否则可能会出现数据竞争条件,导致程序的最终结果在每次执行时都不同。

2.2 Java中的同步机制

Java并发API提供了实现这两种同步的机制,最基本的同步机制是 synchronized 关键字,它可以应用于方法或代码块:
- 应用于方法时,同一时间只有一个线程可以执行该方法。
- 应用于代码块时,需要指定一个对象引用,同一时间只有一个受该对象保护的代码块可以执行。

Java还提供了其他同步机制:
| 同步机制 | 描述 |
| ---- | ---- |
| Lock接口及其实现类 | 用于实现临界区,确保同一时间只有一个线程执行该代码块 |
| Semaphore类 | 实现了Edsger Dijkstra引入的著名信号量同步机制 |
| CountDownLatch | 允许一个或多个线程等待其他线程完成 |
| CyclicBarrier | 允许在一个公共点同步不同的任务 |
| Phaser | 用于实现分阶段的并发任务 |
| Exchanger | 用于实现两个任务之间的数据交换点 |
| CompletableFuture | Java 8引入的新特性,扩展了执行器任务的Future机制,以异步方式生成任务的结果 |

2.3 CommonTask类

该类用于使调用线程在0到10秒之间的随机时间段内休眠,模拟任务的执行时间。代码如下:

public class CommonTask {
    public static void doTask() {
        long duration = ThreadLocalRandom.current().nextLong(10);
        System.out.printf("%s-%s: Working %d seconds\n",new Date(),Thread.currentThread().getName(),duration);
        try {
            TimeUnit.SECONDS.sleep(duration);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

2.4 Lock接口

最基本的同步机制之一是 Lock 接口及其实现类,基本实现类是 ReentrantLock 类。可以使用该类轻松实现临界区。示例代码如下:

public class LockTask implements Runnable {
    private static ReentrantLock lock = new ReentrantLock();
    private String name;
    public LockTask(String name) {
        this.name=name;
    }
    @Override
    public void run() {
        try {
            lock.lock();
            System.out.println("Task: " + name + "; Date: " + new Date() + ": Running the task");
            CommonTask.doTask();
            System.out.println("Task: " + name + "; Date: " + new Date() + ": The execution has finished");
        } finally {
            lock.unlock();
        }
    }
}

可以使用以下代码在执行器中执行十个任务来验证:

public class LockMain {
    public static void main(String[] args) {
        ThreadPoolExecutor executor=(ThreadPoolExecutor) Executors.newCachedThreadPool();
        for (int i=0; i<10; i++) {
            executor.execute(new LockTask("Task "+i));
        }
        executor.shutdown();
        try {
            executor.awaitTermination(1, TimeUnit.DAYS);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

2.5 Semaphore类

信号量机制由Edsger Dijkstra在1962年引入,用于控制对一个或多个共享资源的访问。该机制基于一个内部计数器和两个方法 wait() signal()

在Java中,信号量由 Semaphore 类实现, wait() 方法称为 acquire() signal() 方法称为 release() 。示例代码如下:

public class SemaphoreTask implements Runnable{
    private Semaphore semaphore;
    public SemaphoreTask(Semaphore semaphore) {
        this.semaphore=semaphore;
    }
    @Override
    public void run() {
        try {
            semaphore.acquire();
            CommonTask.doTask();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            semaphore.release();
        }
    }
}

在主程序中,可以执行十个任务,共享一个初始化为两个共享资源的 Semaphore 类,这样将有两个任务同时运行:

public static void main(String[] args) {
    Semaphore semaphore=new Semaphore(2);
    ThreadPoolExecutor executor=(ThreadPoolExecutor) Executors.newCachedThreadPool();
    for (int i=0; i<10; i++) {
        executor.execute(new SemaphoreTask(semaphore));
    }
    executor.shutdown();
    try {
        executor.awaitTermination(1, TimeUnit.DAYS);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

2.6 CountDownLatch类

该类提供了一种机制,用于等待一个或多个并发任务的完成。它有一个内部计数器,必须用要等待的任务数量进行初始化。 await() 方法会使调用线程休眠,直到内部计数器达到零, countDown() 方法会减少内部计数器。

示例代码如下:

public class CountDownTask implements Runnable {
    private CountDownLatch countDownLatch;
    public CountDownTask(CountDownLatch countDownLatch) {
        this.countDownLatch=countDownLatch;
    }
    @Override
    public void run() {
        CommonTask.doTask();
        countDownLatch.countDown();
    }
}

main() 方法中,可以执行任务并使用 CountDownLatch await() 方法等待它们完成:

public static void main(String[] args) {
    CountDownLatch countDownLatch=new CountDownLatch(10);
    ThreadPoolExecutor executor=(ThreadPoolExecutor) Executors.newCachedThreadPool();
    System.out.println("Main: Launching tasks");
    for (int i=0; i<10; i++) {
        executor.execute(new CountDownTask(countDownLatch));
    }
    try {
        countDownLatch.await();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    executor.shutdown();
}

2.7 CyclicBarrier类

该类允许在一个公共点同步一些任务。所有任务将在该点等待,直到所有任务都到达。它内部也管理一个计数器,记录尚未到达该点的任务数量。当一个任务到达指定点时,必须执行 await() 方法等待其他任务。当所有任务都到达时, CyclicBarrier 对象会唤醒它们,使它们继续执行。

示例代码如下:

public class BarrierTask implements Runnable {
    private CyclicBarrier barrier;
    public BarrierTask(CyclicBarrier barrier) {
        this.barrier=barrier;
    }
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+": Phase 1");
        CommonTask.doTask();
        try {
            barrier.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (BrokenBarrierException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+": Phase 2");
    }
}

还可以实现另一个 Runnable 对象,当所有任务都执行了 await() 方法时,由 CyclicBarrier 执行:

public class FinishBarrierTask implements Runnable {
    @Override
    public void run() {
        System.out.println("FinishBarrierTask: All the tasks have finished");
    }
}

main() 方法中,可以执行十个任务,并使用 CyclicBarrier 进行同步:

public static void main(String[] args) {
    CyclicBarrier barrier=new CyclicBarrier(10,new FinishBarrierTask());
    ThreadPoolExecutor executor=(ThreadPoolExecutor) Executors.newCachedThreadPool();
    for (int i=0; i<10; i++) {
        executor.execute(new BarrierTask(barrier));
    }
    executor.shutdown();
    try {
        executor.awaitTermination(1, TimeUnit.DAYS);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

2.8 CompletableFuture类

这是Java 8并发API中引入的一种新的同步机制,扩展了 Future 机制,使其更强大和灵活。它允许实现事件驱动模型,将任务链接起来,只有在前一个任务完成后,后续任务才会执行。

2.8.1 创建CompletableFuture

可以使用构造函数创建 CompletableFuture ,也可以使用 runAsync() supplyAsync() 方法创建:
- runAsync() :执行一个 Runnable 对象,返回 CompletableFuture<Void> ,该计算不能返回任何结果。
- supplyAsync() :执行一个 Supplier 接口的实现,该接口的 get() 方法包含任务的代码并返回结果。

2.8.2 任务执行顺序方法

CompletableFuture 类提供了许多方法来组织任务的执行顺序,实现事件驱动模型:
- thenApplyAsync() :接收一个 Function 接口的实现作为参数,当调用的 CompletableFuture 完成时执行该函数,返回一个 CompletableFuture 以获取该函数的结果。
- thenComposeAsync() :类似于 thenApplyAsync ,但当提供的函数也返回 CompletableFuture 时很有用。
- thenAcceptAsync() :类似于 thenApplyAsync ,但参数是一个 Consumer 接口的实现,该计算不返回结果。
- thenRunAsync() :接收一个 Runnable 对象作为参数。
- thenCombineAsync() :接收两个参数,一个是另一个 CompletableFuture 实例,另一个是 BiFunction 接口的实现,当两个 CompletableFuture 都完成时执行该 BiFunction
- runAfterBothAsync() :接收两个参数,一个是另一个 CompletableFuture ,另一个是 Runnable 接口的实现,当两个 CompletableFuture 都完成时执行该 Runnable
- runAfterEitherAsync() :当其中一个 CompletableFuture 对象完成时,执行 Runnable 任务。
- allOf() :接收一个可变的 CompletableFuture 对象列表,返回一个 CompletableFuture<Void> ,当所有 CompletableFuture 对象都完成时返回结果。
- anyOf() :类似于 allOf ,但当其中一个 CompletableFuture 完成时返回结果。

2.8.3 获取结果

可以使用 get() join() 方法获取 CompletableFuture 返回的结果:
- get() :抛出 ExecutionException ,这是一个受检查的异常。
- join() :抛出 RuntimeException ,这是一个未受检查的异常。

大多数方法带有 Async 后缀,表示这些方法将使用 ForkJoinPool.commonPool 实例并发执行。没有 Async 后缀的方法将串行执行,带有 Async 后缀且有一个执行器实例作为额外参数的方法, CompletableFuture 将在该执行器中异步执行。

2.9 使用CompletableFuture类

2.9.1 任务树

使用一个包含20,000个亚马逊产品的集合,实现以下任务树:

graph LR
    A[Reading the Examples] --> B[Searching The Data]
    A --> C[Get the Users]
    A --> D[Best Rated Product]
    A --> E[Best Selling Product]
    B --> F[Writing Search Results]
    D --> G[Best Product Data]
    E --> G
2.9.2 辅助任务
  • LoadTask :从磁盘加载产品信息并返回一个 Product 对象列表。
public class LoadTask implements Supplier<List<Product>> {
    private Path path;
    public LoadTask (Path path) {
        this.path=path;
    }
    @Override
    public List<Product> get() {
        List<Product> productList=null;
        try {
            productList = Files.walk(path, FileVisitOption.FOLLOW_LINKS).parallel()
                   .filter(f -> f.toString().endsWith(".txt"))  
                   .map(ProductLoader::load).collect(Collectors.toList());
        } catch (IOException e) {
            e.printStackTrace();
        }
        return productList;
    }
}
  • SearchTask :在 Product 对象列表中搜索包含指定单词的产品。
public class SearchTask implements Function<List<Product>, List<Product>> {
    private String query;
    public SearchTask(String query) {
        this.query=query;
    }
    @Override
    public List<Product> apply(List<Product> products) {
        System.out.println(new Date()+": CompletableTask: start");
        List<Product> ret = products.stream()
               .filter(product -> product.getTitle().toLowerCase().contains(query))
               .collect(Collectors.toList());
        System.out.println(new Date()+": CompletableTask: end: "+ret.size());
        return ret;
    }
}
  • WriteTask :将搜索结果写入文件。
public class WriteTask implements Consumer<List<Product>> {
    @Override
    public void accept(List<Product> products) {
        // implementation is omitted
    }
}
2.9.3 main()方法
public class CompletableMain {
    public static void main(String[] args) {
        Path file = Paths.get("data","category");
        System.out.println(new Date() + ": Main: Loading products");
        LoadTask loadTask = new LoadTask(file);
        CompletableFuture<List<Product>> loadFuture = CompletableFuture.supplyAsync(loadTask);

        System.out.println(new Date() + ": Main: Then apply for search");
        CompletableFuture<List<Product>> completableSearch = loadFuture.thenApplyAsync(new SearchTask("love"));

        CompletableFuture<Void> completableWrite = completableSearch.thenAcceptAsync(new WriteTask());
        completableWrite.exceptionally(ex -> {
            System.out.println(new Date() + ": Main: Exception " + ex.getMessage());
            return null;
        });

        System.out.println(new Date() + ": Main: Then apply for users");
        CompletableFuture<List<String>> completableUsers = loadFuture.thenApplyAsync(resultList -> {
            System.out.println(new Date() + ": Main: Completable users: start");
            List<String> users = resultList.stream()
                   .flatMap(p -> p.getReviews().stream())
                   .map(review -> review.getUser())
                   .distinct()
                   .collect(Collectors.toList());
            System.out.println(new Date() + ": Main: Completable users: end");
            return users;
        });

        System.out.println(new Date() + ": Main: Then apply for best rated product....");
        CompletableFuture<Product> completableProduct = loadFuture.thenApplyAsync(resultList -> {
            Product maxProduct = null;
            double maxScore = 0.0;
            System.out.println(new Date() + ": Main: Completable product: start");
            for (Product product : resultList) {
                if (!product.getReviews().isEmpty()) {
                    double score = product.getReviews().stream()
                           .mapToDouble(review -> review.getValue())
                           .average().getAsDouble();
                    if (score > maxScore) {
                        maxProduct = product;
                        maxScore = score;
                    }
                }
            }
            System.out.println(new Date() + ": Main: Completable product: end");
            return maxProduct;
        });

        System.out.println(new Date() + ": Main: Then apply for best selling product....");
        CompletableFuture<Product> completableBestSellingProduct = loadFuture.thenApplyAsync(resultList -> {
            System.out.println(new Date() + ": Main: Completable best selling: start");
            Product bestProduct = resultList.stream()
                   .min(Comparator.comparingLong(Product::getSalesrank))
                   .orElse(null);
            System.out.println(new Date() + ": Main: Completable best selling: end");
            return bestProduct;
        });

        CompletableFuture<String> completableProductResult = completableBestSellingProduct.thenCombineAsync(
                completableProduct,  
                (bestSellingProduct, bestRatedProduct) -> {
                    System.out.println(new Date() + ": Main: Completable product result: start");
                    String ret = "The best selling product is " + bestSellingProduct.getTitle() + "\n";
                    ret += "The best rated product is " + bestRatedProduct.getTitle();
                    System.out.println(new Date() + ": Main: Completable product result: end");
                    return ret;
                });

        System.out.println(new Date() + ": Main: Waiting for results");
        CompletableFuture<Void> finalCompletableFuture = CompletableFuture.allOf(completableProductResult, completableUsers, completableWrite);
        finalCompletableFuture.join();
        try {
            System.out.println("Number of loaded products: " + loadFuture.get().size());
            System.out.println("Number of found products: " + completableSearch.get().size());
            System.out.println("Number of users: " + completableUsers.get().size());
            System.out.println("Best rated product: " + completableProduct.get().getTitle());
            System.out.println("Best selling product: " + completableBestSellingProduct.get().getTitle());
            System.out.println("Product result: " + completableProductResult.get());
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }
}

主程序执行所有配置并等待任务完成,任务的执行顺序按照配置进行。

3. 代码执行流程分析

3.1 整体执行流程

上述使用 CompletableFuture 实现的任务树,整体执行流程如下:
1. 从磁盘加载产品信息。
2. 并行执行四个任务:
- 搜索包含指定单词的产品,并将结果写入文件。
- 获取购买产品的用户列表。
- 找出评分最高的产品。
- 找出销量最好的产品。
3. 合并评分最高和销量最好的产品信息。
4. 等待所有任务完成并输出结果。

3.2 各步骤详细分析

3.2.1 加载产品信息
Path file = Paths.get("data","category");
System.out.println(new Date() + ": Main: Loading products");
LoadTask loadTask = new LoadTask(file);
CompletableFuture<List<Product>> loadFuture = CompletableFuture.supplyAsync(loadTask);
  • 使用 LoadTask 从磁盘加载产品信息。
  • supplyAsync 方法异步执行 LoadTask ,返回一个 CompletableFuture 对象 loadFuture
3.2.2 搜索产品并写入结果
System.out.println(new Date() + ": Main: Then apply for search");
CompletableFuture<List<Product>> completableSearch = loadFuture.thenApplyAsync(new SearchTask("love"));
CompletableFuture<Void> completableWrite = completableSearch.thenAcceptAsync(new WriteTask());
completableWrite.exceptionally(ex -> {
    System.out.println(new Date() + ": Main: Exception " + ex.getMessage());
    return null;
});
  • thenApplyAsync 方法在 loadFuture 完成后,异步执行 SearchTask ,搜索包含 “love” 的产品。
  • thenAcceptAsync 方法在搜索完成后,异步执行 WriteTask ,将搜索结果写入文件。
  • exceptionally 方法处理 WriteTask 可能抛出的异常。
3.2.3 获取用户列表
System.out.println(new Date() + ": Main: Then apply for users");
CompletableFuture<List<String>> completableUsers = loadFuture.thenApplyAsync(resultList -> {
    System.out.println(new Date() + ": Main: Completable users: start");
    List<String> users = resultList.stream()
           .flatMap(p -> p.getReviews().stream())
           .map(review -> review.getUser())
           .distinct()
           .collect(Collectors.toList());
    System.out.println(new Date() + ": Main: Completable users: end");
    return users;
});
  • thenApplyAsync 方法在 loadFuture 完成后,异步执行一个 lambda 表达式,获取购买产品的用户列表。
3.2.4 找出评分最高和销量最好的产品
System.out.println(new Date() + ": Main: Then apply for best rated product....");
CompletableFuture<Product> completableProduct = loadFuture.thenApplyAsync(resultList -> {
    Product maxProduct = null;
    double maxScore = 0.0;
    System.out.println(new Date() + ": Main: Completable product: start");
    for (Product product : resultList) {
        if (!product.getReviews().isEmpty()) {
            double score = product.getReviews().stream()
                   .mapToDouble(review -> review.getValue())
                   .average().getAsDouble();
            if (score > maxScore) {
                maxProduct = product;
                maxScore = score;
            }
        }
    }
    System.out.println(new Date() + ": Main: Completable product: end");
    return maxProduct;
});

System.out.println(new Date() + ": Main: Then apply for best selling product....");
CompletableFuture<Product> completableBestSellingProduct = loadFuture.thenApplyAsync(resultList -> {
    System.out.println(new Date() + ": Main: Completable best selling: start");
    Product bestProduct = resultList.stream()
           .min(Comparator.comparingLong(Product::getSalesrank))
           .orElse(null);
    System.out.println(new Date() + ": Main: Completable best selling: end");
    return bestProduct;
});
  • 分别使用 thenApplyAsync 方法在 loadFuture 完成后,异步执行两个 lambda 表达式,找出评分最高和销量最好的产品。
3.2.5 合并产品信息
CompletableFuture<String> completableProductResult = completableBestSellingProduct.thenCombineAsync(
        completableProduct,  
        (bestSellingProduct, bestRatedProduct) -> {
            System.out.println(new Date() + ": Main: Completable product result: start");
            String ret = "The best selling product is " + bestSellingProduct.getTitle() + "\n";
            ret += "The best rated product is " + bestRatedProduct.getTitle();
            System.out.println(new Date() + ": Main: Completable product result: end");
            return ret;
        });
  • thenCombineAsync 方法在评分最高和销量最好的产品任务都完成后,异步执行一个 lambda 表达式,合并两个产品的信息。
3.2.6 等待所有任务完成并输出结果
System.out.println(new Date() + ": Main: Waiting for results");
CompletableFuture<Void> finalCompletableFuture = CompletableFuture.allOf(completableProductResult, completableUsers, completableWrite);
finalCompletableFuture.join();
try {
    System.out.println("Number of loaded products: " + loadFuture.get().size());
    System.out.println("Number of found products: " + completableSearch.get().size());
    System.out.println("Number of users: " + completableUsers.get().size());
    System.out.println("Best rated product: " + completableProduct.get().getTitle());
    System.out.println("Best selling product: " + completableBestSellingProduct.get().getTitle());
    System.out.println("Product result: " + completableProductResult.get());
} catch (InterruptedException | ExecutionException e) {
    e.printStackTrace();
}
  • allOf 方法创建一个新的 CompletableFuture 对象,等待所有任务完成。
  • join 方法阻塞主线程,直到所有任务完成。
  • 使用 get 方法获取各个任务的结果并输出。

3.3 总结

通过使用 CompletableFuture ,可以方便地实现异步任务的并发执行和任务之间的依赖关系。在实际开发中,可以根据具体需求,灵活运用 CompletableFuture 的各种方法,实现复杂的任务调度和处理。

4. 同步机制对比

4.1 不同同步机制的特点

同步机制 特点 适用场景
synchronized 关键字 简单易用,可应用于方法或代码块 简单的同步需求,如保护单个方法或代码块
Lock 接口及其实现类 功能强大,可实现更复杂的同步逻辑 需要更细粒度的同步控制,如可重入锁、公平锁等
Semaphore 基于信号量机制,控制对共享资源的访问 限制对有限资源的并发访问
CountDownLatch 允许一个或多个线程等待其他线程完成 一个或多个线程需要等待一组任务完成后再继续执行
CyclicBarrier 在公共点同步任务 多个任务需要在某个点同步后再继续执行
Phaser 实现分阶段的并发任务 任务可以划分为多个阶段,每个阶段需要同步
Exchanger 实现两个任务之间的数据交换 两个任务需要交换数据的场景
CompletableFuture 扩展了 Future 机制,支持事件驱动模型 异步任务的并发执行和任务之间的依赖关系

4.2 选择合适的同步机制

在选择同步机制时,需要考虑以下因素:
- 同步需求的复杂度 :如果同步需求简单,使用 synchronized 关键字即可;如果需要更复杂的同步逻辑,可选择 Lock 接口及其实现类。
- 资源的并发访问控制 :如果需要限制对共享资源的并发访问,可使用 Semaphore 类。
- 任务的同步点 :如果任务需要在某个点同步,可使用 CyclicBarrier Phaser ;如果一个或多个线程需要等待其他线程完成,可使用 CountDownLatch
- 任务的异步执行和依赖关系 :如果需要实现异步任务的并发执行和任务之间的依赖关系,可使用 CompletableFuture

5. 注意事项

5.1 锁的使用

  • 使用 Lock 接口及其实现类时,必须在 finally 块中释放锁,以确保锁一定会被释放,避免死锁。
try {
    lock.lock();
    // 临界区代码
} finally {
    lock.unlock();
}

5.2 异常处理

  • 在使用 CompletableFuture 时,要注意异常处理。可以使用 exceptionally 方法处理任务执行过程中抛出的异常。
completableWrite.exceptionally(ex -> {
    System.out.println(new Date() + ": Main: Exception " + ex.getMessage());
    return null;
});

5.3 资源管理

  • 在使用 Semaphore 类时,要确保在使用完共享资源后调用 release() 方法,以释放资源。
try {
    semaphore.acquire();
    // 使用共享资源
} catch (InterruptedException e) {
    e.printStackTrace();
} finally {
    semaphore.release();
}

6. 总结

本文介绍了 Java 并发编程中的数据结构与同步工具,包括累加器类、各种同步机制以及 CompletableFuture 的使用。通过合理使用这些工具,可以实现高效、安全的并发编程。在实际开发中,需要根据具体需求选择合适的同步机制,并注意锁的使用、异常处理和资源管理等问题。

通过本文的学习,你应该对 Java 并发编程有了更深入的理解,能够运用这些知识解决实际开发中的并发问题。希望本文对你有所帮助!

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值