并发:core java 14

本文深入探讨并发编程的核心概念,包括线程与进程的区别、锁机制、条件变量、原子操作及其实现机制。详解了Java中synchronized关键字、ReentrantLock、AQS、线程池、阻塞队列等关键组件的原理与应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Core Java 第14章 并发
解决并发时共享数据的安全问题的两种方式:

  • 加锁,控制被保护代码的访问线程的个数。悲观锁,线程会阻塞。
  • CAS+版本戳。乐观锁,线程不阻塞,但并发数较多时,会造成CPU浪费。

基本概念

线程与进程的最本质区别是:

  • 进程是资源分配的基本单位;

  • 线程是CPU计算的基本单位。

  • 每个进程拥有各自独立的内存空间,进程之间不能直接访问彼此的内存数据;

  • 同一个进程内的所有线程共享这个进程的内存空间,进程内的一个数据可以被多个线程同时访问。因此线程间的通信相对进程间的通信要高效的多。

  • 进程使得一台计算机能够同时执行多个应用程序,进程是多个应用程序的并发执行。

  • 线程使得一个应用程序能够同时执行多个任务,线程是一个应用程序内的多个任务的并发执行。

  • 进程是程序在数据集上的一次运行。

  • 线程依赖于进程存在,是进程中的一个执行路径,

线程的定义及创建

建议通过实现Runnable接口或者Callable接口的方式定义线程,便于扩展。

创建线程的方式:

  • 继承Thread类。 ——模板模式
  • 实现Runnable接口,封装成Task类。 —— 策略模式
  • 实现Callable接口,封装到FutureTask中,再封装成Thread类,最后在需要的地方通过FutureTask::get方法异步获取call方法的返回值。 ——Future模式

无论哪种方式最终都是创建一个Runnable接口的实例对象:

  • Thread类定义本身就实现了Runable接口。
  • FutureTask类定义也实现了Runable接口,因此可以用它来创建一个Thread类对象。

Future接口

future用来代表一个异步计算的结果。提供了检测异步计算是否结束、等待计算结束、获取异步计算结果的方法。

  • get():此方法会阻塞当前线程,直至future所代表的异步计算结束,然后获取计算结果。
  • get(long,timeunit):试图在指定时间内获取计算结果,如果异步计算在指定时间内仍未返回结果,则此方法抛出TimeoutException。
  • cancel(boolean):此方法用于取消future所代表的异步计算任务的执行。但是并不能确保异步任务真被取消。可以通过isCancelled()方法来获取任务是否是在执行完毕前被取消掉了。
  • isCancelled():如果future所代表的异步计算在执行完成前被取消掉了,则返回true
  • isDone():如果future所代表的异步计算任务已经结束,则返回true。无论是执行完毕自然结束、还是由于没有被捕获的异常而中断、或者是被取消掉,只要计算任务已经结束,就返回true。

RunnableFuture

一个Runnable的Future:Future代表一个异步计算的结果,RunnableFuture则代表了一个可执行的异步计算的结果。当对Futrue进一步扩展后,就可以用线程来执行计算获得结果了。

  • interface RunnableFuture extends Runnable, Future
  • run():相比Future接口,此接口多了一个继承自Runnable接口的run方法。

FutureTask

  • 实现了RunnableFuture接口(间接实现了Runnable接口和Future接口),
  • 持有一个Callable接口的实例,定义了一个int类型成员变量state用于存储异步计算的任务的状态,同时定义了几个int类型的常量表示异步计算任务的状态:NEW=0 COMPLETING=1 NORMAL=2 EXCEPTIONAL=3 CANCELLED=4 INTERRUPTING=5 INTERRUPTED=6;持有一个用于代表执行异步计算任务的Thread实例runner(实际是会将运行当前futureTask所持有的Callable实例的call方法的线程的引用赋值给runner);定义了一个单链表WaitNode类,并定义了一个此类的实例waiters作为成员变量,用于存储等待异步计算完成的所有线程。
  • 定义了两个构造函数,其中一个参数为Callble实例,另一个构造函数参数为Runable接口实例对象和一个用于存储异步执行结果的对象(此构造函数会调用Executors中的工具方法将两个参数通过一个RunnableAdapter构造成一个Callable实例)。
  • run()方法首先确认任务处于NEW状态,然后将runner赋值为当前线程,然后调用所持有的Callable实例的call()方法执行计算任务,并获取方法的返回值result,然后调用set(result)方法;set()方法使用Unsafe的CAS确认任务原状态为NEW并将状态更新为COMPLETING,然后将outcome赋值为result,接着将状态更新为NORMAL,最后调用finishCompletion()方法来通知所有调用了get()方法的等待异步计算结果的其它线程。get()方法对于处在COMPLETING状态及以前的任务,会将当前线程链接进等待链表waiters中去,并调用LockSupport.park(this)方法来使得当前线程阻塞,等待执行异步计算的线程执行完成。finishCompletion()方法会遍历所有阻塞等待异步计算结果的线程链表waiters,调用LockSupport.unpark(t)来释放这些阻塞的线程t。方法cancel()方法是通过调用其持有的Thread实例的interrupt()方法实现的。

线程的中断

如果一个线程启动后,用户就对其失去了控制,无法中止其执行,是非常不友好的。
用户可以通过thread.interrupt();来中断线程thread,这个方法会调用native方法interrupt0(),会将线程的中断状态置为true,中断状态是每个线程都有的boolean标志;
用户可以通过实例方法thread.isInterrupted()来判断线程thread是否被中断。
用户还可以通过静态方法Thread.interrupted()来判断当前线程是否被中断,此方法会将中断标志位重置为false;
用户应该不断检测中断状态,并对此做出相应的操作。
但是当线程正在执行一个长时间的操作,例如大数据处理操作。或者是被阻塞的操作,且此操作是无法中断的操作,例如一些IO阻塞的操作,线程将不能被如期中断,此时应考虑选择可中断的IO
但是当线程中有sleep()\wait()等阻塞方法阻塞时,这些方法内部会调用interrupted()方法自行检测中断状态,当检测到为true时,会清除中断状态,并抛出InterruptedException异常。
用户不应压制InterruptedException异常,如下代码不推荐:

void mySubTask(){
	...
	try{sleep(delay);}
	catch(InterruptedException e){} //do not ignore!
	...
}

可以在异常处理块中再次设置中断状态,以便调用者在需要时获取线程退出的原因——执行完毕后正常退出、还是被中断后退出:

void mySubTask(){
	...
	try{sleep(delay);}
	catch(InterruptedException e){ Thread.currentThread().interrupt();  } 
	...
}

或者直接抛出异常

void mySubTask() throws InterruptedException {
	...
	sleep(delay); 
	...
}

线程的状态-state

在这里插入图片描述
new \ runnable \ blocked \ waited \ time-waited \ terminated

  • runnable:就绪状态下等待运行的线程(start、获取了等待的锁、等待的条件被满足了);和正在运行的线程,这两种情况下线程的状态都是runnale。
  • blocked:当一个线程试图获取一个内部的对象锁(而不是java.util.concurrent库中的锁),而该锁被其它线程占有时——即调用synchronized(o)等待获取o的内部对象锁时。
  • waited :当线程自己判断到自己等待的资源未到,而调用了wait() ,或者等待其它线程执行完毕调用join(),或者调用了java.concurrent.util包中的Lock.lock() \ Condition.await()时,
  • time-waited:计时等待。当调用计时等待方法时,如thread.sleep \ o.wait \ thread.join \ lock.tryLock \ condition.await(time)
  • terminated:run方法执行完毕正常退出,自然死亡;或者因为一个没有捕获的异常终止了run方法而意外死亡。

线程的优先级

windows支持7个优先级,而oracle为linux提供的java虚拟机中,线程的优先级被忽略-所有线程有相同的优先级。
线程优先级有可能造成低优先级的线程一直处于饥饿状态(CPU一直选择执行高优先级线程,低优先级线程始终没有机会执行),如非必要,无需设置优先级。

守护线程-deamon

当虚拟机中只剩下守护线程时,虚拟机就退出了。

守护线程就是为其它线程服务的线程,当所有非守护线程都工作完毕,守护线程也没有运行的意义了。

处理线程的异常

线程的run方法没有声明异常,java提供了一个Thread.UncaughtExceptionHandler接口,这个接口只有一个方法:uncaughtException(Thread t,Throwable e),开发者可以为线程t设置一个这个接口的实例来处理线程的运行时异常,方式是t.setUncaughtExceptionHandler(h)

也可以用静态方法Thread.setDefaultUncaughtExceptionHandler为所有线程安装一个默认的处理器。

如果没有设置默认的线程异常处理器,也没有为每个线程设置特定的异常处理器,则线程的处理器就是该线程的ThreadGroup对象,因为ThreadGroup类实现了Thread.UncaughtExceptionHandler接口,当线程出现异常时,会调用其ThreadGroup对象的uncaughtException方法。此方法逻辑如下:调用父ThreadGroup的uncaughtException方法;如果没有父,则调用全局默认的处理器,如果没有全局默认处理器,则输出栈轨迹(StackTrace)到标准错误流上。

竞争条件与同步

当多个线程对同一共享数据操作时,形成了竞态条件。

可重入锁-ReentrantLock

可重入是指,同一个线程可以重复的获得自己已经持有的锁。

因此,如果在执行一个加锁的代码块的过程中,又调用了另一个含有加锁代码块的方法,如果两个代码块加的是同一个锁,嵌套的调用的代码块中的锁仍然可以进入,即可重入。

synchronized关键字加锁:
java中每个对象有一个监视器锁monitor,每个monitor有一个记录当前占有monitor的线程(owner_thread)和一个进入数(enter_count)。当有线程试图对对象加锁时:

  • 首先查看此对象的monitor的进入数是否为0
  • 如果进入数不为0,说明monitor已经被占,则比较当前占有monitor的线程和请求占有monitor的线程是否为同一个线程,如果是则进入数+1,否则请求占有monitor的线程阻塞。
  • 如果进入数为0,则说明monitor未被占用,将占有monitor的线程设置为请求加锁的线程,并将进入数+1。
  • 当线程退出某个synchronized代码块时:将进入数-1,当进入数减至0时,owner_thread线程退出monitor,不再是这个monitor的所有者,其它被阻塞的线程开始尝试进入monitor。

ReentrantLock锁:ReentrantLock对象统一有一个execlusiveOwnerThread存储持有lock对象的线程,也会保持一个持有计数hold_count来跟踪对lock方法的嵌套调用,线程每一次调用锁的lock方法都要有unlock来释放锁。

公平锁也无法确保公平

可以通过ReentrantLock(boolean fair)构造公平锁,听起来很好,但是相对较慢,且线程的调度是操作系统控制的,JVM并不能完全确保公平。

条件对象\条件变量- conditional var

通常,线程获得了锁,进入了临界区后,却发现还需要满足某一条件才能执行。要使用一个条件对象来管理那些已经获得了锁却由于其它条件不足而不能做有效工作的线程。

lock.lock();
try{
	while(account[i]<amount){
		// wait
		...
	}
	// transfer funds
	...
}
finally{

	bankLock.unlock();
}
内部锁和条件-synchronized \ wait \ notifyAll \ notify

内部锁:synchronized(o),任一对象都有一个监视器锁monitor,
内部条件:o.wait()任一对象内部锁内都有一个内部条件。
由内部锁锁来管理哪些试图进入synchronized方法(代码块)的线程,由内部条件来管理那些调用wait的线程。

内部锁和内部条件的一些局限性

  • 不能中断一个正在试图获得锁的线程
  • 试图获得锁时,不能设定超时退出
  • 每个锁仅有一个内部条件,可能是不够的
最好既不使用Lock/Condition,也不使用synchronized关键字
  • 许多情况下可以使用java.concurrent.util包中的一种机制,它会为你处理所有的加锁。
  • 如果synchronized关键字适合你的程序,那么请尽量使用它,这样可以减少编写的代码量,减少出错的几率。
  • 如果特别需要Lock/Condition结构提供的独有特性时,才使用它们
客户端锁定

当synchronized作用于非静态方法时,线程获得的是调用此方法的当前对象的锁。
当synchronized作用于一个不是调用当前方法的对象的对象时,其实是在使用一个对象的锁来实现额外的原子操作,也称为客户端锁定
例如:当我们将accounts定义成同步类Vector时,…
通常客户端锁定是非常脆弱的,不推荐使用。此时应该从类设计上入手,将需要保护的代码块,设计在被锁定的对象所属类中实现,这样锁定的就是this,不属于客户端锁定。

监视器概念

监视器(monitor)是20世纪70年代就被提出的一个用于解决并发的安全性问题的概念。
在这之前多年来,研究人员努力寻找一种可以让程序员不需要考虑如何加锁,就可以保证多线程的安全性。监视器这一概念是这个问题的最成功的解决方案之一。用Java的术语来讲,监视器具有如下特性:

  • 监视器是只包含私有域的类
  • 每个监视器类的对象有一个相关的锁
  • 使用该锁对所有的方法进行加锁(对所有方法自动加锁,不需显式声明)
  • 该锁可以有任意多个相关条件

java设计者不是很精确地实现了监视器这一概念,即synchronized \ wait \ notifyAll。
然而,java的类并不要求所有域必须私有、不要求所有方法必须声明为synchronized、内部锁对客户是可用的,这三点同监视器概念的提出者的要求相差很多,这降低了java线程的安全性。
java设计者对安全性的轻视激怒了监视器概念的提出者,他严厉批评了这一方案。

原子性

CPU执行一系列操作的过程中不会中断,即操作系统会确保CPU一旦开始执行第一个操作后,则会不中断地执行完这一系列操作后才有可能被调度执行其它线程,这样的一系列操作就是原子性操作。例如赋值操作int a=1。而a=a+1这样的操作就不是原子操作。

JVM提供了Unsafe类,此类提供了硬件级别的原子操作。

java.util.concurrent.atomic包中的原子类,如AtomicInteger、AtomicLong等类中的原子方法,都是Unsafe类提供的支持。

多线程安全,就要保证临界区操作的原子性:

  • synchronized代码块,以及lock锁的代码块都能确保多线程安全性,其中之一就是能确保原子性。
  • 另外有一种方式可以确保多线程安全,就是对于可进行原子操作的共享变量,加volatile修饰符。volatile既能却保共享变量的可见性、也能确保有序性(防止指令重排),再结合共享变量本身可进行的原子操作,就能确保共享变量的安全性。一般情况下这种方式用于通过标志位进行同步的线程。
volatile域

有时,仅仅为了读写一两个实例域就使用同步,显得开销过大。
对此,我们先分析一下,多线程并发如何出现竞态条件的:

  • 多处理器情况下,每个CPU都有自己的寄存器和缓存,共享变量被多个线程使用时,被复制到各自的CPU或者缓存中去了,
  • 编译器可以改变指令顺序而使得吞吐量最大化。

当使用锁来保护临界区时,以上情况都会被锁机制解决。

而volatile关键字为实例域的同步访问提供了一种免锁机制。如果实例域被声明为volatile,则就告知了编译器和虚拟机,该域是多线程共享的数据域。

但是要注意,volatile不能提供原子性,因此需要确保对volatile域的操作都是原子操作,才能保证多线程安全。
或者利用java.util.concurrent.atomic包中的类型,这些类使用了高效的机器级指令(Unsafe,而非锁)来保证非赋值操作的其它操作的原子性。

Unsafe

Unsafe源码

  • 提供硬件级别的原子操作,方式是CAS,以Unsafe类中的getAndAddInt方法为例:
public final int getAndAddInt(Object o, long offset, int delta) {
    int v;
    do {
        v = getIntVolatile(o, offset);
    } while (!compareAndSwapInt(o, offset, v, v + delta));  // 如果在do的过程中,v被其它线程更新了,则compareAndSwapInt将为false,则再次尝试继续do
    return v;
}
  • Unsafe类有两个native方法,为线程同步阻塞提供了支持:
public native void park(boolean isAbsolute, long timeout);--线程挂起
public native void unpark(Object thread);--线程恢复

而在工具类LockSupport提供的多线程同步阻塞方法中,均依赖Unsafe:

LockSupport.park(Object blocker){ 
	Thread t = Thread.currentThread();
	setBlocker(t, blocker);  //  设置线程的阻塞对象,即线程请求占有的资源
	Unsafe.park(false, 0L); // 没有设置阻塞超时时间的阻塞,natvie
	setBlocker(t, null);   // 当执行到此行,说明线程获取了请求的资源,那么需要设置阻塞对象为null
}
LockSupport.unpark(Thread t){ 
	if(t!=null) 
		Unsafe.unpark(t);
}		

AbstractQueuedSynchronizer(AQS-抽象队列同步器?) \ FutureTask \ SychronousQueue.TransferQueue(FIFO) \ SychronousQueue.StackQueue(LIFO)等类的同步阻塞均是调用LockSupport的工具方法来是的。

而ReentredLock正是依赖AQS实现的同步阻塞,其它同步队列 ArrayBlockingQueue \ LinkedBlockingQueue \ PrirotyQueue 都是依赖ReentredLock实现的。

综上,Unsafa是实现阻塞的基础工具类,LockSupport封装了Unsafe为多线程阻塞提供支持。

  • 提供了内存分配和内存释放API
    allocateMemory、reallocateMemory、freeMemory
  • 可以定位某对象的字段的内存位置,也可以修改字段值,即使字段是私有的
public native long objectFieldOffset(Field field);
public native void putOrderedInt(Object obj, long offset, int value);
public native void putOrderedLong(Object obj, long offset, long value);
...
java.util.concurrent.atomic

由Unsafe的CAS来提供的硬件级别的原子操作

AtomicInteger \ AtomicIntegerArray \ AtomicIntegerFieldUpdater
AtomicInteger
incrementAndGet();将Integer的value+1,并对其做原子更新,返回更新后的值
decrementAndGet();将Integer的value-1,并对其做原子更新,返回更新后的值
addAndGet(int delta);将Integer的value+delta,并对其做原子更新,返回更新后的值
compareAndSet(int except,int update);如果Integer对象当前的value==except,则将Integer的value原子更新为update,返回true;否则返回false
getAndIncrement();将Integer的value+1,并对其做原子更新,返回更新前的值
getAndDecrement();将Integer的value+1,并对其做原子更新,返回更新前的值
getAndAdd(int delta);将Integer的value+delta,并对其做原子更新,返回更新前的值
getAndSet(int update);将Integer的value原子更新为update,返回true
set(int value);
get();
intValue();
accumulateAndGet(int x,IntBinaryOperator o);将操作符o应用于x和原value,计算所得的值作为新的value赋值给当前对象并返回。
updateAndGet(IntUnaryOperator f);将操作符f应用于当前value,计算所得值作为新的值赋值给当前对象。
AtomicIntegerArray

AtomicIntegerArray类中方法和AtomicInteger类中的方法功能基本一致,不过全都多了一个参数,即数组下标,用于指定要原子地更新哪个位置的元素。

AtomicIntegerFieldUpdater

基于反射的,因此要有被更新的字段的访问权限。另外字段必须是volatile的。

AtomicIntegerFieldUpdater类中的方法和AtomicInteger类中的方法的功能基本一致,不同的是必须加一个参数即要更新的对象o,另外更新前要先获取o的要更新的字段的AtomicIntegerFieldUpdater的实例,如下:

 public static void main(String[] args){
		AtomicIntegerFieldUpdater updater = AtomicIntegerFieldUpdater.newUpdater(Test.MyThread.class,"count"); 
		MyThread count = new MyThread(5);
		int updated = updater.incrementAndGet(count);
		out.println(updated);
	}

	static class MyThread 
	{
		volatile int count;

		public MyThread(int count){
			this.count = count;
		}		
	}

AtomicReference \ AtomicReferenceArray \ AtomicReferenceFieldUpdater

这些类提供引用类型对象的原子更新

AtomicStampedReference

为了解决ABA问题,引入版本戳机制。

每次读取前先获取版本戳,然后读取数据,并对数据做计算,最后将计算后的数据跟新更新时,再次读取一次,比较一下再次读取得的数据的版本戳与第一次读取时的数据的版本戳是否一致,如果不一致了,说明数据已经被别的线程更新过了。如果版本戳没有变,则可以更新,但必须同时更新戳。以便告诉其它线程,此数据被更新过了。

LongAdder \ LongAccumulator \ DoubleAdder \ DoubleAccumulator

如果有大量线程会对AtomicInteger等原子变量进行操作,会经历大量的比较和失败,java8提供了LongAdder和其它类,大大提高了这种情况下的操作的性能。

阻塞队列

public interface BlockingQueue<E> extends Queue<E>

即是队列,自然继承了Queue的接口方法,另外,其为多线程同步提供了阻塞或者非阻塞方法,还有超时不再等待的同步方法,如下:

E take();      // 取队头,队列空时阻塞
put(E e);     // 向队尾添加元素,队列满时阻塞
boolean  offer( e , timeout, timeUnit);  // 添加元素,超时则放弃,返回false
boolean  poll(timeout, timeUnit);  // 取队头,超时则放弃,返回false

ArrayBlockingQueue

采用ReentredLock+notFull+ notEmpty实现的

LinkedBlockingQueue

由于数据结构是链表,则其头尾不需要同步,因此取头、加尾两种操作并不竞争,只有多个同时去取头的线程之间彼此才需要同步。

因此,采用takeLock \ putLock 两个锁,配合notFull \ notEmpty 实现的。

Executors.newFixedThreadPool()构造的执行器,用的就LinkedBlockingQueue阻塞队列

new ThreadPoolExecutor( nthreads, nthreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());

PriorityBlockingQueue

小顶堆实现的,阻塞队列。自动扩容。
lockInterruptly + notEmpty

DelayedQueue

只有入队时间超过了指定时长的元素,才有机会出队。

入队的元素必须是Delayed接口的子类的对象。

通过lock.lockInterruptly() + available.await();方式实现阻塞

SychronousQueue

内部只允许包含一个元素的队列(必须有一个线程做了take()操作,才能有一个线程做put()操作,反之亦然)。

支持FIFO和LIFO。

Executors.newCachedThreadPool()构造的执行器中用的阻塞队列就是Synchronous类的实例。

new ThreadPoolExecutor( 0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());

采用LockSupport.park()和LockSupport.unPark()来实现阻塞和解锁。

执行器 Executor

执行器,就是执行线程的工具。

执行器服务 ExecutorService

执行器服务,扩展了执行器

线程池执行器 ThreadPoolExecutor

实现了ExecutorService接口,采用线程池的方式管理、执行线程

在这里插入图片描述
Unsafe -> LockSupport ->AQS->Lock -> 同步器(Semaphore | countdownLantch)\ CopyOnWriteArrayList
Unsafe ->Atomic*
Unsafe -> ConcurrentLinkedQueue \ ConcurrentLinkedDeque

ThreadLocal

空间换时间,每个线程都有一个变量的副本,线程遇到共享变量不会阻塞。
(加锁,则是时间换空间,阻塞但是不会有多个副本)

注意:变量使用完后一定要记得.remove()。
否则,如果线程池中的线程一直不被销毁,则每个线程持有的ThreadLocalMap类型的变量threadLocals 也不会被回收,虽然threadLocals 中的Entry定义为extends WeakReference<ThreadLocal>,但GC只会对Entry引用的的key即ThreadLocal对象回收,而value则不会被回收。在实际场景中,如果不主动.remove(),那么就会出现一个任务早已经被线程运行完,但此任务相关的ThreadLocal的值却永远贮留在threadLocals中的情况。另外一种情况是,任务执行时间很长,但是ThreadLocal对象的业务功能其实只在任务的某小段时间中有意义,如果不及时.remove(),则对象的存在贯穿了线程的始终。因此,开发人员需要有意识的在业务代码中主动对ThreadLocal对象.remove()。用法如下:

class ThreadLocalExample{
	ThreadLocal<Integer> localInt ;
	public void useAndReleaseLocal(){
		localInt = new ThreadLocal<>();
		localInt.set(1);
		// business code with localInt;
		localInt.remove();
	}
}

应用场景

  • 当一个数据需要传递多个方法调用,比如:spring中每个request的Attribute,有时候在Service层甚至也要获取

completionFutrueTask

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值