JUC总结系列篇 (一):对Java线程池的理解和使用总结

文章结构顺序:
一.为什么需要线程池
1.1.线程的消耗 1.2.高并发对线程的开销
二.线程池的优势
三. 线程池的创建参数详解
四.线程池工作原理
五. 线程池的异常处理方案
六.常用的线程池阻塞队列
七.拒绝策略
八. 常见线程池的特点和使用场景
九. 线程池的状态
十.线程池的几个常用API
十一. 线程池的配置

一.为什么需要线程池?

1.线程的消耗:

我们知道,线程是重资源,JVM中的线程与操作系统中的线程是一对一的关系,所以在JVM中每创建一个线程就需要调用操作系统提供的API创建线程,赋予资源,而销毁线程同样也需要系统调用。而系统调用就意味着需要上下文切换等开销。因此线程的创建和销毁是一个重操作,并且线程本身也占用资源。

何为线程上下文切换?

多线程编程中一般线程的个数都大于CPU核心的个数,而一个CPU核心在任意时刻只能被一个线程利用,为了让所有的线程都能得到有效的执行,CPU采取的策略是让每个线程分配时间片并轮转的形式,当一个线程的时间片用完的时候就会重新处于就绪状态,将CPU让给其他线程使用,这个过程就属于一次上下文切换。
简单来说就是先把前一个任务的 CPU 上下文(也就是 CPU 寄存器和程序计数器)保存起来,然后加载新任务的上下文到这些寄存器和程序计数器,最后再跳转到程序计数器所指的新位置,运行新任务。

为什么需要多线程?

我们都知道线程是CPU调度的最小单位,在单核时代,如果是纯运算的操作是不需要多线程的呀,一个线程一直执行运算即可,但如果这个线程正在等待I/O操作,此时CPU就处于空闲状态,这就浪费了CPU的算力,CPU算力也是很珍贵的资源。因此就有了多线程,在某线程等待I/O等操作的时候,另一个线程马上顶上,充分利用CPU,提高处理效率。

2.高并发对线程的开销:

如果并发的请求数量非常多,但每个线程执行的时间很短,这样就会频繁的创建和销毁线程,如此一来会大大降低系统的效率。可能出现服务器在为每个请求创建新线程和销毁线程上花费的时间和消耗的系统资源要比处理实际的用户请求的时间和资源更多。

二.线程池的优势:

1. 降低资源消耗:

线程池通常会维护一些线程(数量为 corePoolSize),这些线程被重复使用来执行不同的任务,任务完成后不会销毁。在待处理任务量很大的时候,通过对线程资源的复用,避免了线程的频繁创建与销毁,从而降低了系统资源消耗。

2. 提高响应速度:

由于线程池维护了一批 alive 状态的线程,当任务到达时,不需要再创建线程,而是直接由这些线程去执行任务,从而减少了任务的等待时间。

3.提高线程的可管理性:

使用线程池可以对线程进行统一的分配,调优和监控。

三. 线程池的创建参数详解

线程池的构造方法

线程池的实现类是ThreadPoolExecutor
一共有四个构造方法
在这里插入图片描述

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), defaultHandler);
    }


    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             threadFactory, defaultHandler);
    }


    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              RejectedExecutionHandler handler) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), handler);
    }

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

由构造方法代码可以知道,有俩个参数是非必要的,源码有默认实现。一个是ThreadFactory(线程的创建工厂类), 默认使用DefaultThreadFactory

    public static ThreadFactory defaultThreadFactory() {
        return new DefaultThreadFactory();
    }

一个是RejectedExecutionHandler(拒绝策略),默认使用AbortPolicy

   private static final RejectedExecutionHandler defaultHandler = new AbortPolicy();

四个构造方法最后都调用自身参数最多的构造方法,也就是第四个,所以主要分析这个构造方法。

构造方法参数详解:

corePoolSize(必需):核心线程数。即池中一直保持存活的线程数,即使这些线程处于空闲。但是将allowCoreThreadTimeOut参数设置为true后,核心线程处于空闲一段时间以上,也会被回收。
maximumPoolSize(必需):池中允许的最大线程数。当核心线程全部繁忙且任务队列打满之后,线程池会临时追加线程,直到总线程数达到maximumPoolSize这个上限。
keepAliveTime(必需):线程空闲超时时间。当非核心线程处于空闲状态的时间超过这个时间后,该线程将被回收。将allowCoreThreadTimeOut参数设置为true后,核心线程也会被回收。
unit(必需):keepAliveTime参数的时间单位。有:TimeUnit.DAYS(天)、TimeUnit.HOURS(小时)、TimeUnit.MINUTES(分钟)、TimeUnit.SECONDS(秒)、TimeUnit.MILLISECONDS(毫秒)、TimeUnit.MICROSECONDS(微秒)、TimeUnit.NANOSECONDS(纳秒)
workQueue(必需):任务队列,采用阻塞队列实现。当核心线程全部繁忙时,后续由execute方法提交的Runnable将存放在任务队列中,等待被线程处理。
threadFactory(可选):线程工厂。指定线程池创建线程的方式。
handler(可选):拒绝策略。当线程池中线程数达到maximumPoolSize并且workQueue的任务满时,后续提交的任务将被拒绝,handler可以指定用什么方式拒绝任务。

四.线程池工作原理

工作流程,对应execute()方法:

在这里插入图片描述

  • 提交一个任务,线程池里存活的核心线程数小于线程数corePoolSize时,线程池会创建一个核心线程去处理提交的任务。
  • 如果线程池核心线程数已满,即线程数已经等于corePoolSize,一个新提交的任务,会被放进任务队列workQueue排队等待执行。
  • 当线程池里面存活的线程数已经等于corePoolSize了,并且任务队列workQueue也满,判断线程数是否达到maximumPoolSize,即最大线程数是否已满,如果没到达,创建一个非核心线程执行提交的任务。
  • 如果当前的线程数达到了maximumPoolSize,还有新的任务过来的话,直接采用拒绝策略处理。

线程池工作流程的形象比喻

  • 核心线程比作公司正式员工
  • 非核心线程比作外包员工
  • 阻塞队列比作需求池
  • 提交的任务比作提需求
    在这里插入图片描述
  • 当产品提个需求,正式员工(核心线程)先接需求(执行任务)
  • 如果正式员工都有需求在做,即核心线程数已满),产品就把需求先放需求池(阻塞队列)。
  • 如果需求池(阻塞队列)也满了,但是这时候产品继续提需求,怎么办呢?那就请外包(非核心线程)来做。
  • 如果所有员工(最大线程数也满了)都有需求在做了,那就执行拒绝策略。
  • 如果外包员工把需求做完了,它经过一段(keepAliveTime)空闲时间,就离开公司了。

五. 线程池的异常处理方案

1.submit()

        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(5, 10, 1, TimeUnit.SECONDS, new ArrayBlockingQueue<>(5));
        for (int i = 0; i < 5; i++) {
            threadPool.submit(() -> {
                    System.out.println("current thread name   " + Thread.currentThread().getName());
                    Object object = null;
                    System.out.println("result## "+object.toString());

            });
        }

在这里插入图片描述
在使用submit提交任务的时候,即使出现错误也不会自动抛出。除非我们使用try/catch去捕获
修改代码:

        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(5, 10, 1, TimeUnit.SECONDS, new ArrayBlockingQueue<>(5));
        for (int i = 0; i < 5; i++) {
            threadPool.submit(() -> {
                try {
                    System.out.println("current thread name   " + Thread.currentThread().getName());
                    Object object = null;
                    System.out.println("result## "+object.toString());
                } catch (Exception e) {
                    e.printStackTrace();
                }

            });
        }

在这里插入图片描述
此时就可以获取到异常了。
原因是submit提供了一个返回值Future<?>,而我们可以利用这个返回值的get方法来获取异常和处理。

        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(5, 10, 1, TimeUnit.SECONDS, new ArrayBlockingQueue<>(5));
        for (int i = 0; i < 5; i++) {
            Future<?> submit = threadPool.submit(() -> {
                    System.out.println("current thread name   " + Thread.currentThread().getName());
                    Object object = null;
                    System.out.println("result## " + object.toString());

            });
            try {
                submit.get();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } catch (ExecutionException e) {
                throw new RuntimeException(e);
            }
        }

2.execute

        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(5, 10, 1, TimeUnit.SECONDS, new ArrayBlockingQueue<>(5));
        for (int i = 0; i < 5; i++) {
            threadPool.execute(() -> {
                    System.out.println("current thread name   " + Thread.currentThread().getName());
                    Object object = null;
                    System.out.println("result## " + object.toString());

            });

        }

在这里插入图片描述
使用execute出现异常则会直接抛出。

3.设置自定义ThreadFactory,为工作者线程设置UncaughtExceptionHandler,在uncaughtException方法中处理异常

        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(5, 10, 1, TimeUnit.SECONDS, new ArrayBlockingQueue<>(5),(r) -> {
            Thread t = new Thread(r);
            t.setUncaughtExceptionHandler(
                    (t1, e) -> {
                        System.out.println(t1.getName() + "线程抛出的异常"+e);
                    });
            return t;
        });
        for (int i = 0; i < 5; i++) {
            threadPool.execute(() -> {
                    System.out.println("current thread name   " + Thread.currentThread().getName());
                    Object object = null;
                    System.out.println("result## " + object.toString());

            });

        }

在这里插入图片描述

4.重写ThreadPoolExecutor的afterExecute方法,处理传递的异常引用

class ExtendedExecutor extends ThreadPoolExecutor {
    // 这可是jdk文档里面给的例子。。
    protected void afterExecute(Runnable r, Throwable t) {
        super.afterExecute(r, t);
        if (t == null && r instanceof Future<?>) {
            try {
                Object result = ((Future<?>) r).get();
            } catch (CancellationException ce) {
                t = ce;
            } catch (ExecutionException ee) {
                t = ee.getCause();
            } catch (InterruptedException ie) {
                Thread.currentThread().interrupt(); // ignore/reset
            }
        }
        if (t != null)
            System.out.println(t);
    }
}}

总结:
在这里插入图片描述

六.常用的线程池阻塞队列

使用ThreadPoolExecutor需要指定一个实现了BlockingQueue接口的任务等待队列。在ThreadPoolExecutor线程池的API文档中,一共推荐了三种等待队列,它们是:SynchronousQueueLinkedBlockingQueueArrayBlockingQueue

  • SynchronousQueue:同步队列。SynchronousQueue 最大的不同之处在于,它的容量为 0,所以没有一个地方来暂存元素,导致每次取数据都要先阻塞,直到有数据被放入;同理,每次放数据的时候也会阻塞,直到有消费者来取。
    注意:与线程的同步区别开来,使用SynchronousQueue也是无法保证线程同步的,保证同步依旧需要使用同步知识来解决

  • LinkedBlockingQueue:无界队列(严格来说并非无界,上限是Integer.MAX_VALUE),基于链表结构。使用无界队列后,当核心线程都繁忙时,后续任务可以无限加入队列,因此线程池中线程数不会超过核心线程数。这种队列可以提高线程池吞吐量,但代价是牺牲内存空间,甚至会导致内存溢出(OOM)。另外,使用它时可以指定容量,这样它也就是一种有界队列了。

  • ArrayBlockingQueue:有界队列,基于数组实现。在线程池初始化时,指定队列的容量,后续无法再调整。这种有界队列有利于防止资源耗尽,但可能更难调整和控制。
    另外,Java还提供了另外4种队列:

  • PriorityBlockingQueue:支持优先级排序的无界阻塞队列。存放在PriorityBlockingQueue中的元素必须实现Comparable接口,这样才能通过实现compareTo()方法进行排序。优先级最高的元素将始终排在队列的头部;PriorityBlockingQueue不会保证优先级一样的元素的排序,也不保证当前队列中除了优先级最高的元素以外的元素,随时处于正确排序的位置。

  • DelayQueue:延迟队列。基于二叉堆实现,同时具备:无界队列、阻塞队列、优先队列的特征。DelayQueue延迟队列中存放的对象,必须是实现Delayed接口的类对象。通过执行时延从队列中提取任务,时间没到任务取不出来。更多内容请见DelayQueue。

  • LinkedBlockingDeque:双端队列。基于链表实现,既可以从尾部插入/取出元素,还可以从头部插入元素/取出元素。

  • LinkedTransferQueue:由链表结构组成的无界阻塞队列。这个队列比较特别的时,采用一种预占模式,意思就是消费者线程取元素时,如果队列不为空,则直接取走数据,若队列为空,那就生成一个节点(节点元素为null)入队,然后消费者线程被等待在这个节点上,后面生产者线程入队时发现有一个元素为null的节点,生产者线程就不入队了,直接就将元素填充到该节点,并唤醒该节点等待的线程,被唤醒的消费者线程取走元素。

七.拒绝策略

线程池有一个重要的机制:拒绝策略。当线程池workQueue已满且无法再创建新线程池时,就要拒绝后续任务了。拒绝策略需要实现RejectedExecutionHandler接口,不过Executors框架已经为我们实现了4种拒绝策略:

  • AbortPolicy(默认):丢弃任务并抛出RejectedExecutionException异常。
  • CallerRunsPolicy:直接运行这个任务的run方法,但并非是由线程池的线程处理,而是交由任务的调用线程处理。
  • DiscardPolicy:直接丢弃任务,不抛出任何异常。
  • DiscardOldestPolicy:将当前处于等待队列列头的等待任务强行取出,然后再试图将当前被拒绝的任务提交到线程池执行。

八. 常见线程池

newFixedThreadPool (固定数目线程的线程池)
newCachedThreadPool(可缓存线程的线程池)
newSingleThreadExecutor(单线程的线程池)
newScheduledThreadPool(定时及周期执行的线程池)

1.newFixedThreadPool

  public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>(),
                                      threadFactory);
    }

线程池特点
核心线程数和最大线程数大小一样
没有所谓的非空闲时间,即keepAliveTime为0
阻塞队列为无界队列LinkedBlockingQueue
在这里插入图片描述
工作流程:

  • 提交任务
  • 如果线程数少于核心线程,创建核心线程执行任务
  • 如果线程数等于核心线程,把任务添加到LinkedBlockingQueue阻塞队列
  • 如果线程执行完任务,去阻塞队列取任务,继续执行。

使用无界队列的线程池会导致内存飙升吗?

答案 :会的,newFixedThreadPool使用了无界的阻塞队列LinkedBlockingQueue,如果线程获取一个任务后,任务的执行时间比较长(比如,上面Run里面使用Thread.sleep()设置了10秒),会导致队列的任务越积越多,导致机器内存使用不停飙升, 最终导致OOM
代码如下:

   ExecutorService executor = Executors.newFixedThreadPool(10);
                    for (int i = 0; i < Integer.MAX_VALUE; i++) {
                        executor.execute(()->{
                            try {
                                Thread.sleep(10000);
                            } catch (InterruptedException e) {
                                //do nothing
                            }
            });

使用场景

FixedThreadPool 适用于处理CPU密集型的任务,确保CPU在长期被工作线程使用的情况下,尽可能的少的分配线程,即适用执行长期的任务。

2. newCachedThreadPool

   public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>(),
                                      threadFactory);
    }

线程池特点:

  • 核心线程数为0
  • 最大线程数为Integer.MAX_VALUE
  • 阻塞队列是SynchronousQueue
  • 非核心线程空闲存活时间为60秒

当提交任务的速度大于处理任务的速度时,每次提交一个任务,就必然会创建一个线程。极端情况下会创建过多的线程,耗尽 CPU 和内存资源。由于空闲 60 秒的线程会被终止,长时间保持空闲的 CachedThreadPool 不会占用任何资源。

工作流程:

在这里插入图片描述

  • 提交任务
  • 因为没有核心线程,所以任务直接加到SynchronousQueue队列。
  • 判断是否有空闲线程,如果有,就去取出任务执行。
  • 如果没有空闲线程,就新建一个线程执行。
  • 执行完任务的线程,还可以存活60秒,如果在这期间,接到任务,可以继续活下去;否则,被销毁。

实例代码:

  ExecutorService executor = Executors.newCachedThreadPool();
        for (int i = 0; i < 5; i++) {
            executor.execute(() -> {
                System.out.println(Thread.currentThread().getName()+"正在执行");
            });
        }

在这里插入图片描述

使用场景

用于并发执行大量短期的小任务。

3newSingleThreadExecutor

  public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>(),
                                    threadFactory));
    }

线程池特点

核心线程数为1
最大线程数也为1
阻塞队列是LinkedBlockingQueue
keepAliveTime为0

工作机制

在这里插入图片描述

  • 提交任务
  • 线程池是否有一条线程在,如果没有,新建线程执行任务
  • 如果有,讲任务加到阻塞队列
  • 当前的唯一线程,从队列取任务,执行完一个,再继续取,一个人(一条线程)夜以继日地干活。

实例代码

  ExecutorService executor = Executors.newSingleThreadExecutor();
                for (int i = 0; i < 5; i++) {
                    executor.execute(() -> {
                        System.out.println(Thread.currentThread().getName()+"正在执行");
                    });
        }

在这里插入图片描述

使用场景

适用于串行执行任务的场景,一个任务一个任务地执行。

4.newScheduledThreadPool

    public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue());
    }

线程池特点

  • 最大线程数为Integer.MAX_VALUE
  • 阻塞队列是DelayedWorkQueue
  • keepAliveTime为0
  • scheduleAtFixedRate() :按某种速率周期执行
  • scheduleWithFixedDelay():在某个延迟后执行

工作机制

  • 添加一个任务
  • 线程池中的线程从 DelayQueue 中取任务
  • 线程从 DelayQueue 中获取 time 大于等于当前时间的task
  • 执行完后修改这个 task 的 time 为下次被执行的时间
  • 这个 task 放回DelayQueue队列中
    /**
    创建一个给定初始延迟的间隔性的任务,之后的下次执行时间是上一次任务从执行到结束所需要的时间+* 给定的间隔时间
    */
    ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
        scheduledExecutorService.scheduleWithFixedDelay(()->{
            System.out.println("current Time" + System.currentTimeMillis());
            System.out.println(Thread.currentThread().getName()+"正在执行");
        }, 1, 3, TimeUnit.SECONDS);

在这里插入图片描述

使用场景

周期性执行任务的场景,需要限制线程数量的场景

九. 线程池的状态

   COUNT_BITS = 29;
   private static final int RUNNING    = -1 << COUNT_BITS;
   private static final int SHUTDOWN   =  0 << COUNT_BITS;
   private static final int STOP       =  1 << COUNT_BITS;
   private static final int TIDYING    =  2 << COUNT_BITS;
   private static final int TERMINATED =  3 << COUNT_BITS;

RUNNING

该状态的线程池会接收新任务,并处理阻塞队列中的任务;
调用线程池的shutdown()方法,可以切换到SHUTDOWN状态;
调用线程池的shutdownNow()方法,可以切换到STOP状态;

SHUTDOWN

该状态的线程池不会接收新任务,但会处理阻塞队列中的任务;
队列为空,并且线程池中执行的任务也为空,进入TIDYING状态;

STOP

该状态的线程不会接收新任务,也不会处理阻塞队列中的任务,而且会中断正在运行的任务;
线程池中执行的任务为空,进入TIDYING状态;

TIDYING

该状态表明所有的任务已经运行终止,记录的任务数量为0。
terminated()执行完毕,进入TERMINATED状态

TERMINATED

该状态表示线程池彻底终止

几种状态的转移:

在这里插入图片描述

十.线程池的几个常用API

1.线程初始化

默认情况下,创建线程池之后,线程池中是没有线程的,需要提交任务之后才会创建线程。
在实际中如果需要线程池创建之后立即创建线程,可以通过以下两个方法办到:
prestartCoreThread():boolean prestartCoreThread(),初始化一个核心线程

public boolean prestartCoreThread() {
    return addIfUnderCorePoolSize(null); //注意传进去的参数是null
}

prestartAllCoreThreads():int prestartAllCoreThreads(),初始化所有核心线程,并返回初始化的线程数

public int prestartAllCoreThreads() {
    int n = 0;
    while (addIfUnderCorePoolSize(null))//注意传进去的参数是null
        ++n;
    return n;
}

2. 线程池的关闭

shutdown不会立即终止线程池,而是要等所有任务缓存队列中的任务都执行完后才终止,但再也不会接受新的任务
shutdownNow立即终止线程池,并尝试打断正在执行的任务,并且清空任务缓存队列,返回尚未执行的任务

十一. 线程池的配置

CPU密集型:

线程数 = 机器的CPU核心数 + 1
机器CPU核心数Runtime.getRuntime().availableProcessors()
为何要+1?:当线程所需要的数据在内存中时,线程可以直接拿来使用。而当数据存放在磁盘的虚拟内存中时,操作系统需要去把虚拟内存中的数据调度到内存中来,此时的线程会处于页缺失状态被操作系统挂起,直到数据调度完成才会被唤醒,若此时的CUP是空闲状态,就可以让用额外的线程使用,因此需要加1

IO密集型:

根据很多人的开发经验(很多开源框架也是如此)总结:
线程数 = 机器的CPU核心数 * 2

混合型:

根据具体需求分析:
用不同的线程池执行不同类型的任务,让任务按照类型区分开,而不是混杂在一起,这样就可以根据是CPU密集型还是IO密集型去设置线程数了,达到更好的性能

参考与感谢:

JAVA线程池解析
深入Java线程池:从设计思想到源码解读

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值