线程池使用和自定义线程池

目录

1 线程基础概述

1.1,线程池的作用 

1.2,为什么要用线程池? 

1.3,比较重要的几个类

1.4,new Thread的弊端

2 四种线程池 

2.1 源码分析

2.2 RejectedExecutionHandler 线程池四种拒绝任务策略

2.3 示例使用

1,newCachedThreadPool 

2,newFixedThreadPool 

3,newScheduledThreadPool 

4,newSingleThreadExecutor 

3 自定义线程池

3.1 整合懒汉模式和自定义线程池

3.2 自定义线程池工具类,需要借助队列来实现


1 线程基础概述

1.1,线程池的作用 

线程池作用就是限制系统中执行线程的数量。 
根据系统的环境情况,可以自动或手动设置线程数量,达到运行的最佳效果。 
少了浪费了系统资源,多了造成系统拥挤效率不高。 
用线程池控制线程数量,其他线程排 队等候。 
一个任务执行完毕,再从队列的中取最前面的任务开始执行。 
若队列中没有等待进程,线程池的这一资源处于等待。 
当一个新任务需要运行时,如果线程池 中有等待的工作线程,就可以开始运行了;否则进入等待队列。 

1.2,为什么要用线程池? 

1.减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。 
2.可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。 
Java里面线程池的顶级接口是Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是ExecutorService。 

1.3,比较重要的几个类

类 
描述

ExecutorService 
真正的线程池接口。

ScheduledExecutorService 
能和Timer/TimerTask类似,解决那些需要任务重复执行的问题。

ThreadPoolExecutor 
ExecutorService的默认实现。

ScheduledThreadPoolExecutor 
继承ThreadPoolExecutor的ScheduledExecutorService接口实现,周期性任务调度的类实现。

1.4,new Thread的弊端

public class TestNewThread {
 
    public static void main(String[] args) {
        new Thread(new Runnable() {
 
            @Override
            public void run() {
                System.out.println("start");
            }
        }).start();
    }
}

执行一个异步任务你还只是如下new Thread吗? 
那你就out太多了,new Thread的弊端如下: 
1.每次new Thread新建对象性能差。 
2.线程缺乏统一管理,可能无限制新建线程,相互之间竞争,及可能占用过多系统资源导致死机或oom。 
3.缺乏更多功能,如定时执行、定期执行、线程中断。 
相比new Thread,Java提供的四种线程池的好处在于: 
1.重用存在的线程,减少对象创建、消亡的开销,性能佳。 
2.可有效控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。 
3.提供定时执行、定期执行、单线程、并发数控制等功能。

2 四种线程池 

Java通过Executors提供四种线程池,分别为: 
1,newCachedThreadPoo 
创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。 
2,newFixedThreadPool 
创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。 
3,newScheduledThreadPool 
创建一个定长线程池,支持定时及周期性任务执行。 
4,newSingleThreadExecutor 
创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

2.1 源码分析

newCachedThreadPool 
这是一个可缓存线程池,可以灵活的回收空闲线程,无可回收线程时,新建线程 
public static ExecutorService newCachedThreadPool() { 
return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 
60L, TimeUnit.SECONDS, 
new SynchronousQueue()); 
}码可以看出底层调用的是ThreadPoolExecutor方法,传入一个同步的阻塞队列实现

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

通过源码可以看出,我们可以传入线程池的核心线程数(最小线程数),最大线程数量,保持时间,时间单位,阻塞队列这些参数,最大线程数设置为jvm可用的cpu数量为最佳实践

newWorkStealingPool 
创建持有足够线程的线程池来并行,通过使用多个队列减少竞争,不传参数,则默认设定为cpu的数量 
源码:

 public static ExecutorService newWorkStealingPool() {
    return new ForkJoinPool
        (Runtime.getRuntime().availableProcessors(),
         ForkJoinPool.defaultForkJoinWorkerThreadFactory,
         null, true);
}

通过源码可以看出底层调用的是ForkJoinPool线程池

下面说一下ForkJoinPool

 public ForkJoinPool(int parallelism,
                    ForkJoinWorkerThreadFactory factory,
                    UncaughtExceptionHandler handler,
                    boolean asyncMode) {
    this(checkParallelism(parallelism),
         checkFactory(factory),
         handler,
         asyncMode ? FIFO_QUEUE : LIFO_QUEUE,
         "ForkJoinPool-" + nextPoolId() + "-worker-");
    checkPermission();
}

使用一个无限队列来保存需要执行的任务,可以传入线程的数量,不传入,则默认使用当前计算机中可用的cpu数量,使用分治法来解决问题,使用fork()和join()来进行调用

newSingleThreadExecutor 
创建一个单线程化的线程池,保证所有任务按照指定的顺序执行(FIFO,LIFO,优先级),当要求进程限制时,可以进行使用

源码:

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

newFixedThreadPool 
创建一个固定线程数量,可重用的线程池

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

newScheduledThreadPool 
创建一个可定期或者延时执行任务的线程池

源码:

return new ScheduledThreadPoolExecutor(corePoolSize); 

通过源码可以看出底层调用的是一个ScheduledThreadPoolExecutor,然后传入线程数量

下面来介绍一下ScheduledThreadPoolExecutor

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

通过源码可以看出底层调用了ThreadPoolExecutor,维护了一个延迟队列,可以传入线程数量,传入延时的时间等参数,下面给出一个demo

 public static void main(String[] args) {
    ScheduledExecutorService pool = Executors.newScheduledThreadPool(5);
    for (int i = 0; i < 15; i = i + 5) {
        pool.schedule(() -> System.out.println("我被执行了,当前时间" + new Date()), i, TimeUnit.SECONDS);
    }
    pool.shutdown();
}

执行结果

我被执行了,当前时间Fri Jan 12 11:20:41 CST 2018 
我被执行了,当前时间Fri Jan 12 11:20:46 CST 2018 
我被执行了,当前时间Fri Jan 12 11:20:51 CST 2018

为什么使用schedule()而不使用submit()或者execute()呢,下面通过源码来分析

 public void execute(Runnable command) {
    schedule(command, 0, NANOSECONDS);
}
public Future<?> submit(Runnable task) {
    return schedule(task, 0, NANOSECONDS);
}

通过源码可以发现这两个方法都是调用的schedule(),而且将延时时间设置为了0,所以想要实现延时操作,需要直接调用schedule()

下面我们再来分析一下submit()和execute()的以及shutdown()和shutdownNow()的区别

submit(),提交一个线程任务,可以接受回调函数的返回值吗,适用于需要处理返回着或者异常的业务场景 
execute(),执行一个任务,没有返回值 
shutdown(),表示不再接受新任务,但不会强行终止已经提交或者正在执行中的任务 
shutdownNow(),对于尚未执行的任务全部取消,正在执行的任务全部发出interrupt(),停止执行 
五种线程池的适应场景 
newCachedThreadPool:用来创建一个可以无限扩大的线程池,适用于服务器负载较轻,执行很多短期异步任务。 
newFixedThreadPool:创建一个固定大小的线程池,因为采用无界的阻塞队列,所以实际线程数量永远不会变化,适用于可以预测线程数量的业务中,或者服务器负载较重,对当前线程数量进行限制。 
newSingleThreadExecutor:创建一个单线程的线程池,适用于需要保证顺序执行各个任务,并且在任意时间点,不会有多个线程是活动的场景。 
newScheduledThreadPool:可以延时启动,定时启动的线程池,适用于需要多个后台线程执行周期任务的场景。 
newWorkStealingPool:创建一个拥有多个任务队列的线程池,可以减少连接数,创建当前可用cpu数量的线程来并行执行,适用于大耗时的操作,可以并行来执行

2.2 RejectedExecutionHandler 线程池四种拒绝任务策略

《Java线程池》:任务拒绝策略 
在没有分析线程池原理之前先来分析下为什么有任务拒绝的情况发生。

这里先假设一个前提:线程池有一个任务队列,用于缓存所有待处理的任务,正在处理的任务将从任务队列中移除。因此在任务队列长度有限的情况下就会出现新任务的拒绝处理问题,需要有一种策略来处理应该加入任务队列却因为队列已满无法加入的情况。另外在线程池关闭的时候也需要对任务加入队列操作进行额外的协调处理。

RejectedExecutionHandler提供了四种方式来处理任务拒绝策略

1、直接丢弃(DiscardPolicy)

2、丢弃队列中最老的任务(DiscardOldestPolicy)。

3、抛异常(AbortPolicy)

4、将任务分给调用线程来执行(CallerRunsPolicy)。

这四种策略是独立无关的,是对任务拒绝处理的四中表现形式。最简单的方式就是直接丢弃任务。但是却有两种方式,到底是该丢弃哪一个任务,比如可以丢弃当前将要加入队列的任务本身(DiscardPolicy)或者丢弃任务队列中最旧任务(DiscardOldestPolicy)。丢弃最旧任务也不是简单的丢弃最旧的任务,而是有一些额外的处理。除了丢弃任务还可以直接抛出一个异常(RejectedExecutionException),这是比较简单的方式。抛出异常的方式(AbortPolicy)尽管实现方式比较简单,但是由于抛出一个RuntimeException,因此会中断调用者的处理过程。除了抛出异常以外还可以不进入线程池执行,在这种方式(CallerRunsPolicy)中任务将有调用者线程去执行。

2.3 示例使用

1,newCachedThreadPool 

创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程, 那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。

package io.ymq.thread.demo1;
 
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
 
/**
 * 描述: 创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。
 * 此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
 *
 * @author yanpenglei
 * @create 2017-10-12 11:13
 **/
public class TestNewCachedThreadPool {
    public static void main(String[] args) {
        ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
        for (int i = 1; i <= 10; i++) {
            final int index = i;
            try {
                Thread.sleep(index * 1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
 
            cachedThreadPool.execute(new Runnable() {
 
                @Override
                public void run() {
                    String threadName = Thread.currentThread().getName();
                    System.out.println("执行:" + index + ",线程名称:" + threadName);
                }
            });
        }
    }
}
响应: 
执行:1,线程名称:pool-1-thread-1 
执行:2,线程名称:pool-1-thread-1 
执行:3,线程名称:pool-1-thread-1 
执行:4,线程名称:pool-1-thread-1 
执行:5,线程名称:pool-1-thread-1 
执行:6,线程名称:pool-1-thread-1 
执行:7,线程名称:pool-1-thread-1 
执行:8,线程名称:pool-1-thread-1 
执行:9,线程名称:pool-1-thread-1 
执行:10,线程名称:pool-1-thread-1 

2,newFixedThreadPool 

描述:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。 
线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。

package io.ymq.thread.demo2;
 
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
 
/**
 * 描述:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。
 * 线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
 *
 * @author yanpenglei
 * @create 2017-10-12 11:30
 **/
public class TestNewFixedThreadPool {
 
    public static void main(String[] args) {
 
        ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
 
        for (int i = 1; i <= 10; i++) {
            final int index = i;
            fixedThreadPool.execute(new Runnable() {
 
                @Override
                public void run() {
                    try {
                        String threadName = Thread.currentThread().getName();
                        System.out.println("执行:" + index + ",线程名称:" + threadName);
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
 
                        e.printStackTrace();
                    }
                }
            });
        }
 
    }
}

因为线程池大小为3,每个任务输出index后sleep 2秒,所以每两秒打印3个数字,和线程名称。 
响应: 

执行:2,线程名称:pool-1-thread-2 
执行:3,线程名称:pool-1-thread-3 
执行:1,线程名称:pool-1-thread-1

执行:4,线程名称:pool-1-thread-1 
执行:6,线程名称:pool-1-thread-2 
执行:5,线程名称:pool-1-thread-3

执行:7,线程名称:pool-1-thread-1 
执行:9,线程名称:pool-1-thread-3 
执行:8,线程名称:pool-1-thread-2

执行:10,线程名称:pool-1-thread-1

3,newScheduledThreadPool 

创建一个定长线程池,支持定时及周期性任务执行。延迟执行

package io.ymq.thread.demo3;
 
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
 
/**
 * 描述:创建一个定长线程池,支持定时及周期性任务执行。延迟执行
 *
 * @author yanpenglei
 * @create 2017-10-12 11:53
 **/
public class TestNewScheduledThreadPool {
 
    public static void main(String[] args) {
 
        ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
 
        scheduledThreadPool.schedule(new Runnable() {
 
            @Override
            public void run() {
                System.out.println("表示延迟3秒执行。");
            }
        }, 3, TimeUnit.SECONDS);
 
 
        scheduledThreadPool.scheduleAtFixedRate(new Runnable() {
 
            @Override
            public void run() {
                System.out.println("表示延迟1秒后每3秒执行一次。");
            }
        }, 1, 3, TimeUnit.SECONDS);
    }
 
}
表示延迟1秒后每3秒执行一次。 
表示延迟3秒执行。 
表示延迟1秒后每3秒执行一次。 
表示延迟1秒后每3秒执行一次。 
表示延迟1秒后每3秒执行一次。 
表示延迟1秒后每3秒执行一次。 

4,newSingleThreadExecutor 

创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

package io.ymq.thread.demo4;
 
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
 
/**
 * 描述:创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
 *
 * @author yanpenglei
 * @create 2017-10-12 12:05
 **/
public class TestNewSingleThreadExecutor {
 
    public static void main(String[] args) {
        ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
        for (int i = 1; i <= 10; i++) {
            final int index = i;
            singleThreadExecutor.execute(new Runnable() {
 
                @Override
                public void run() {
                    try {
                        String threadName = Thread.currentThread().getName();
                        System.out.println("执行:" + index + ",线程名称:" + threadName);
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }
}
结果依次输出,相当于顺序执行各个任务。 
响应: 
执行:1,线程名称:pool-1-thread-1 
执行:2,线程名称:pool-1-thread-1 
执行:3,线程名称:pool-1-thread-1 
执行:4,线程名称:pool-1-thread-1 
执行:5,线程名称:pool-1-thread-1 
执行:6,线程名称:pool-1-thread-1 
执行:7,线程名称:pool-1-thread-1 
执行:8,线程名称:pool-1-thread-1 
执行:9,线程名称:pool-1-thread-1 
执行:10,线程名称:pool-1-thread-1

3 自定义线程池

提交一个任务到线程池中,线程池的处理流程如下:

1、判断线程池里的核心线程是否都在执行任务,如果不是(核心线程空闲或者还有核心线程没有被创建)则创建一个新的工作线程来执行任务。如果核心线程都在执行任务,则进入下个流程。

2、线程池判断工作队列是否已满,如果工作队列没有满,则将新提交的任务存储在这个工作队列里。如果工作队列满了,则进入下个流程。

3、判断线程池里的线程是否都处于工作状态,如果没有,则创建一个新的工作线程来执行

任务。如果已经满了,则交给饱和策略来处理这个任务。

3.1 整合懒汉模式和自定义线程池

public class ThreadPools {
    private volatile static ThreadPools threadPool = null;
    //核心线程数,实际运行的线程数
    public static final int corePoolSize = 1;
    //最大线程数
    public static final int maximumPoolSize = 2;
    //存活时间
    public static final long keepAliveTime = 60L;
    public static final TimeUnit unit = TimeUnit.SECONDS;
    //缓存队列
    public static final ArrayBlockingQueue<Runnable> arrayBlockingQueue = new ArrayBlockingQueue(3);
    //线程池
    public static ThreadPoolExecutor threadPoolExecutor = null;
    private ThreadPools(){
        threadPoolExecutor = new ThreadPoolExecutor(corePoolSize,maximumPoolSize,keepAliveTime,unit,arrayBlockingQueue);
    }
    public static void executor(int num,Runnable runnable){
        if(num <= 0){
            return;
        }
        if(threadPool == null){
            synchronized (ThreadPools.class){
                if(threadPool == null){
                    threadPool = new ThreadPools();
                }
            }
        }
        for(int i = 0;i < num;i++){
            threadPoolExecutor.execute(runnable);
        }
        threadPoolExecutor.shutdown();
    }
}
 
class Run implements Runnable{
    @Override
    public void run() {
        //具体操作
        System.out.println(Thread.currentThread().getName());
    }
}
public class Test {
    public static void main(String[] args) {
        ThreadPools.executor(5,new Run());
    }
}

3.2 自定义线程池工具类,需要借助队列来实现

1.首先线程池中线程个数,有默认线程数

2.工作线程组,线程池启动时有默认线程数的线程运行(从阻塞队列中获取去线程并运行)

3.线程任务,用户需要执行的线程任务

4.BlockingQueue阻塞队列

5.线程池需要有execute方法,用来执行于把用户的线程任务放在队列中去

6.销毁线程方法interrupt,并清空队列

这里注意:taskQueue既是任务队列,也是线程工作池的队列,都从此队列取任务执行

public class ThreadPoolDemo {
    //线程中默认线程的个数
    private static int threadCount = 5;
    //队列中默认任务的个数
    private static int queueCount = 100;
    //工作线程组
    private WorkThread[] workThreads;
    //任务队列,作为一个缓冲,
    //这里注意:taskQueue既是任务队列,也是线程工作池的队列,都从此队列取任务执行。
    private BlockingQueue<Runnable> taskQueue;
    //用户在构建线程池的时候,希望启动的线程数
    private int work_num;

    /**
     * @param work_num  线程池中工作线程的个数
     * @param taskCount
     */
    public ThreadPoolDemo(int work_num, int taskCount) {
        if (work_num <= 0) work_num = threadCount;
        if (taskCount <= 0) taskCount = queueCount;
        this.work_num = work_num;
        taskQueue = new ArrayBlockingQueue<>(taskCount);
        workThreads = new WorkThread[work_num];
        for (int i = 0; i <work_num ; i++) {
            workThreads[i]=new WorkThread();
            workThreads[i].start();
        }
        
        //Runtime.getRuntime().availableProcessors();
    }

    /**
     * 执行任务,其实只是把任务加入任务队列,什么时候执行有线程池管理器决定
     * @param task
     */
    public void execute(Runnable task){
        try {
            taskQueue.put(task);
            System.out.println("....线程队列大小................>>>>>>"+taskQueue.size());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    /**
     * 销毁线程池,该方法保证在所有任务都完成的情况下才销毁所有线程,否则等待任务完成才销毁
     */
    public void destory(){
        System.out.println(".....ready close pool");
        for (int i = 0; i < work_num; i++) {
            WorkThread t = workThreads[i];
            if(t.isAlive()){
                t.stopWork();
            }
            workThreads[i]=null;
        }
        taskQueue.clear();//清空任务队列
        System.out.println("线程池销毁/");
    }
    // 覆盖toString方法,返回线程池信息:工作线程个数和已完成任务个数
    @Override
    public String toString() {
        return "WorkThread number:" + work_num
                + "  wait task number:" + taskQueue.size();
    }
    public int getQueueCount(){
       return taskQueue.size();
    }

    private class WorkThread extends Thread {
        @Override
        public void run() {
            Runnable r = null;
            while (!interrupted()) {
                try {
                    if(taskQueue.size()>0){
                        r = taskQueue.take();
                        if (r != null) {
                            System.out.println("线程......" + r + "......ready exec ........");
                            r.run();
                        }
                        r = null;//heap gc
                    }

                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }
        }
        public void stopWork(){
            interrupt();
        }
    }

}

测试类

public class PoolTest {
    public static void main(String[] args) {
        ThreadPoolDemo pool = new ThreadPoolDemo(3,0);
        MyTask t = new MyTask("test thread A");
        pool.execute(new MyTask("test thread A"));
        pool.execute(new MyTask("test thread B"));
        pool.execute(new MyTask("test thread C"));
        pool.execute(new MyTask("test thread D"));
        pool.execute(new MyTask("test thread E"));
        System.out.println(pool);
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        int queueCount = pool.getQueueCount();
        if(queueCount==0){
            pool.destory();
        }
        System.out.println(pool);
    }

    public static class MyTask extends Thread {
        private String name;

        public MyTask(String name) {
            this.name = name;
        }

        @Override
        public String toString() {
            return this.name;
        }

        @Override
        public void run() {
            try {
                Thread.currentThread().setName(name);
                Thread.sleep(new Random().nextInt(1000)+2000 );

            } catch (InterruptedException e) {
                System.out.println("任务....." + name + ".......sleep InterruptedException:" + Thread.currentThread().isInterrupted());
                //Thread.currentThread().interrupt();
            }
            System.out.println("任务....." + name + ".....完成");
        }

    }
}

运行结果:

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值