Java并发编程的艺术学习笔记

第一章 并发编程的挑战

并发编程的目的:为了让程序运行的更快。
但并不是启动的线程越多就能让程序最大限度地并发执行。

CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务。但是,在切换前会保存上一个任务的状态,以便下一次切换回这个任务时,可以再加载这个任务的状态。所以任务从保存到再加载的过程就是一次上下文切换

  • 如何减少上下文切换?
    1.无锁并发编程
    2.CAS算法
    3.使用最少线程
    4.使用协程

  • 避免死锁的常见方法?
    1.避免一个线程同时获取多个锁
    2.避免一个线程在所内同时占用多个资源,尽量保证每个锁只占用一个资源
    3.尝试使用定时锁,使用lock.tryLock(timeout)来代替使用内部锁机制
    4.对于数据库锁,加强和解锁必须在一个数据库连接里,否则会出现解锁失败的情况

  • 资源限制引发的并发编程问题?
    将某段串行的代码并发执行,因为受限于资源,任然在串行执行,会增加上下文切换和资源调度的时间。

  • 解决资源限制的问题?
    1.对于硬件资源限制,可以考虑使用集群并行执行程序。
    2.对于软件资源限制,可以考虑使用资源池将资源复用。

第二章 并发机制的底层实现原理

Java代码在编译后会变成Java字节码,字节码被类加载器加载到JVM里,JVM执行字节码,最终需要转化为汇编指令在CPU上执行,Java中所使用的并发机制依赖于JVM的实现和CPU的指令。

2.1 volatile的应用

1.volatile的定义与实现原理

  • 定义:Java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致地更新,线程应该确保通过排他锁单独获得这个变量。

  • volition是如何实现可见性的?
    汇编指令下可以查看到是通过Lock前缀的指令来修饰实现的,它会在多核处理器下发生两件事。
    1.将当前处理器缓存行的数据写回到系统内存(进行“缓存锁定”)
    2.这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效(嗅探)

  • 实现原理
    为了提高处理速度,处理器不直接和内存进行通信,而是先将系统内存的数据读到内部缓存后再进行操作,但操作完不知道何时会写到内存。如果对声明了volatile的变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。为了保证各个处理器的缓存时一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。

2. volatile的优化?(适用于Java 7之前)

1.为什么追加64字节能够提高并发编程的效率?
对于部分处理器,不支持部分填充缓存行,意味着,如果队列的头节点和尾节点都不足64字节的话,处理器会将他们都读取到同一个高速缓存行中,在多处理器下每个处理器都会缓存同样的头,尾节点,当一个处理器试图修改头节点时,会将整个缓存行锁定,那么在缓存一致性机制的作用下,会导致其他处理器不能访问自己高速缓存中的尾节点,而队列的入队和出队操作则需要不停修改头节点和尾节点,所以在多处理器的情况下将会严重影响到队列的入队和出队效率。
追加到64字节的方式来填满高速缓存区的缓存行,避免头节点和为节点加载到同一个缓存行,使头尾节点在修改时不会互相锁定。
2.以下两个场景不适应
1)缓存行非64字节宽的处理器
2)共享变量不会被频繁地写

2.2 synchronized的实现原理与应用

Java中的每一个对象都可以作为锁。具体变现为2个形式:

  • 对于普通同步方法,锁是当前实例对象
  • 对于静态同步方法,锁是当前类的Class对象
  • 对于同步方法块,锁是Synchronized括号里配置的对象。

2.2.1 Java对象头

synchronized用的锁是存在Java对象头里的。

2.2.2 锁升级与对比

1.偏向锁
产生原因:大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。
实现原理:当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储所偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁。只需要简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示已经获得锁。如果失败,需要再测试一下Mark Word中偏向锁的表示是否设置成1(表示当前是偏向锁):如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。
偏向锁撤销:等到竞争出现才会释放锁的机制。需要等到全局安全点。
2.轻量级锁:
加锁: 线程在执行同步块之前,JVM会先在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到所记录中,官方称为Displaced Mark Word.然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。
解锁: 会使用原子的CAS操作将Displaced Mark Word替换回到对象头。如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成中重量级锁。
在这里插入图片描述

2.3 原子操作的实现

处理器如何实现原子操作?
1.通过总线锁保证原子性
2.通过缓存锁定确保原子性
Java如何实现原子操作?
1.锁机制:保证了只有获得锁的线程才能够操作锁定的内存区域
2.循环CAS
CAS实现原子操作的三大问题
1.ABA问题
2.循环时间长开销大
3.只能保证一个共享变量的原子操作。

第三章 Java内存模型(未写)

第四章 Java并发编程基础

4.1 线程简介

4.1.1 什么是线程

现代操作系统在运行一个程序时,会为其创建一个进程。
现代操作系统调度的最小单元是线程,也叫轻量级进程,在一个进程中可以创建多个线程,这些线程都拥有各自的计数器,堆栈和局部变量等属性,并且能够访问共享的内存变量。

4.1.2 为什么要使用多线程

1)更多的处理器核心
2)更快的响应时间
3)更好的编程模型

4.1.3 线程优先级

现代操作系统基本采用时分的形式调度运行的线程,操作系统会分出一个个时间片,线程会分配到若干时间片,当线程的时间片用完了就会发生线程调度,并等待着下一次分配。线程优先级就是决定线程需要多或者少分配一些处理器资源的线程属性。
setPriority(int)方法来修改优先级,默认为5.

4.1.4 线程的状态

在这里插入图片描述

在这里插入图片描述

4.1.5 Daemon线程

Daemon线程是一种支持型线程,主要作用被用于程序中后台调度以及支持型工作。
设置Thread.setDaemon(true)
在构建Daemon线程时,不能依靠finally块中的内容来确保执行关闭或清理资源的逻辑。

4.2.1 等待/通知机制

在这里插入图片描述WaitThread首先获取了对象的锁,然后调用对象的wait()方法,从而放弃了锁并进入了对象的等待队列WaitQueue中,进入等待状态。由于WaitThread释放了对象的锁,NotifyThread随后获取了对象的锁,并调用对象的notify()方法,将WaitThread从WaitQueue移到SynchronizedQueue中,此时WaitThread的状态变为阻塞状态。NotifyThread释放了锁之后,WaitThread再次获取到锁并从wait()方法返回继续执行。

4.2.2 管道输入/输出流

管道输入/输出流和普通的文件输入/输出流或者网络输入/输出流不同之处在于,主要用于线程之间的数据传输,而传输的媒介为内存。
具体实现:PipedOutStream,PipedInputStream,PipedReader和PipedWriter,前面两面向字节,后面两面向字符。

4.2.3 ThreadLocal的使用

ThreadLocal,即线程变量,是一个以ThreadLocal对象为键,任意对象为值得存储结构。这个结构被附带在线程上,也就是说已给线程可以根据一个ThreadLocal对象查询到绑定在这个线程上的一个值。
通过set(T)方法来设置一个值,在当前线程下再通过get()方法获取原先设置的值。

第五章 Java中的锁

5.1 Lock接口

Lock锁相比于synchronized,不具备自动获取释放锁,需要显式得调用。但却具备可操作性,可中断的获取锁,超时获取锁等。

Lock lock=new ReentrantLock();
lock.lock();
try{
}finally{
	lock.unlock();
}

在这里插入图片描述

5.2 队列同步器

队列同步器AbstractQueuedSynchronizer,是用来构建锁或者其他同步组件的基础框架,它使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。
同步器的主要作用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态,在抽象方法的实现过程中免不了要对同步状态进行更改,这时就需要使用同步器提供的3个方法(getState(),setState(int newState)和compareAndSetState(int expect,int update))来进行操作,因为它们能够保证状态的改变是安全的。子类推荐被定义为自定义同步组件的静态内部类,同步器自身没有实现任何同步接口,它仅仅是定义了若干同步状态获取和释放的方法来供自定义同步组件使用,同步器既可以支持独占式地获取同步状态,也可以支持共享地获取同步状态,这样就可以方便实现不同类型的同步组件。
同步器是实现锁的关键,在锁的实现中聚合同步器,利用同步器实现锁的语义。二者关系:锁是面向使用者的,它定义了使用者与锁交互的接口,隐藏了实现细节;同步器面向的是锁的实现者,它简化了锁的实现方式,屏蔽了同步状态管理,线程的排队,等待与唤醒等底层操作。

5.2.1队列同步器的接口与示例

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
示例如下:
在这里插入图片描述

5.2.2队列同步器的实现分析

1.同步队列
同步器依赖内部的同步队列来完成同步状态的管理,当前线程获取同步状态失败时,同步器会将当前线程以及等待状态等信息构造成为一个节点并将其加入同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点中的线程唤醒,使其再次尝试获取同步状态。

同步队列中的节点用来保存获取同步状态失败的线程引用,等待状态以及前驱和后继节点,节点的属性类型以及描述。

在这里插入图片描述
在这里插入图片描述在这里插入图片描述2.独占式同步状态获取与释放
在获取同步状态时,同步器维护一个同步队列,获取状态失败的线程都会被加入到队列中并在队列中进行自旋;移出队列(停止自旋)的条件是前驱节点为头节点且成功获取了同步状态。在释放同步状态时,同步器调用tryRelease(int arg)方法释放同步状态,然后唤醒头节点的后继节点。

在这里插入图片描述在这里插入图片描述

3.共享式同步状态获取与释放
共享式获取与独占式获取最主要的区别在于同一时刻能否有多个线程同时获取到同步状态。
acquireShared(int arg)方法可以共享式地获取同步状态。
releaseShare(int arg)方法可以释放同步状态
对于能够支持多个线程同时访问的并发组件,它和独占式主要区别在于tryReleaseShared(int arg)方法必须确保同步状态(或者资源数)线程安全释放,一般是通过循环和CAS来保证的,因为释放同步状态的操作会同时来自多个线程。
在这里插入图片描述
4.独占式超时获取同步状态
在这里插入图片描述
5.自定义同步组件
可以自定义允许最大同步线程数等。

5.3 重入锁

重入锁表示锁能够支持一个线程对资源的重复加锁。还支持获取锁时的公平与非公平性选择。
1.实现重进入
1)线程再次获取锁。
2)锁的最终释放。(ReentrantLock通过组合自定义同步器来实现锁的获取与释放,内部通过计数来判别)。
2.公平与非公平锁的区别
公平锁获取顺序应该符合请求的绝对时间顺序,也就是FIFO;而非公平锁只要CAS设置同步状态成功,则表示当前线程获取锁。
公平锁每次都是从同步队列中的第一个节点获取到锁,而非同步锁出现了一个线程连续获取锁的情况。

5.4 读写锁

读写锁内部维护了一对锁,读锁和写锁。
除了保证写操作对读操作的可见性以及并发性的提升外,读写锁能够简化读写交互场景的编程方式。
写操作时会阻塞后续的读操作。
在这里插入图片描述

5.4.1 读写锁的实现

1.读写状态的设计
如果在一个整型变量上维护多种状态,就一定要“按位切割使用”这个变量,读写锁将变量切分成了两部分,高16位表示读,低16位表示写。
2.写锁的获取与释放
写锁是一个支持重进入的排他锁。如果当前线程已经获得了写锁,则增加写状态,如果当前线程在获得写锁时已经有读锁或其他线程不是已经获取写锁的线程,则当前线程进入等待状态。
重入条件之外,还增加了一个读锁是否存在的判断。如果存在读锁,则写锁不能被获取。
3.读锁的获取与释放
读锁是一个支持重进入的共享锁,它能够被多个线程同时获取,在没有其他线程访问时,读锁总会被成功地获取,而所做的也只是增加读状态。
4.锁降级
锁降级指的是写锁降级成为读锁。锁降级是指把持住写锁,再获取到读锁,随后释放写锁的过程。

5.5 LockSupport工具

阻塞和唤醒一个线程时可以使用该工具。
在这里插入图片描述

5.6 Condition接口

Condition接口的等待/通知模式特性。
在这里插入图片描述

5.6.1 Condition接口与示例

Condition是依赖Lock对象的。
在这里插入图片描述Condition方法
在这里插入图片描述
在这里插入图片描述

5.6.2 Condition的实现分析

1.等待队列
一个Condition包含一个等待队列,Condition拥有首节点和尾节点。当前线程调用Condition.await()方法,将会以当前线程构造节点,并将节点从尾部加入等待队列。
在这里插入图片描述

在Object中的监视器模型上,一个对象拥有一个同步队列和等待队列,而并发包中的Lock拥有一个同步队列和多个等待队列。

在这里插入图片描述Condition的实现是同步器的内部类,因此每个Condition实例都能够访问同步器提供的方法,相当于每个Condition都拥有所属同步器的引用。
2.等待

当调用await()方法时,相当于同步队列的首节点移动到Condition的等待队列中。
在这里插入图片描述3.通知
调用Condition的signal()方法,将会唤醒在等待队列中等待时间最长的节点(首节点),在唤醒节点之前,会将节点移动到同步队列中。
Condition的signalAll()方法,相当于对等待队列中的每个节点均执行一次signal()方法,效果就是将等待队列中所有节点全部移动到同步队列中,并唤醒每个节点的线程。
在这里插入图片描述

第六章 Java并发容器和框架

6.1 ConcurrentHashMap的实现原理与使用

6.1.1 为什么要使用ConcurrentHashMap

1.线程不安全的HashMap
在多线程环境下,使用HashMap进行put操作会引起死循环,HashMap的Entry链表形成环形数据结构,导致CPU利用率接近100%,所以在并发情况下不能使用HashMap.
2.效率低下的HashTable
HashTable容器使用synchronized来保证线程安全。
3.ConcurrentHashMap的锁分段技术可以有效提升并发访问率。

6.2 ConcurrentLinkedQueue

ConcurrentLinkedQueue是一个基于链表节点的无界线程安全队列,它采用先进先出的规则对节点进行排序,当我们添加一个元素的时候,它会添加到队列的尾部;当我们获取一个元素时,它会返回队列头部的元素。它采用了“wait-free”算法(即CAS算法)来实现。

6.3 Java中的阻塞队列

6.3.1 什么是阻塞队列

阻塞队列是一个支持两个附加操作的队列。这两个附加的操作支持阻塞的插入和移除方法。
1)支持阻塞的插入方法:当队列满时,队列会阻塞插入元素的线程,知道队列不满。
2)支持阻塞的移除方法:在队列为空时,获取元素的线程会等待队列变为非空。

四种处理方式:
在这里插入图片描述
1)抛出异常:当队列满时,如果在往队列里插入元素,会抛出IllegalStateException(“Queue full”)异常。当队列空时,从队列里获取元素会抛出NoSuchElementException异常。
2)返回特殊值:当往队列插入元素时,会返回元素是否插入成功,成功返回true。如果是移除方法,则是从队列里取出一个元素,如果没有则返回null.
3)一直阻塞:当阻塞队列满时,如果生产者线程往队列里put元素,队列会一直阻塞生产线程,直到队列可用或者响应中断退出。当队列空时,如果消费者线程从队列里take元素,队列会阻塞住消费者线程,直到队列不为空。
4)超时退出:当阻塞队列满时,如果生产者线程往队列里插入元素,队列会阻塞生产者线程一段时间,如果超过了指定的时间,生产者线程就会退出。

6.3.2 Java里的阻塞队列

在这里插入图片描述

6.4 Fork/Join框架

6.4.1 什么是Fork/Join框架

Fork/Join框架用于执行任务的框架,是一个把大任务分割成若干个小任务,最终汇总每一个小任务结果后得到大任务结果的框架。
在这里插入图片描述

6.4.2 工作窃取算法

工作窃取算法是指某个线程从其他队列里获取任务来执行。
优点:充分利用线程进行并行计算,减少了线程间的竞争。
缺点:在某些情况下还是存在竞争,比如双端队列里只有一个任务时。并且该算法会消耗了更多的系统资源,比如创建多个线程和多个双端队列。

6.4.3 Fork/Join框架的设计

1.ForkJoinTask:提供在任务中执行fork()和join()操作的机制。通常情况下继承其子类RecursiveAction:用于没有返回结果的任务。RecursiveTask:用于返回结果的任务。
2.ForkJoinPool:ForkJoinTask需要通过ForkJoinPool来执行。
任务分割出的子任务会添加到当前工作线程所维护的双端队列中,进入队列的头部。
当一个工作线程的队列里暂时没有任务时,他会随机从其他工作线程的队列的尾部获取一个任务。

6.4.4 Fork/Join框架的异常处理

ForkJoinTask提供了isCompletedAbnormally()方法来检查任务是否已经抛出异常或已经被取消,并且可以通过ForkJoinTask的getException方法获取异常。

6.4.5 Fork/Join框架的实现原理

ForkJoinPool由ForkJoinTask数组和ForkJoinWorkerThread数组组成,ForkJoinTask数组负责将存放程序提交给ForkJoinPool的任务,而ForkJoinWorkerThread数组负责执行这些任务。

第七章 Java中的13个原子操作类

7.1 原子更新基本类型类

AtomicBoolean:原子更新布尔类型
AtomicInteger:原子更新整型
AtomicLong:原子更新长整型

7.2 原子更新数组

AtomicIntegerArray:原子更新整型数组里的元素
AtomicLongArray:原子更新长整型数组里的元素
AtomicReferenceArray:原子更新引用类型数组里的元素。
AtomicIntegerArray:类主要提供原子的方式更新数组里的整型,其常用方法如下。
int addAndGet(int i,int delta):以原子方式将输入值与数组中索引i的元素相加。
boolean compareAndSet(int i,int expect,int update):如果当前值等于预期值,则以原子方式将数组位置i的元素设置成update值。

7.3 原子更新引用类型

AtomicReference:原子更新引用类型
AtomicReferenceFieldUpdater:原子更新引用类型里的字段
AtomicMarkableReference:原子更新带有标记位的引用类型

7.4 原子更新字段类

AtomicIntegerFieldUpdater:原子更新整型的字段的更新器
AtomicLongFieldUpdater:原子更新长整型字段的更新器
AtomicStampedReference:原子更新带有版本号的引用类型。

第八章 Java中的并发工具类

8.1 等待多线程完成的CountDownLatch

CountDownLatch允许一个或多个线程等待其他线程完成操作。

public class CountDownLatchTest {
     staticCountDownLatch c=new CountDownLatch(2);

    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(1);
                c.countDown();
                System.out.println(2);
                c.countDown();
            }
        }).start();
        c.await();
        System.out.println("3");
    }
}

8.2 同不屏障CyclicBarrier

让一组线程到达一个屏障时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续运行。

public class CyclicBarrierTest {
     staticCyclicBarrier c=new CyclicBarrier(2);

    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    c.await();
                } catch (Exception e) {
                    e.printStackTrace();
                }
                System.out.println(1);
            }
        }).start();
        try {
            c.await();
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println(2);
    }
}

CyclicBarrier和CountDownLatch的区别
CountDownLatch的计数器只能使用一次,而CyclicBarrier的计数器可以使用reset()方法重置。

8.3 控制并发线程数的Semaphore

Semaphore(信号量)是用来控制同时访问特定资源的线程数量,它通过协调各个线程,以保证合理的使用公共资源。

8.4线程间交换数据的Exchanger

Exchanger(交换者)是一个用于线程间协作的工具类。Exchanger用于进行线程间的数据交换。它提供一个同步点,在这个同步点,两个线程可以交换彼此的数据。

第九章 Java中的线程池

线程池的好处

  • 降低资源消耗
  • 提高响应速度
  • 提高线程的可管理性

9.1 线程池的实现原理

在这里插入图片描述

9.2 线程池的使用

9.2.1 线程池的创建

创建线程池

new ThreadPoolExecutor(corePoolSize,maxinumPoolSize,keepAliveTime,milliseconds,runnableTaskQueue,handler);

1.corePoolSize:线程池的基本大小
2.runnableTaskQueue:任务队列
3.maxinumPoolSize:线程池最大数量
4.ThreadFactory:用于设置创建线程的工厂
5.RejectedExectutionHandler:饱和策略

9.2.2 向线程池提交任务

execute():用于提交不需要返回值的任务

threadsPool,execute(new Runnable(){
	@Override
	public void run(){
		
	}
});

submit()用于提交需要返回值的任务

Future<Object> future=executor.submit(harReturnValuetask);
	try{
	Object a=future.get();
	}catch(InterruptedException e){
	//处理中断异常
	}catch(ExecutionException e){
	//处理无法执行任务异常
	}finally{
	//关闭线程池
	executor.shutdown();
	}

9.2.3 关闭线程池

shutdown(中断所有没有正在执行任务的线程)或shutdownNow(将线程池设为stop,尝试停止所有线程,并返回等待执行任务的列表)方法关闭线程池

9.2.4 线程池的监控

taskCount:线程池需要执行的任务数量
completedTaskCount:线程池在运行过程中已完成的任务数量,小于或等于taskCount
largestPoolSize:线程池的线程数量
getPoolSize:线程池的线程数量
getActiveCount:获取活动的线程数

第十章 Executor框架

10.1 Executor框架简介

10.1.1 框架的两级调度模式

在上层,Java多线程程序通常把应用分解为若干个任务,然后使用用户级的调度器(Executor框架)将这些任务映射为固定数量的线程;在底层,操作系统内核将这些线程映射到硬件处理器上
在这里插入图片描述

10.1.2 Executor框架的结构与成员(了解)

1.Executor框架的结构
主要由三部分组成
1)任务。包括被执行任务需要实现的接口:Runnable接口或Callable接口。
2)任务的执行。包括任务执行机制的核心接口Executor,以及继承自Executor的ExecutorService接口。Executor框架有两个关键类实现了ExecutorService接口(ThreadPoolExecutor和ScheduledThreadPoolExecutor)。
3)异步计算的结果。包括接口Future和实现Future接口的FutureTask类。
2.Executor框架的成员
(1)ThreadPoolExecutor
(2)ScheduledThreadPool
(3)Funture接口
(4)Runnable接口和Callable接口

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值