JUC线程池-ThreadPoolExecutor源码详细解读

最近在读JUC源码包的源码,参考了大量JUC线程池: ThreadPoolExecutor详解的内容后,这里做一下笔记,其中掺杂了我阅读源码对原文的一些补充,小伙伴可以关注 https://www.pdai.tech/,是一个我个人认为非常不错的技术博主写的java全栈知识体系

ThreadPoolExecutor使用详解

其实java线程池的实现原理很简单,说白了就是一个线程集合workerSet和一个阻塞队列workQueue。当用户向线程池提交一个任务(也就是线程)时,线程池会先将任务放入workQueue中workerSet中的线程会不断的从workQueue中获取线程然后执行。当workQueue中没有任务的时候,worker就会阻塞,直到队列中有任务了就取出来继续执行。

在这里插入图片描述

Execute原理

当一个任务提交至线程池之后(任务执行前三步判断):

  1. 线程池首先当前运行的线程数量是否少于corePoolSize。如果是,则创建一个新的工作线程来执行任务。如果都在执行任务,则进入2.
  2. 判断BlockingQueue是否已经满了,倘若还没有满,则将线程放入BlockingQueue。否则进入3.
  3. 如果创建一个新的工作线程将使当前运行的线程数量超过maximumPoolSize,则交给RejectedExecutionHandler来处理任务。

当ThreadPoolExecutor创建新线程时,通过CAS来更新线程池的状态ctl.

也就是优先使用corePoolSize个线程,如果核心线程数都在工作,那么再放入阻塞队列,如果阻塞队列也满了,那么就启动额外的线程来工作,直到达到max线程数,如果max线程也都满负荷了,再来任务只能执行拒绝策略

参数

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              RejectedExecutionHandler handler)
  • corePoolSize 线程池中的核心线程数,当提交一个任务时,线程池创建一个新线程执行任务,直到当前线程数等于corePoolSize, 即使有其他空闲线程能够执行新来的任务, 也会继续创建线程;如果当前线程数为corePoolSize,继续提交的任务被保存到阻塞队列中,等待被执行;如果执行了线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动所有核心线程。
  • workQueue 用来保存等待被执行的任务的阻塞队列. 在JDK中提供了如下阻塞队列: 具体可以参考JUC 集合: BlockQueue详解
    • ArrayBlockingQueue: 基于数组结构的有界阻塞队列,按FIFO排序任务;
    • LinkedBlockingQuene: 基于链表结构的阻塞队列,按FIFO排序任务,吞吐量通常要高于ArrayBlockingQuene;
    • SynchronousQuene: 一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQuene
    • PriorityBlockingQuene: 具有优先级的无界阻塞队列;

LinkedBlockingQueueArrayBlockingQueue在插入删除节点性能方面更优,但是二者在put(), take()任务的时均需要加锁,SynchronousQueue使用无锁算法,根据节点的状态判断执行,而不需要用到锁,其核心是Transfer.transfer().

  • maximumPoolSize 线程池中允许的最大线程数。如果当前阻塞队列满了,且继续提交任务,则创建新的线程执行任务,前提是当前线程数小于maximumPoolSize;当阻塞队列是无界队列, 则maximumPoolSize则不起作用, 因为无法提交至核心线程池的线程会一直持续地放入workQueue.
  • keepAliveTime 线程空闲时的存活时间,即当线程没有任务执行时,该线程继续存活的时间;默认情况下,该参数只在线程数大于corePoolSize时才有用, 超过这个时间的空闲线程将被终止;
  • unit keepAliveTime的单位
  • threadFactory 创建线程的工厂,通过自定义的线程工厂可以给每个新建的线程设置一个具有识别度的线程名。默认为DefaultThreadFactory
  • handler 线程池的饱和策略,当阻塞队列满了,且没有空闲的工作线程,如果继续提交任务,必须采取一种策略处理该任务,线程池提供了4种策略:
    • AbortPolicy: 直接抛出异常,默认策略;
    • CallerRunsPolicy: 用调用者所在的线程来执行任务;
    • DiscardOldestPolicy: 丢弃阻塞队列中靠最前的任务,并执行当前任务;
    • DiscardPolicy: 直接丢弃任务;

当然也可以根据应用场景实现RejectedExecutionHandler接口,自定义饱和策略,如记录日志或持久化存储不能处理的任务。

三种类型

这里指的三种类型是 Executors工具类提供的三种线程池参数模板,最终都是调用ThreadPoolExecutor构造出来

newFixedThreadPool

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

线程池的线程数量达corePoolSize后(core=max),即使线程池没有可执行任务时,也不会释放线程。

FixedThreadPool的工作队列为无界队列LinkedBlockingQueue(队列容量为Integer.MAX_VALUE), 这会导致以下问题:

  • 线程池里的线程数量不超过corePoolSize,这导致了maximumPoolSize和keepAliveTime将会是个无用参数
  • 由于使用了无界队列, 所以FixedThreadPool永远不会拒绝, 即饱和策略失效

newSingleThreadExecutor

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

初始化的线程池中只有一个线程,如果该线程异常结束,会重新创建一个新的线程继续执行任务,唯一的线程可以保证所提交任务的顺序执行.(core=max=1)

由于使用了无界队列, 所以SingleThreadPool永远不会拒绝, 即饱和策略失效

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

线程池的线程数可达到Integer.MAX_VALUE,即2147483647,内部使用SynchronousQueue作为阻塞队列; 和newFixedThreadPool创建的线程池不同,newCachedThreadPool在没有任务执行时,当线程的空闲时间超过keepAliveTime,会自动释放线程资源,当提交新任务时,如果没有空闲线程,则创建新线程执行任务,会导致一定的系统开销; 执行过程与前两种稍微不同:

  • 主线程调用SynchronousQueue的offer()方法放入task, 倘若此时线程池中有空闲的线程尝试读取 SynchronousQueue的task, 即调用了SynchronousQueue的poll(), 那么主线程将该task交给空闲线程. 否则执行(2)
  • 当线程池为空或者没有空闲的线程, 则创建新的线程执行任务.
  • 执行完任务的线程倘若在60s内仍空闲, 则会被终止. 因此长时间空闲的CachedThreadPool不会持有任何线程资源.

也就是说,因为SynchronousQueue是个同步阻塞队列,并且只能容纳一个任务,所以如果submit的时候,发现已经有空闲线程在等着了,那么就直接将task交给空闲线程;

如果submit的时候发现没有空闲线程,那么就创建新线程;

关闭线程池

遍历线程池中的所有线程,然后逐个调用线程的interrupt方法来中断线程.

关闭方式 - shutdown

将线程池里的线程状态设置成SHUTDOWN状态, 然后中断所有没有正在执行任务的线程.

关闭方式 - shutdownNow

将线程池里的线程状态设置成STOP状态, 然后停止所有正在执行或暂停任务的线程.

这里貌似有点问题,shutDown的时候修改的应该是线程池状态

只要调用这两个关闭方法中的任意一个, isShutDown() 返回true. 当所有任务都成功关闭了, isTerminated()返回true.

ThreadPoolExecutor源码详解

几个关键属性

//这个属性是用来存放 当前运行的worker数量以及线程池状态的
//int是32位的,这里把int的高3位拿来充当线程池状态的标志位,后29位拿来充当当前运行worker的数量
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
//存放任务的阻塞队列
private final BlockingQueue<Runnable> workQueue;
//worker的集合,用set来存放
private final HashSet<Worker> workers = new HashSet<Worker>();
//历史达到的worker数最大值
private int largestPoolSize;
//当队列满了并且worker的数量达到maxSize的时候,执行具体的拒绝策略
private volatile RejectedExecutionHandler handler;
//超出coreSize的worker的生存时间
private volatile long keepAliveTime;
//常驻worker的数量
private volatile int corePoolSize;
//最大worker的数量,一般当workQueue满了才会用到这个参数
private volatile int maximumPoolSize;

内部状态

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static final int COUNT_BITS = Integer.SIZE - 3;    //29
private static final int CAPACITY   = (1 << COUNT_BITS) - 1;   //00011111111..(29个1),也说明了用低29位来存储worker数目,29个1表示最大worker数目,另外29个1也起到了掩码作用,用于后续的 & 和 | 操作

// runState is stored in the high-order bits
//-1 转为二进制位 111...11(32个1),<<29之后,就是11100000..000(29个0),这也侧面反映了左移29位,也就是使用int ctl 高三位来表示state
private static final int RUNNING    = -1 << COUNT_BITS;			//高三位111
private static final int SHUTDOWN   =  0 << COUNT_BITS;			//高三位000
private static final int STOP       =  1 << COUNT_BITS;			//高三位001
private static final int TIDYING    =  2 << COUNT_BITS;			//高三位010
private static final int TERMINATED =  3 << COUNT_BITS;			//高三位011

// Packing and unpacking ctl
private static int runStateOf(int c)     { return c & ~CAPACITY; }
private static int workerCountOf(int c)  { return c & CAPACITY; }
private static int ctlOf(int rs, int wc) { return rs | wc; }

其中AtomicInteger变量ctl的功能非常强大: 利用低29位表示线程池中线程数,通过高3位表示线程池的运行状态:

  • RUNNING: -1 << COUNT_BITS,即高3位为111,该状态的线程池会接收新任务,并处理阻塞队列中的任务;
  • SHUTDOWN: 0 << COUNT_BITS,即高3位为000,该状态的线程池不会接收新任务,但会处理阻塞队列中的任务;
  • STOP : 1 << COUNT_BITS,即高3位为001,该状态的线程不会接收新任务,也不会处理阻塞队列中的任务,而且会中断正在运行的任务;(注意和SHUTDOWN状态区别)
  • TIDYING : 2 << COUNT_BITS,即高3位为010, 所有的任务都已经终止;
  • TERMINATED: 3 << COUNT_BITS,即高3位为011, terminated()方法已经执行完成
  • 可以看出,从数值上来说, RUNNING < SHUTDOWN < STOP < TIDYING < TERMINATED

在这里插入图片描述

任务执行

execute –> addWorker –>runworker (getTask)

线程池的工作线程通过Woker类实现,在ReentrantLock锁的保证下,把Woker实例插入到HashSet后,并启动Woker中的线程。 从Woker类的构造方法实现可以发现: 线程工厂在创建线程thread时,将Woker实例本身this作为参数传入,当执行start方法启动线程thread时,本质是执行了Worker的runWorker方法。 firstTask执行完成之后,通过getTask方法从阻塞队列中获取等待的任务,如果队列中没有任务,getTask方法会被阻塞并挂起,不会占用cpu资源;

        Worker(Runnable firstTask) {
            setState(-1); // inhibit interrupts until runWorker
            this.firstTask = firstTask;
            this.thread = getThreadFactory().newThread(this);
        }
				public void run() {
            runWorker(this);
        }

Worker本身是一个Runnable 实现,而在其构造内部的Thread线程对象的时候,将自己传给了这个Thread,也就意味着这个thread.start()的时候,要调用worker的run()方法

而addWorker方法内部,则是先new了一个Worker,然后拿出worker内部的thread对象,调用thread.start();所以顺理成章的线程启动,而执行的是runWorker方法,在runWorker()内部则是不断的阻塞获取任务队列的任务;

execute()方法

ThreadPoolExecutor.execute(task)实现了Executor.execute(task)

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
   //这里开始判断core,queue,max,也就是任务执行前的三个参数判断,来决定是使用core线程,还是入队,还是使用max线程,或者是拒绝
    int c = ctl.get();
    if (workerCountOf(c) < corePoolSize) {  
    //workerCountOf获取线程池的当前线程数;小于corePoolSize,执行addWorker创建新线程执行command任务
       if (addWorker(command, true))
            return;			//addWorker成功,则直接return了
        c = ctl.get();		//不成功,或者worker数目 > core,则执行下面的,这里重新获取ctl,因为addWorker可能并发
    }
    // double check: c, recheck
    // 线程池处于RUNNING状态,把提交的任务成功放入阻塞队列中(队列未满)
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
      //将cmd 入队之后再check一次状态?保证cmd入队时一定是 RUNNING的,因为上面是先检查 RUNNING在 进入队列,由于时间差,可能进入队列时 线程池不可用了,所以入队后再查一次,保证cmd进入的时候一定是RUNNING
        // recheck and if necessary 回滚到入队操作前,即倘若线程池shutdown状态,就remove(command)
        //如果线程池没有RUNNING,成功从阻塞队列中删除任务,执行reject方法处理任务
        if (! isRunning(recheck) && remove(command))
            reject(command);
        //线程池处于running状态,但是没有线程,则创建线程
      //虽然上面是两个判断! isRunning(recheck) && remove(command),但是这个循环进入的前提是 workQueue.offer(command) 成功,所以remove(command)一般也只能返回true,所以下面的else if,针对的是! isRunning(recheck)返回false ,也就是isRunning(recheck) = true,线程池running
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    // 往线程池中创建新的线程失败,则reject任务
  //这里没有再判断RUNNING,因为addWorker中判断了
  //看起来判断 线程池RUNNIGN 在这里是跟 queue操作绑定在一起;
  //也就是避免 入队成功,但是线程池stop了,导致没有线程执行cmd
    else if (!addWorker(command, false))
        reject(command);
}


  • 为什么需要double check线程池的状态?

在多线程环境下,线程池的状态时刻在变化,而ctl.get()是非原子操作,很有可能刚获取了线程池状态后线程池状态就改变了。判断是否将command加入workque是线程池之前的状态。倘若没有double check,万一线程池处于非running状态(在多线程环境下很有可能发生),那么command永远不会执行(放进去也没用,必须马上返回错误)。

addWorker方法

从方法execute的实现可以看出: addWorker主要负责创建新的线程并执行任务 线程池创建新线程执行任务时,需要 获取全局锁:

private final ReentrantLock mainLock = new ReentrantLock();
private boolean addWorker(Runnable firstTask, boolean core) {
    // CAS更新线程池数量
    retry:
    for (;;) {
        int c = ctl.get();
        int rs = runStateOf(c);

        // Check if queue empty only if necessary.
        if (rs >= SHUTDOWN &&
            ! (rs == SHUTDOWN &&
                firstTask == null &&
                ! workQueue.isEmpty()))			//如果workQueue 非空,说明其他线程已经占满core并且往workQueue里放入cmd了,所以这里就不创建core了
            return false;

        for (;;) {
            int wc = workerCountOf(c);		//重新检查coreSize
            if (wc >= CAPACITY ||
                wc >= (core ? corePoolSize : maximumPoolSize))
                return false;
            if (compareAndIncrementWorkerCount(c))  //cas + 1 workerCount
                break retry;		//成功则退出循环
            c = ctl.get();  // Re-read ctl
            if (runStateOf(c) != rs)			//如果状态变了,则重新执行外层循环,来判断状态
                continue retry;
            // 同样的,里面的for循环如果 只是因为cas 修改workerCount不成功而 ctl中的state不变,则会一直内层循环
        }
    }

    boolean workerStarted = false;
    boolean workerAdded = false;
    Worker w = null;
    try {
        w = new Worker(firstTask);
        final Thread t = w.thread;
        if (t != null) {
            // 线程池重入锁
            final ReentrantLock mainLock = this.mainLock;
            mainLock.lock();			//当前线程获取到线程池的mainLock并上锁,避免并发加入Worker到WorkerSet?
            try {
                // Recheck while holding lock.
                // Back out on ThreadFactory failure or if
                // shut down before lock acquired.
                int rs = runStateOf(ctl.get());

                if (rs < SHUTDOWN ||
                    (rs == SHUTDOWN && firstTask == null)) {
                    if (t.isAlive()) // precheck that t is startable
                        throw new IllegalThreadStateException();
                    workers.add(w);
                    int s = workers.size();
                    if (s > largestPoolSize)
                        largestPoolSize = s;			//更新历史最大worker数
                    workerAdded = true;
                }
            } finally {
                mainLock.unlock();
            }
            if (workerAdded) {
                t.start();  // 线程启动,执行任务(Worker.thread(firstTask).start());
                workerStarted = true;
            }
        }
    } finally {
        if (! workerStarted)
            addWorkerFailed(w);
    }
    return workerStarted;
}

Worker类的runworker方法

worker中的thread.start()之后,就执行worker的run方法,其实就是执行runWorker()

 private final class Worker extends AbstractQueuedSynchronizer implements Runnable{
     Worker(Runnable firstTask) {
         setState(-1); // inhibit interrupts until runWorker
         this.firstTask = firstTask;
         this.thread = getThreadFactory().newThread(this); // 创建线程
     }
     /** Delegates main run loop to outer runWorker  */
     public void run() {
         runWorker(this);
     }
     // ...
 }

addWorker中多处可能检查不通过返回false,则回到execute继续执行,再尝试放入queue或者使用max线程,或者reject

  • 继承了AQS类,可以方便的实现工作线程的中止操作;
  • 实现了Runnable接口,可以将自身作为一个任务在工作线程中执行;
  • 当前提交的任务firstTask作为参数传入Worker的构造方法;

一些属性还有构造方法:

//运行的线程,前面addWorker方法中就是直接通过启动这个线程来启动这个worker
final Thread thread;
//当一个worker刚创建的时候,就先尝试执行这个任务
Runnable firstTask;
//记录完成任务的数量
volatile long completedTasks;

Worker(Runnable firstTask) {
    setState(-1); // inhibit interrupts until runWorker
    this.firstTask = firstTask;
    //创建一个Thread,将自己设置给他,后面这个thread启动的时候,也就是执行worker的run方法
    this.thread = getThreadFactory().newThread(this);
}   

Worker本身是一个AQS的极简实现,用于实现一个互斥锁

 protected boolean isHeldExclusively() {
            return getState() != 0;
        }

        protected boolean tryAcquire(int unused) {
            if (compareAndSetState(0, 1)) {
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }

        protected boolean tryRelease(int unused) {
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }

        public void lock()        { acquire(1); }
        public boolean tryLock()  { return tryAcquire(1); }
        public void unlock()      { release(1); }
        public boolean isLocked() { return isHeldExclusively(); }

内部的thread在执行 runWorker(Worker w)方法的时候,会使用当前worker对象作为锁,来保证这个线程完全占有这个worker,不会被其他线程抢占这个锁;

runWorker方法是线程池的核心:

  • 线程启动之后,通过unlock方法释放锁,设置AQS的state为0,表示运行可中断;
  • Worker执行firstTask或从workQueue中获取任务:
    • 进行加锁操作,保证thread不被其他线程中断(除非线程池被中断)
    • 检查线程池状态,倘若线程池处于中断状态,当前线程将中断。
    • 执行beforeExecute
    • 执行任务的run方法
    • 执行afterExecute方法
    • 解锁操作

通过getTask方法从阻塞队列中获取等待的任务,如果队列中没有任务,getTask方法会被阻塞并挂起,不会占用cpu资源;

其实Interrupt()方法本身不会中断线程执行,中断线程执行的通常是 那些判断逻辑,比如判断线程被 interrupt了,则抛出异常,抛出的异常会中断线程执行

final void runWorker(Worker w) {
    Thread wt = Thread.currentThread();
    Runnable task = w.firstTask;
    w.firstTask = null;
    w.unlock(); // allow interrupts
    boolean completedAbruptly = true;
    try {
        // 先执行firstTask,再从workerQueue中取task(getTask())
				//这里getTask方法会响应中断来抛出异常,从而中断当前worker和线程
        while (task != null || (task = getTask()) != null) {
            w.lock();		
            // If pool is stopping, ensure thread is interrupted;
            // if not, ensure thread is not interrupted.  This
            // requires a recheck in second case to deal with
            // shutdownNow race while clearing interrupt
            if ((runStateAtLeast(ctl.get(), STOP) ||
                    (Thread.interrupted() &&			//这里似乎是担心由于ShutDownNow引起的线程中断,所以要重新比较一次state,runStateAtLeast(ctl.get(), STOP)返回true,则代表已经STOP了,并且Thread.interrupted()判断成功也说明是被shutDownNow中断了
                     
                    runStateAtLeast(ctl.get(), STOP))) &&
                !wt.isInterrupted())
                wt.interrupt();
            try {
                beforeExecute(wt, task);
                Throwable thrown = null;
                try {
                    task.run();
                } catch (RuntimeException x) {	//总共三层finally
                    thrown = x; throw x;			//抛出异常,打断循环,直接进入上面两层的finally
                } catch (Error x) {
                    thrown = x; throw x;
                } catch (Throwable x) {
                    thrown = x; throw new Error(x);
                } finally {
                    afterExecute(task, thrown);
                }
            } finally {
                task = null;
                w.completedTasks++;
                w.unlock();
            }
        }
        completedAbruptly = false;
    } finally {
        processWorkerExit(w, completedAbruptly);
    }
}
  

beforeExecute,afterExecute是一些前置后置函数,可以被自定义

比如afterExecute可以再抛出异常后进行一定处理。

上面提到的w.unlock(); // allow interrupts,以及w.lock()是忽略中断的含义是:

  • 这里允许中断和忽略中断指的是 允许忽略 shutDown方法造成的中断
  • 因为shutDown方法内,会对所有idle线程进行中断,中断前会获取每个worker的锁,也就是调用每个worker对象的tryLock()方法,成功了才会 中断worker内部thread
  • 所以,一旦unlock了,那么shutdown就可以中断worker,也就可以让worker从getTask()阻塞时抛异常,从而让worker无法取出task
  • 如果while循环进入,那么就说明成功获取task,那么再加锁成功的话,就说明此时已经有一个task提交过来并且被当前thread拿到,需要执行完;那么根据设计预期,shutDown就无法对这个已经获取了worker锁的线程中断,只能等它结束
  • 同样,如果
  • 额外的,如果运行期间抛异常了,则会一层层抛出来,直到最外层的finally(三层try);从而执行processWorkerExit()

对于上面

           if ((runStateAtLeast(ctl.get(), STOP) ||
                        (Thread.interrupted() &&
                         runStateAtLeast(ctl.get(), STOP))) &&
                    !wt.isInterrupted())

解读:

  • 这个条件可以看做,要么是runStateAtLeast(ctl.get(), STOP) && !wt.isInterrupted();要么是 (Thread.interrupted() && runStateAtLeast(ctl.get(), STOP))) && !wt.isInterrupted()
  • 所以首先,如果已经线程池state >= STOP了,那么就看线程状态是不是未中断;不是的话也要给他中断;这就是响应 shutDownNow()
  • 如果线程池还没到STOP;那么就看当前线程是否中断(由于这里会清除中断标记),同时再次检查 线程池状态是否STOP,由于并发时间差,所以很有可能刚刚调用了shutDownNow()来给当前线程设置了中断标记;但是被这里的Thread.interrupted()判断给抹去了,所以再次进入if,给中断标记设置上
  • 所以这里说明,shutDown方法,无法给已经获取lock的worker打上中断标记,所以这个worker可以正常的执行while循环来获取task;,而shutDownNow()就可以,而shutDownNow()方法打上标记之后,不会影响当前task,但是下一次while则会退出;

getTask方法

下面来看一下getTask()方法,这里面涉及到keepAliveTime的使用,从这个方法我们可以看出线程池是怎么让超过corePoolSize的那部分worker销毁的

private Runnable getTask() {
        boolean timedOut = false; // Did the last poll() time out?

        for (;;) {
            int c = ctl.get();
            int rs = runStateOf(c);

            // Check if queue empty only if necessary.
            if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
                decrementWorkerCount();
                return null;
            }		//如果线程池已经进入销毁状态,此时如果STOP了,则不取出新的task,如果还没到STOP,仍然是SHUTDOWN,则看看workQueue是不是空,如果是空,也不取task了

            int wc = workerCountOf(c);

            // Are workers subject to culling?
          /*
         * allowCoreThreadTimeOut是用于设置核心线程是否受keepAliveTime影响。
         * 在allowCoreThreadTimeOut为true或者工作线程数>corePoolSize情况下,
         * 当前工作线程受keepAliveTime影响。
         */
            boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

         /* 这里是判断当前线程能不能返回null,也就是能不能退出runWorker中的while 循环getTask
         * 1. 工作线程数>maximumPoolSize,当前工作线程需要退出。
         * 2. timed && timedOut == true说明当前线程受keepAliveTime影响且上次获取任务超时。
         *    这种情况下只要当前线程不是最后一个工作线程或者任务队列为空,则可以退出。(因为wc > 1,应该有其他线程来处理,为线程数是 cas原子操作,所以不会造成两个线程同时 从1->0 )
         *    换句话说就是,如果队列不为空,则当前线程不能是最后一个工作线程,
         *    否则退出了就没线程处理任务了。
         */ 
            if ((wc > maximumPoolSize || (timed && timedOut))
                && (wc > 1 || workQueue.isEmpty())) {
              //上面通过了,则判断当前线程可以退出,则尝试修改workerCount
              //这里是cas操作,避免两个线程同时都认为自己可以退出,同时退出而没有线程处理task
                if (compareAndDecrementWorkerCount(c))
                    return null;
                continue;   //cas失败,则下一次循环判断
            }
						// 根据timed变量的值决定是时限获取或是阻塞获取任务队列中的任务。
            try {
                Runnable r = timed ?
                    workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                    workQueue.take();
                if (r != null)
                    return r;
                timedOut = true;			//能执行到这儿,说明poll超时了
            } catch (InterruptedException retry) {
              // 在阻塞队列上等待时如果被中断,则清除超时标识重试一次循环。
                timedOut = false;
            }
        }
    }
/**
 * 工作线程从任务队列中拿取任务的核心方法。
 * 根据配置决定采用阻塞或是时限获取。
 * 在以下几种情况会返回null从而接下来线程会退出(runWorker方法循环结束):
 * 1. 当前工作线程数超过了maximumPoolSize(由于maximumPoolSize可以动态调整,这是可能的)。
 * 2. 线程池状态为STOP (因为STOP状态不处理任务队列中的任务了)。
 * 3. 线程池状态为SHUTDOWN,任务队列为空 (因为SHUTDOWN状态仍然需要处理等待中任务)。
 * 4. 根据线程池参数状态以及线程是否空闲超过keepAliveTime决定是否退出当前工作线程。
 */

boolean timed = allowCoreThreadTimeOut || wc > corePoolSize; 是用来决定是否应该结束当前worker;

如果允许核心线程超时(也就是让核心线程在一定idle时间后停止,那么无论wc是多少,都应该尝试销毁每一个超时的worker,因为worker并没有身份,也就没有字段标识是核心线程还是非核心线程;allowCoreThreadTimeOut=true,代表线程池中worker数目可以降到0,而allowCoreThreadTimeOut=false则代表最多降低到coreSize),所以不管当前线程是先启动的还是后启动的(避免误区,不是先启动的就是核心线程,核心线程只是个数字,用来在一定条件下兜底),如果allowCoreThreadTimeOut=true,则都会尝试超时获取,如果一旦超时,则相当于把当前线程当做超时的核心线程,就会尝试停掉;

所以allowCoreThreadTimeOut=true,实际代表任何worker都要有超时检测(因为此时就没有核心不核心的区分了,大家都得超时停止);

或者当前workerCount 已经大于了 corePoolSize;则都应该有条件的触发线程回收(当前worker就尝试停止,来降低线程数回收线程);

所以timed=true,则代表一定要尝试看看能不能停止当前worker,停止的条件就是 通过workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS),来看看 指定时间内能不能获取到task,如果超时了还获取不到,则说明当前worker处于 idle,则再一次循环判断就大概率让其返回一个null,从而退出runWorker()

再次理解,coreSize和maxSize只是一个线程池中核心线程和最大线程的数目,每个Worker并没有身份表示自己是核心的还是非核心的;

所以正常情况下,如果wc > coreSize,则每个worker都应该自己检查自己是不是idle超时了,是的话则停止,用来尽可能减少idle worker数目;

如果allowCoreThreadTimeOut=true,意思是线程池中任何线程如果空闲超过 keepAliveTime 都应该回收(从另一个角度想,这种情况下无论当前线程数是多少,大于还是小于coreSize,无论自己是不是coreThread(因为core也得超时销毁,额外的线程因为超过coreSize了也要超时销毁),每个Worker都得试试看自己是不是idle太久,),那么就是每个线程都应该在getTask的时候自我检查,来及时销毁 idle worker

同样的allowCoreThreadTimeOut=true很有可能在一波任务来临后,造成线程都销毁,下一波任务再来,创建线程性能开销大;

所以allowCoreThreadTimeOut=true的 意图应该是尽量减少线程池内的空闲线程,避免大量idle线程,当然如果每个线程都很忙则不会销毁,如果任务一会多一会少,则线程数会忽上忽下;

任务的提交

submit(Callable c)

submit(FutureTask f)

在这里插入图片描述

  1. submit任务,等待线程池execute
  2. 执行FutureTask类的get方法时,会把主线程封装成WaitNode节点并保存在waiters链表中, 并阻塞等待运行结果;(可能有多个线程在等待 FutureTask的结果,也就是多个线程调用了他的get()方法)
  3. FutureTask任务执行完成后,通过UNSAFE设置waiters相应的waitNode为null,并通过LockSupport类unpark方法唤醒主线程;
public class Test{
    public static void main(String[] args) {

        ExecutorService es = Executors.newCachedThreadPool();
        Future<String> future = es.submit(new Callable<String>() {
            @Override
            public String call() throws Exception {
                try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                return "future result";
            }
        });
        try {
            String result = future.get();
            System.out.println(result);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}


在实际业务场景中,Future和Callable基本是成对出现的,Callable负责产生结果,Future负责获取结果。

  1. Callable接口类似于Runnable,只是Runnable没有返回值。
  2. Callable任务除了返回正常结果之外,如果发生异常,该异常也会被返回,即Future可以拿到异步执行任务各种结果
  3. Future.get方法会导致主线程阻塞,直到Callable任务执行完成;

submit方法

AbstractExecutorService.submit()实现了ExecutorService.submit() 可以获取执行完的返回值, 而ThreadPoolExecutorAbstractExecutorService.submit()的子类,所以submit方法也是ThreadPoolExecutor的方法。

// submit()在ExecutorService中的定义
<T> Future<T> submit(Callable<T> task);

<T> Future<T> submit(Runnable task, T result);

Future<?> submit(Runnable task);
// submit方法在AbstractExecutorService中的实现
public Future<?> submit(Runnable task) {
    if (task == null) throw new NullPointerException();
    // 通过submit方法提交的Callable任务会被封装成了一个FutureTask对象。
    RunnableFuture<Void> ftask = newTaskFor(task, null);
    execute(ftask);
    return ftask;
}

    protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
        return new FutureTask<T>(callable);
    }
    public FutureTask(Callable<V> callable) {
        if (callable == null)
            throw new NullPointerException();
        this.callable = callable;
        this.state = NEW;       // ensure visibility of callable
    }

    protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
        return new FutureTask<T>(runnable, value);
    }
    public FutureTask(Runnable runnable, V result) {
        this.callable = Executors.callable(runnable, result);
        this.state = NEW;       // ensure visibility of callable
    }
		//Executors.callable 将Runnable和一个预设的result封装为一个RunnableAdapter
		//这是因为,普通的Runnable无法有返回值,所以RunnableAdapter 在call()方法内部,将预设的result返回
    public static <T> Callable<T> callable(Runnable task, T result) {
        if (task == null)
            throw new NullPointerException();
        return new RunnableAdapter<T>(task, result);
    }

    static final class RunnableAdapter<T> implements Callable<T> {
        final Runnable task;
        final T result;
        RunnableAdapter(Runnable task, T result) {
            this.task = task;
            this.result = result;
        }
        public T call() {
            task.run();
            return result;
        }
    }


//可以看到,FutureTask内部持有一个callable属性,而Callable.call()方法是有返回值的

通过submit方法提交的Callable任务会被封装成了一个FutureTask对象。通过Executor.execute方法提交FutureTask到线程池中等待被执行,最终执行的是FutureTask的run方法;

线程池中总是调用getTask()得到的Runnable对象的run()方法去执行任务;

所以FutureTask为了既有返回值,又是一个Runnable,它的设计方式是,实现Runnable接口(毋庸置疑),而内部持有callable属性,以及内部有一个outcome属性保存返回值;

其作为Runnable的主要方法run(),则内部其实就是执行callable.call(),然后将返回值设置到outcome上。

FutureTask对象

public class FutureTask implements RunnableFuture 可以将FutureTask提交至线程池中等待被执行(通过FutureTask的run方法来执行)

  • 内部状态
/* The run state of this task, initially NEW. 
    * ...
    * Possible state transitions:
    * NEW -> COMPLETING -> NORMAL
    * NEW -> COMPLETING -> EXCEPTIONAL
    * NEW -> CANCELLED
    * NEW -> INTERRUPTING -> INTERRUPTED
    */
private volatile int state;
private static final int NEW          = 0;
private static final int COMPLETING   = 1;
private static final int NORMAL       = 2;			//正常结束
private static final int EXCEPTIONAL  = 3;
private static final int CANCELLED    = 4;
private static final int INTERRUPTING = 5;
private static final int INTERRUPTED  = 6;

内部状态的修改通过sun.misc.Unsafe修改

  • get方法
public V get() throws InterruptedException, ExecutionException {
    int s = state;
    if (s <= COMPLETING)
        s = awaitDone(false, 0L);
    return report(s);
} 

内部通过awaitDone方法对主线程进行阻塞,具体实现如下:

private int awaitDone(boolean timed, long nanos)
    throws InterruptedException {
    final long deadline = timed ? System.nanoTime() + nanos : 0L;
    WaitNode q = null;
    boolean queued = false;
    for (;;) {
        if (Thread.interrupted()) {
            removeWaiter(q);
            throw new InterruptedException();
        }

        int s = state;
        if (s > COMPLETING) {
            if (q != null)
                q.thread = null;
            return s;
        }
        else if (s == COMPLETING) // cannot time out yet
            Thread.yield();
        else if (q == null)
            q = new WaitNode();
        else if (!queued)
          //这里q.next = waiters 既代表这个expect值是waiters,又把新的waiters赋值给q,同时让q.next -> waiters,所以这是一个栈
            queued = UNSAFE.compareAndSwapObject(this, waitersOffset,q.next = waiters, q);
        else if (timed) {
            nanos = deadline - System.nanoTime();
            if (nanos <= 0L) {
                removeWaiter(q);
                return state;
            }
            LockSupport.parkNanos(this, nanos);
        }
        else
            LockSupport.park(this);
    }
}

  1. 如果主线程被中断,则抛出中断异常;
  2. 判断FutureTask当前的state,如果大于COMPLETING,说明任务已经执行完成,则直接返回;
  3. 如果当前state等于COMPLETING,说明任务已经执行完,这时主线程只需通过yield方法让出cpu资源,等待state变成NORMAL;
  4. 通过WaitNode类封装当前线程,并通过UNSAFE添加到waiters链表(这里通过两次循环才加入链表,第一次只是new 出来Node,第二次如果判断task仍然未结束才 进入链表,其实是一个栈);
  5. 最终通过LockSupport的park或parkNanos挂起线程;

run方法

public void run() {
    if (state != NEW || !UNSAFE.compareAndSwapObject(this, runnerOffset, null, Thread.currentThread()))			//如果状态不是NEW,或者runner不是Null则返回,因为这里要避免并发执行run
        return;
    try {
        Callable<V> c = callable;
        if (c != null && state == NEW) {
            V result;
            boolean ran;
            try {
                result = c.call();
                ran = true;
            } catch (Throwable ex) {
                result = null;
                ran = false;
                setException(ex);
            }
            if (ran)
                set(result);
        }
    } finally {
        // runner must be non-null until state is settled to
        // prevent concurrent calls to run()
        runner = null;  //runner属性在state修改之前必须是 not null的,避免多个线程来并发执行run();这里其实是为了保护上面的if判断   
      //if (state != NEW || !UNSAFE.compareAndSwapObject(this, runnerOffset, null, Thread.currentThread()))	;也就是必须先修改state,从 NEW -> NORMAL ,再把runner=null,如果反过来的话,在并发下很可能前一个线程刚刚执行完,将runner=null赋值了,但是state还没来得及改,也就是state仍然是new,那么第二个线程就可以进入run()方法,最终就是保证这个if判断必须至少有一个条件能够拦住并发线程进入
        // state must be re-read after nulling runner to prevent
        // leaked interrupts 		避免漏掉中断状态
        int s = state;
        if (s >= INTERRUPTING)
            handlePossibleCancellationInterrupt(s);
    }
  
  //cancel()方法可以中断执行FutureTask的线程
      public boolean cancel(boolean mayInterruptIfRunning) {
        if (!(state == NEW &&
              UNSAFE.compareAndSwapInt(this, stateOffset, NEW,
                  mayInterruptIfRunning ? INTERRUPTING : CANCELLED)))
            return false;
        try {    // in case call to interrupt throws exception
            if (mayInterruptIfRunning) {
                try {
                    Thread t = runner;
                    if (t != null)
                        t.interrupt();
                } finally { // final state 最后将task状态改为INTERRUPTED
                    UNSAFE.putOrderedInt(this, stateOffset, INTERRUPTED);
                }
            }
        } finally {
            finishCompletion();
        }
        return true;
    }

      protected void set(V v) {
        if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
            outcome = v;
            UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
            finishCompletion();
        }
    }

FutureTask.run方法是在线程池中被执行的,而非主线程

  1. 通过执行Callable任务的call方法;
  2. 如果call执行成功,则通过set方法保存结果;
  3. 如果call执行有异常,则通过setException保存异常;

set方法和finishCompletion

    //run方法正常时会set 结果到outcome,并修改状态
		protected void set(V v) {
        if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
            outcome = v;
            UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
            finishCompletion();
        }
    }
	//异常时会setException
    protected void setException(Throwable t) {
        if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
            outcome = t;
            UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL); // final state
            finishCompletion();
        }
    }

//以上两个方法,以及cancel(),最后都会调用finishCompletion,代表唤醒线程
//特别的,如果是因为interrupt唤醒,在awaitDone()方法中会判断中断状态并抛出异常,也就是get()-->awaitDone-->异常

可以看到,正常结束时,状态流转为 NEW--->COMPLETING--->NORMAL

UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING);
UNSAFE.putOrderedInt(this, stateOffset, NORMAL);

出异常时:状态流转为 NEW--->COMPLETING--->EXCEPTIONAL

UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)
UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL)

中断时: NEW--->INTERRUPTING--->INTERRUPTED

UNSAFE.compareAndSwapInt(this, stateOffset, NEW,
                  mayInterruptIfRunning ? INTERRUPTING : CANCELLED))
UNSAFE.putOrderedInt(this, stateOffset, INTERRUPTED);

也就是说,state改变是cas操作并且是单向不可逆的,只能从NEW开始,然后是一个中间状态,最后是结束状态

所以线程如果顺利执行完了,则无法中断成功,如果中断了,则即便执行完了,set结果时也会异常

/**
 * Removes and signals all waiting threads, invokes done(), and
 * nulls out callable.
 */
//本质就是唤醒等待的线程,然后将callable = null;  
private void finishCompletion() {
    // assert state > COMPLETING;   预设条件是目前状态已经 > COMPLETIONG
    for (WaitNode q; (q = waiters) != null;) {
        if (UNSAFE.compareAndSwapObject(this, waitersOffset, q, null)) {
            for (;;) {
                Thread t = q.thread;
                if (t != null) {
                    q.thread = null;
                    LockSupport.unpark(t);
                }
                WaitNode next = q.next;
                if (next == null)
                    break;
                q.next = null; // unlink to help gc
                q = next;
            }
            break;
        }
    }

    done();			//留给子类实现的一个方法,在完成task的时候有一个回调?

    callable = null;        // to reduce footprint
}

任务的关闭

shutdown方法会将线程池的状态设置为SHUTDOWN,线程池进入这个状态后,就拒绝再接受任务,然后会将剩余的任务全部执行完

public void shutdown() {
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        //检查是否可以关闭线程
        checkShutdownAccess();
        //设置线程池状态
        advanceRunState(SHUTDOWN);
        //尝试中断worker
        interruptIdleWorkers();
            //预留方法,留给子类实现
        onShutdown(); // hook for ScheduledThreadPoolExecutor
    } finally {
        mainLock.unlock();
    }
    tryTerminate();
}

private void interruptIdleWorkers() {
    interruptIdleWorkers(false);
}

//onlyOne 代表只中断一个
private void interruptIdleWorkers(boolean onlyOne) {
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        //遍历所有的worker
        for (Worker w : workers) {
            Thread t = w.thread;
            //先尝试调用w.tryLock(),如果获取到锁,就说明worker是空闲的,就可以直接中断它
          //对应runWorker中的while循环代码,如果这里能获取到worker内的lock,则说明worker的thread在等待task
            //注意的是,worker自己本身实现了AQS同步框架,然后实现的类似锁的功能
            //它实现的锁是不可重入的,所以如果worker在执行任务的时候,会先进行加锁,这里tryLock()就会返回false
          //如果没中断,并且通过tryLock判断worker空闲,则打断
            if (!t.isInterrupted() && w.tryLock()) {
                try {
                    t.interrupt();
                } catch (SecurityException ignore) {
                } finally {
                    w.unlock();
                }
            }
            if (onlyOne)
                break;
        }
    } finally {
        mainLock.unlock();
    }
}

//SHUTDOWN了之后,submit和execute方法都是无法提交任务了

shutdownNow做的比较绝,它先将线程池状态设置为STOP,然后拒绝所有提交的任务。最后中断左右正在运行中的worker,然后清空任务队列。

public List<Runnable> shutdownNow() {
    List<Runnable> tasks;
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        checkShutdownAccess();
        //检测权限
        advanceRunState(STOP);
        //中断所有的worker
        interruptWorkers();
        //清空任务队列
        tasks = drainQueue();
    } finally {
        mainLock.unlock();
    }
    tryTerminate();
    return tasks;
}

    private List<Runnable> drainQueue() {
        BlockingQueue<Runnable> q = workQueue;
        ArrayList<Runnable> taskList = new ArrayList<Runnable>();
        q.drainTo(taskList);
        if (!q.isEmpty()) {
            for (Runnable r : q.toArray(new Runnable[0])) {
                if (q.remove(r))
                    taskList.add(r);
            }
        }
        return taskList;
    }

private void interruptWorkers() {
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        //遍历所有worker,然后调用中断方法
        for (Worker w : workers)
            w.interruptIfStarted();			//对于那些已经正在执行的FutureTask,如果不响应中断,其实也没辙
    } finally {
        mainLock.unlock();
    }
}

更深入理解

为什么线程池不允许使用Executors去创建? 推荐方式是什么?

阿里的java编码规范中也有提到

线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。 说明:Executors各个方法的弊端:

  • newFixedThreadPool和newSingleThreadExecutor:  主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至OOM。
  • newCachedThreadPool和newScheduledThreadPool:  主要问题是线程数最大数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至OOM。

推荐方式 1

首先引入:commons-lang3包

ScheduledExecutorService executorService = new ScheduledThreadPoolExecutor(1,
        new BasicThreadFactory.Builder().namingPattern("example-schedule-pool-%d").daemon(true).build());

推荐方式 2

首先引入:com.google.guava包

    ThreadFactory namedThreadFactory = new ThreadFactoryBuilder()
        .setNameFormat("demo-pool-%d").build();			//优雅的创建ThreadFactory,用来给线程定义名字
 
    //Common Thread Pool
    ExecutorService pool = new ThreadPoolExecutor(5, 200,
        0L, TimeUnit.MILLISECONDS,
        new LinkedBlockingQueue<Runnable>(1024), namedThreadFactory, new ThreadPoolExecutor.AbortPolicy());
 
    pool.execute(()-> System.out.println(Thread.currentThread().getName()));
    pool.shutdown();//gracefully shutdown
```
           
#### 推荐方式 3
spring配置线程池方式:自定义线程工厂bean需要实现ThreadFactory,可参考该接口的其它默认实现类,使用方式直接注入bean
调用execute(Runnable task)方法即可

```xml
<bean id="userThreadPool"
        class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
        <property name="corePoolSize" value="10" />
        <property name="maxPoolSize" value="100" />
        <property name="queueCapacity" value="2000" />

    <property name="threadFactory" value= threadFactory />
        <property name="rejectedExecutionHandler">
            <ref local="rejectedExecutionHandler" />
        </property>
    </bean>
    //in code
    userThreadPool.execute(thread);

  @pdai: 代码已经复制到剪贴板

配置线程池需要考虑因素

从任务的优先级,任务的执行时间长短,任务的性质(CPU密集/ IO密集),任务的依赖关系这四个角度来分析。并且近可能地使用有界的工作队列。

性质不同的任务可用使用不同规模的线程池分开处理:

  • CPU密集型: 尽可能少的线程,Ncpu+1(线程少,减少CPU切换?)
  • IO密集型: 尽可能多的线程, Ncpu*2,比如数据库连接池(线程稍多,增大吞吐量)
  • 混合型: CPU密集型的任务与IO密集型任务的执行时间差别较小,拆分为两个线程池;否则没有必要拆分。

监控线程池的状态

可以使用ThreadPoolExecutor以下方法:

  • getTaskCount() Returns the approximate total number of tasks that have ever been scheduled for execution.
  • getCompletedTaskCount() Returns the approximate total number of tasks that have completed execution. 返回结果少于getTaskCount()。
  • getLargestPoolSize() Returns the largest number of threads that have ever simultaneously been in the pool. 返回结果小于等于maximumPoolSize
  • getPoolSize() Returns the current number of threads in the pool.
  • getActiveCount() Returns the approximate number of threads that are actively executing tasks.
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值