史上最全ThreadPoolExecutor源码阅读笔记

思维导图

开始之前先贴下关于ThreadPoolExecutor的思维导图,下图是根据本文整理出来的,有问题的地方均希望指出,你我共进步。在这里插入图片描述

ThreadPoolExecutor继承关系:

在这里插入图片描述

Executor:

一个用来执行提交的Runable任务的对象。将任务的提交和执行、线程调度等细节解耦。只有一个execute方法。

ExecutorService:

一个提供方法去管理终止或可以生成一个Future来追踪一个或多个异步任务的Executor。

shutdown

一个serviceshutdown后将拒绝新的任务。有两个方法提供来shutdown:

  1. shutdown:
    允许先前提交的任务在shutdown之前执行完。
  2. shutdownNow:
    不允许等待的任务开始执行,并尝试去stop正在执行的任务。

一个没有在使用也不会使用的service应该shutdown以便资源回收。

AbstractExecutorService

提供ExecutorService的一些默认实现,

ThreadPoolExecutor的自述

在简单了解了ThreadPoolExecutor的父类后,我们来看看这个类本身:

以下内容来自jdk8的源码注解。

ThreadPoolExecutor是一个使用线程池执行每个提交的任务的ExecutorService,通常通过工厂方法进行配置。

  1. newCachedThreadPool:带自动回收机制的无边际线程池
  2. newFixedThreadPool:固定大小的线程池
  3. newSingleThreadExecutor:单个后台线程
  4. ……

所以以后面试官问到线程池的创建方式有几种的时候应该说两种,用new的方式和工厂方法的方式。官方推荐工厂方法方式,然后再讲讲目前jdk支持多少个工厂。

核心线程数和最大线程数

核心线程数最大线程数是线程池的核心配置,一个ThreadPoolExecutor会通过这个核心线程数和最大线程数来动态的调整线程数大小。
当一个新的任务通过execute(Runnable)提交上来,并且少于核心线程数的线程在运行,即时其他线程时闲置的,也会创建一个新的线程去处理这个请求。当当前在运行的线程数大于核心线程小于最大线程数的时候,也会创建新的线程来处理。

  • 设置核心线程数等于最大线程数即可实现一个固定大小的线程池(FixedThreadPool)
  • 设置最大线程数为一个本质上是无边界的值(比如Integer.MAX_VALUE)可以实现一个无边界的线程池(newCachedThreadPool)

核心线程的初始化动作

默认的,核心线程会在第一个任务到达的时候初始化,但是我们可以通过复写prestartCoreThreadprestartAllCoreThreads方法来改变核心线程的初始化行为。

线程的创建

一个新的线程会通过ThreadFactory来创建,如果没有特别指定,会使用defaultThreadFactory,创建的线程会放到相同的ThreadGroup,相同的NORM_PRIORITY优先级,并全是非守护进程状态。我们可以通过自定义ThreadFactory来指定线程的分组,名称,优先级,守护进程等信息。当ThreadFactory创建线程失败时,执行器将继续执行,只是不能执行任何任务。线程应该拥有RuntimePermission("modifyThread")权限.如果工作的线程或其他使用这个线程池的线程没有这个权限,服务可能会降级:配置改变可能不会即时生效,一个shutdown的线程可能保持一种可以关闭不能完成的状态。

保活时间

线程池中的线程数超过核心线程数时,线程闲置时间超过keepAliveTime则会被销毁。这提供了一个手段在线程不活跃的时候减少资源消耗。通过设置keepAliveTimeLong.MAX_VALUE TimeUnit#NANOSECONDS可以有效地阻止空闲线程在shutdown之前被收回。一般情况下这个keepAliveTime方案只会在线程数超过核心线程数时被使用,但是可以通过allowCoreThreadTimeOut(boolean)来让其在没有超过核心线程数时生效。只要keepAliveTime的值非零。

线程队列

一个BlockingQueue被用于调度和持有被提交的任务,如果当前运行的线程数小于核心线程数,执行器通常直接创建新的线程,而不会讲任务放到队列中。如果当前运行的线程数大于等于核心线程数时,执行器通常将新任务加到队列而不是直接创建线程。如果请求无法加入队列,执行器在线程数没超过最大线程数的情况下创建新的线程,否则会拒绝这个任务。

队列的排队策略有三种:

  1. 直接握手队列:工作队列的一个很好的默认选择是 SynchronousQueue,它将任务交给线程而不用其他方式保留它们。这里如果没有一个线程可以立即执行。则尝试将一个任务加入队列会失败,所以会创建一个新的线程。处理可能具有内部依赖性的请求集时,此策略可避免锁定。 直接握手队列一般需 要无限的maximumPoolSizes来避免拒绝新提交的任务。因此有不停的任务到达,并且到达速度超过了线程的平均处理速度,线程数量会无限增长。
  2. 无界队列:使用一个无界队列(比如LinkedBlockingQueue不设定容量),将导致在核心线程在忙的情况下新的任务都会到队列中等待。因此不会创建超过核心线程数的线程。因此maximumPoolSize将失效。这个策略适合所有任务都完全不依赖别的任务的,任务彼此不影响运行情况。比如在网页服务器中。这种策略可以平滑处理瞬时爆发的请求,但是当任务到达速度超过任务的平均处理速度时,任务队列会无限增长。
  3. 有界队列:用一个有界队列(比如ArrayBlockingQueue)和有界的maximumPoolSizes来防止资源耗尽,但是会更难调度和控制。队列大小和最大池大小可以相互调整:使用大队列和小池可以最大限度地减少 CPU 使用率、操作系统资源和上下文切换开销,但会导致人为地降低吞吐量。如果任务频繁阻塞(例如,如果它们受 I/O 限制),系统可能能为更多的线程分配调度时间,超过你允许的方位。使用一个小的队列,往往需要设置一个大的线程池,这会使得CPU更忙,但可能会遇到不可接受的调度开销,这也会降低吞吐量。【换句话说就是有界队列需要合理设置队列大小和线程池大小,不然吞吐量会比较低。】

拒绝任务

在执行器shutdown后提交的任务会被拒绝。还有就是当线程池到达最大线程数,并且队列也满了的情况下提交的任务也会被拒绝。在任何情况下,execute方法执行调用RejectedExecutionHandler#rejectedExecution(Runnable, ThreadPoolExecutor)方法,提供了4种预定义的处理策略:

  1. ThreadPoolExecutor.AbortPolicy:默认策略,在拒绝时抛出运行时异常RejectedExecutionException
  2. ThreadPoolExecutor.CallerRunsPolicy:调用线程自身的execute方法来执行这个任务。这种策略提供了一种简单的反馈控制机制,可以减缓提交新任务的速度。
  3. ThreadPoolExecutor.DiscardPolicy:无法执行的任务被简单地丢弃。
  4. ThreadPoolExecutor.DiscardOldestPolicy:如果执行器没有关闭,那么在队列头的任务将被丢弃,然后再次执行(可能会再次失败,导致重复)。

也可以定义并使用其他的RejectedExecutionHandler类,这样做需要小心,特别是当策略设计为仅在特定容量或排队策略下工作时。

钩子函数

ThreadPoolExecutor提供了可复写的protected方法beforeExecuteafterExecute,这两个方法可以在每个任务在执行前后被调用。这可以用于操作执行环境,比如:重新初始化线程局部变量,收集统计收集,或者添加日志。此外,terminated方法可以被复写来完成任务执行完成后需要做的任何特殊处理。如果钩子函数或回调函数失败,内部工作线程可能会依次失败,并突然终止。

队列维护

getQueue方法允许访问工作队列,以便监控和调试队列。强烈建议不要将此方法用于任何其他目的。提供的两个方法removepurge可用于在大量排队任务被取消时协助存储回收。

线程池的回收

程序中不再引用和没有剩余线程的线程池将会自动shutdown。如果你想确保没有被引用的线程池在用户忘记调用shutdown时被回收,你必须通过设置合适的keepAliveTime,设置一个小于0的核心线程数或设置allowCoreThreadTimeOut来保证不被使用的线程最终会死掉。

部分静态变量和成员变量解读

ThreadPoolExecutor里面有定义一个变量ctl和几个常量,下面对其意义解读:

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static final int COUNT_BITS = Integer.SIZE - 3;
private staticfinal int CAPACITY   = (1 << COUNT_BITS) - 1;

// runState is stored in the high-order bits
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;

ctl

线程池状态的主要控制,是一个由两个概念字段打包在一起的AtomicInteger:

  • workerCount:有效线程数;
  • runState:标识线程池的运行状态;

为了将这两个字段打包到一个int上,ThreadPoolExecutor限制workerCount为(2^29)-1 (大概5亿) 个线程,而不是 (2^31)-1(大概20亿) 个,如果将来数量不够了,就将会变成AtomicLong;【但是double在的操作不是原子性的,所以目前而言,int更快更便捷】

这里补个基础知识:java中int占4个字节,一个字节占8位,也就是一个int32位,ctl用低29位来标识线程数,可以标识的数量就是(2^29)-1 ,剩下的高三位来标识线程池的运行状态。
ctl = 运行状态 | 工作线程数。

workerCount可能会出现短暂的和实际存活线程数不相等的情况,比如ThreadFactory在调用时创建线程失败,以及正在退出的线程在终止之前仍然在记录上。用户可见的线程池大小是当前的workers集合的大小。

运行状态

线程池在生命周期的运行状态控制由,线程池有下面五种状态:

  • RUNNING(正在运行): 接收新的任务,并处理队列中的任务。
  • SHUTDOWN(关闭):不在接收新的任务,但是会处理队列中的任务。
  • STOP(停止):不在接收新的任务,也不会处理队列中的任务,并且打断正在执行的任务。
  • TIDYING(整理):所有的任务被终止,workerCount为0.转到TIDYING状态的线程将执行terminated钩子函数。
  • TERMINATED(终止):terminated方法执行完成。

这些状态的数值顺序是很重要的,允许顺序比较,运行状态是单调递增的,过渡方式是:

  • RUNNING -> SHUTDOWN:在调用 shutdown() 时,可能隐含在 finalize() 中。
  • (RUNNING or SHUTDOWN) -> STOP:调用 shutdown() ;
  • SHUTDOWN -> TIDYING:当队列和线程池都为空时;
  • STOP -> TIDYING:当线程池为空时;
  • TIDYING -> TERMINATED:当 terminate() 钩子方法完成时;

当状态达到 TERMINATED 时,在 awaitTermination() 中等待的线程将返回。

BlockingQueue

用于保存任务和移交给工作线程的队列,ThreadPoolExecutor不是用workQueue.isEmpty()来判断队列为空,而是调用isEmpty来判断(例如,在决定是否从 SHUTDOWN 转换到 TIDYING 时,我们必须这样做)。这适用于特殊用途的队列,例如 DelayQueues,允许 poll() 返回 null,即使它稍后可能在延迟到期时返回非 null。

ReentrantLock

锁定对相关工作集和关系簿的访问。虽然可以使用并发集合,但是事实证明使用锁更好。其中的一个原因是它会序列化interruptIdleWorkers,会避免不必要的中断风暴。尤其在shutdown期间。否则退出线程将同时中断那些尚未中断的线程。它还简化了最大池大小等的一些相关统计簿记。我们还在shutdown和shutdownNow上持有mainLock,为了确保workers set是稳定的,同时分别检查中断和实际中断的权限。

workers

包含池中所有工作线程的集合。仅在持有 mainLock 时访问。

Worker是ThreadPoolExecutor的一个内部类。Worker 类主要维护线程运行任务的中断控制状态,以及其他次要的簿记。 此类机会性地扩展 AbstractQueuedSynchronizer 以简化获取和释放围绕每个任务执行的锁。 这可以防止旨在唤醒等待任务的工作线程的中断,而不是中断正在运行的任务。 我们实现了一个简单的不可重入互斥锁,而不是使用 ReentrantLock,因为我们不希望工作任务在调用诸如 setCorePoolSize 之类的池控制方法时能够重新获取锁。 此外,为了在线程实际开始运行任务之前抑制中断,我们将锁定状态初始化为负值,并在启动时(在 runWorker 中)清除它。

ThreadFactory

新线程的工厂。 所有线程都是使用这个工厂创建的(通过方法 addWorker)。 所有调用者都必须为 addWorker 失败做好准备,这可能反映了系统或用户限制线程数的策略。 即使它不被视为错误,创建线程失败也可能导致新任务被拒绝或现有任务卡在队列中。 我们更进一步,即使在尝试创建线程时可能会抛出 OutOfMemoryError 等错误时,也保留池不变量。 由于需要在 Thread.start 中分配本机堆栈,因此此类错误相当常见,并且用户将希望执行干净池关闭以进行清理。 可能有足够的内存可供清理代码完成而不会遇到另一个 OutOfMemoryError。

RejectedExecutionHandler

在任务被拒绝时做的操作。默认是AbortPolicy。

核心方法源码阅读

这里对三个核心方法进行记录:

getTask

private Runnable getTask() {
        boolean timedOut = false; // 定义一个boolean值记录上次poll操作是否超时。

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

            // 如果当前的状态是非RUNNING状态,或者是STOP及以上状态,并且work队列是空的,那就对WordkerCount进行递减,并且返回null。
            if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
                decrementWorkerCount();
                return null;
            }
			//从c中取出当前的工作线程数
            int wc = workerCountOf(c);

            //标记是否对工作线程对象进行补刀,实质是看当前线程数是否超过核心线程数,因为allowCoreThreadTimeOut 为true时,可以认为核心线程数为0;
            boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
			//这里逻辑比较复杂,最好先看看timeOut的变换逻辑
			//如果当前线程数大于最大线程数或者time和timedOut都为true,并且当前线程数大于1或工作队列为空,则cas自旋将count-1.
			//这里当前线程数大于最大线程数时不会执行新任务,并且会将workercount-1
            if ((wc > maximumPoolSize || (timed && timedOut))
                && (wc > 1 || workQueue.isEmpty())) {
                if (compareAndDecrementWorkerCount(c))
                    return null;
                continue;
            }
			/**当前线程数如果超过核心线程数(或者allowCoreThreadTimeOut为true)
			,则通过poll从阻塞队列中获取任务,否则通过take从阻塞队列中获取任务。poll和take的区别在于poll超时返回null,而take会一直等待返回。
			如果获取过程中遇到异常则会将timeOut重置为false;
			*/
            try {
                Runnable r = timed ?
                    workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                    workQueue.take();
                if (r != null)
                    return r;
                timedOut = true;
            } catch (InterruptedException retry) {
                timedOut = false;
            }
        }
    }

execute

execute用来提交任务,然后在未来的某一个时间执行任务。

 public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        int c = ctl.get();
        //如果当前的工作线程数小于核心线程,尝试开启新的线程来执行任务,失败则重新获取当前的ctl(因为ctl可能已经被修改了),成功则直接返回。
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        /**如果当前线程池在运行状态,并且任务加入工作等待队列成功则再次获取ctl,对线程池的状态再次确认
        ,判断如果线程池不在运行状态了就移除当前任务进行回滚补刀,并由拒绝handler进行拒绝处理。如果当前的有效工作线程数为0,那么向工作集中加入ull(这个地方没太看懂)
        如果线程池不在运行状态或者加入队列失败,那么尝试直接新建线程来运行该任务,如果新建失败则拒绝。
        */
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        else if (!addWorker(command, false))
            reject(command);
    }

addWorker

根据线程状态和核心线程数及最大线程数判断是否可以创建新的线程来执行任务。

private boolean addWorker(Runnable firstTask, boolean core) {
        retry:
        for (;;) {
            int c = ctl.get();
            int rs = runStateOf(c);

            // Check if queue empty only if necessary.
            //如果当前状态不是运行状态,并且即时处于shutdown状态也没有需要执行的任务,那么直接返回fasle。
            if (rs >= SHUTDOWN &&
                ! (rs == SHUTDOWN &&
                   firstTask == null &&
                   ! workQueue.isEmpty()))
                return false;
			/**判断当前有效线程数是否大于ThreadPoolExecutor允许的最大线程数(当然这个目前而言肯定是false)
			,所以简化就是判断有没有超过核心线程数或最大线程数(根据当前要创建的线程来判断),如果超过了就返回false;
			如果满足线程创建状态,对workerCount进行自旋+1(先线程安全的对workerCount进行+1后再去实际创建线程,避免实际线程数超过允许的最大线程数)
			,成功则然后跳出双重循环
			,失败则再次获取ctl的值对现在的状况进行重新判断,
			如果当前的运行状态有改变,则重新开始外部循环对状态进行判断。
			否则就是当前的线程数有改变,继续循环内部循环。
			*/
            for (;;) {
                int wc = workerCountOf(c);
                if (wc >= CAPACITY ||
                    wc >= (core ? corePoolSize : maximumPoolSize))
                    return false;
                if (compareAndIncrementWorkerCount(c))
                    break retry;
                c = ctl.get();  // Re-read ctl
                if (runStateOf(c) != rs)
                    continue retry;
                // else CAS failed due to workerCount change; retry inner loop
            }
        }
		//经过上面的双重循环,已经预先给workerCount+1了,保证了workerCount不会超标,现在开始作手创建线程。
        boolean workerStarted = false;
        boolean workerAdded = false;
        Worker w = null;
        try {
            w = new Worker(firstTask);
            final Thread t = w.thread;
            if (t != null) {
            /**获取mainLock锁来操作workers集合
            ,首先判断当前的运行状态
            ,如果当前运行状态是运行状态,或者是shutdown状态但当前任务是null,则判断刚才创建的线程有没有启动,如果启动了就抛出IllegalThreadStateException异常。
            反之将创建的work加载workers集合。更新最后达到的最大线程数。最后启动刚才创建的线程。
            如果创建失败,则将尝试终止线程池。
            不管成功创建与否,最后都要释放mainLock锁。
            */
                final ReentrantLock mainLock = this.mainLock;
                mainLock.lock();
                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;
                        workerAdded = true;
                    }
                } finally {
                    mainLock.unlock();
                }
                if (workerAdded) {
                    t.start();
                    workerStarted = true;
                }
            }
        } finally {
            if (! workerStarted)
                addWorkerFailed(w);
        }
        return workerStarted;
    }

线程池的创建方式

new方式创建

//使用默认的线程工厂,和默认的任务拒绝异常处理器。
public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue);
//自定义拒绝异常处理器
public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              RejectedExecutionHandler handler) ;
//自定义线程工厂和拒绝异常处理器
public ThreadPoolExecutor(int corePoolSize,
                            int maximumPoolSize,
                            long keepAliveTime,
                            TimeUnit unit,
                            BlockingQueue<Runnable> workQueue,
                            ThreadFactory threadFactory,
                            RejectedExecutionHandler handler) 

工厂方法创建

工厂方法创建线程池涉及到另一个类Executors,这个类是Executor、ExecutorService、ScheduledExecutorService、ThreadFactory、Callable这几个类的工厂和工具类。目前只看与线程池创建相关的几个方法,Executors提供了创建以下几种线程池的工厂方法:

  • FixedThreadPool:定长线程池,核心线程数=最大线程数,保活时长为0.池中线程一直存活,直到调用shutdown或shutdownNow。
  • WorkStealingPool:创建一个可以工作窃取的线程池。
  • SingleThreadExecutor:一个线程,无界队列。
  • CachedThreadPool:0个核心线程,无界线程数,队列策略采用直接握手队列,不等待,直接创建线程。
  • 还有几个和定时任务相关的线程池:ScheduledExecutorService

工作窃取线程池里面有多个线程及多个队列,每个线程分别处理一个队列,当有线程完成自己队列里面的任务时就可以去别的队列里面获取任务来做,以提高效率。

常见面试题解答

1.什么是线程池,为什么需要线程池

线程池管理了一堆线程,这些线程的生命周期由线程池来管理,当有任务提交到线程池时,可以使用现有的线程来执行任务或创建新的线程来执行任务。在高并发的情况下,使用线程池可以可以减少线程创建销毁等开销,并且可以达到并发线程的削峰作用,当任务超过线程池的处理能力时,过多的任务可以放到队列中等到现有任务执行完成后再执行。

2.线程池的创建方式有几种

线程池的创建方式准确来说应该是两种,工厂方法创建和new的方式创建。工厂方法提供了好几种类型的线程池的工厂创建方法,具体参考本文的线程池创建方式。

3.为什么不建议使用 Executors静态工厂构建线程池

不建议使用Executors静态工厂构建线程池只是阿里提出来的,目的是为了让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。但是ThreadPoolExecutor本身却强烈推荐程序员们使用静态工厂的方式来构建线程池。

4. 线程池有哪几种工作队列

  • ArrayBlockingQueue (有界队列):是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
  • LinkedBlockingQueue (无界队列):一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。
  • SynchronousQueue(同步队列): 一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。
  • DelayQueue(延迟队列):一个任务定时周期的延迟执行的队列。根据指定的执行时间从小到大排序,否则根据插入到队列的先后排序。
  • PriorityBlockingQueue(优先级队列): 一个具有优先级得无限阻塞队列。

5. 怎么理解无界队列和有界队列

  • 有界队列即长度有限,满了以后ArrayBlockingQueue会插入阻塞。
  • 无界队列就是里面能放无数的东西而不会因为队列长度限制被阻塞,但是可能会出现OOM异常。

6.线程池中的几种核心参数

  • corePoolSize:核心线程池大小。
  • maximumPoolSize: 线程池最大线程数。
  • keepAliveTime:线程闲置时的保活时常。
  • ctl:线程池的运行状态和有效线程数打包组合成的一个AtomicInteger。高3位标识线程池状态,低29位标识有效线程数。

相关知识

ThreadPoolTaskExecutor(Spring中对ThreadPoolExecutor的包装)、BlockingQueue(阻塞队列)、DelayQueues(延时队列)、ReentrantLock(重入锁),ScheduledExecutorService(支持定时或周期执行任务的定长线程池)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值