线程和进程

进程、线程

进程、线程的基本概念

  • 进程:
    • 进程是操作系统进行资源分配最小单位;所谓的资源包含:CPU、内存空间、磁盘IO等等。同一个进程中的多条线程是共享该进程中的全部系统资源的,而不同的进程之间则是相互独立的
    • 进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动程序是死的,静态的,而进程是活的,动态的。
    • 进程可分为系统进程和用户进程;凡是用于完成操作系统的各种功能的进程就是系统进程,他们就是处于运行状态下的操作系统本身;而用户进程就是所有由你启动的进程
  • 线程:
    • 线程是进程中的一个实体,是CPU调度分派基本单位(最小单位),线程是比进程更小的,能够独立运行的基本单位
    • 线程本身基本上不拥有系统资源,只会拥有一点在运行中必不可少的资源(如程序计数器、一组寄存器和栈),但是他可以共享当前所属进程的所有资源
    • 真正执行任务的单位

并行、并发

  • 并行:

    • 并行是真正意义上的同时运行
    • 并行是指应用能够同时执行不同的任务
  • 并发:

    • 并发是一定需要时间单位的,也即是说单位时间内的并发量是多少,离开了时间单位讨论并发量其实是没有意义的。
    • 并发本质上就是通过一定的方式,达成所谓的看起来多个程序一起运行的效果;最容易理解的方式就是:时间片轮转进程调度法
    • 并发就是指应用能够交替执行不同的任务,比如单CPU核心下执行多线程并非是同时执行多个任务,而是以你几乎不可能察觉的速度不断去切换这两个任务,以达到同时执行的效果
  • 高并发的优势

    1. 充分利用CPU的资源
      • 因为程序的基本调度单元是线程,并且一个线程也只能在一个CPU的一个核的一个线程跑,如果你是个i3的CPU的话,最差也是双核心4线程的运算能力:如果是一个线程的程序的话,那是要浪费3/4的CPU性能:如果设计一个多线程的程序的话,那它就可以同时在多个CPU的多个核的多个线程上跑,可以充分地利用CPU,减少CPU的空闲时间,发挥它的运算能力,提高并发量
    2. 加快响应用户的时间
    3. 使得代码模块化、异步化、简单化
  • 多线程的注意事项

    1. 线程之间的安全性

      • 因为线程是享有同一个进程里的所有资源的,所以当存在多个线程的时候,就会存在相同资源被多个线程同时使用的时候,就会存在线程安全问题

        为什么会造成线程安全呢?需要结合JMM(Java Memory Model)技术来分析,具体原理请见后续关于JMM分析的博文
    2. 线程之间的死循环

      • 为了解决多线程之间的安全问题,JAVA引入了锁机制,但是这也同时会带来死锁问题:不同的线程都在等待那些不可能被释放的锁,从而导致所有的工作无法完成
    3. 线程开辟的太多导致资源耗尽造成死机

      • 开辟线程是需要消耗资源的,而某些资源是有限的,如文字描述符、栈等,当开辟了过多的线程时,则可能导致系统资源被耗尽以及CPU过渡切换,进而造成死机

JAVA中的线程

  • JAVA中启动线程的方式

    1. Thread方式新启线程

      public class NewThread {
          private static class UserThread extends Thread {
      
              @Override
              public void run() {
                  super.run();
                  //do my work
                  System.out.println("this is User Thread extends Thread");
              }
          }
          
          public static void main(String[] args) {
              UserThread userThread = new UserThread();
              //调用start方法来进行线程启动
              //start方法同一个线程只能调用一次
              userThread.start();
          }
      } 
      
      /*
      * 查阅JDK中的Thread类中的start方法
      **/
      public synchronized void start() {
          //首先会判断当前Thread对象状态,如果调用过一次,则会抛出异常
      	if (this.threadStatus != 0) {
              throw new IllegalThreadStateException();
           } else {
              this.group.add(this);
              boolean var1 = false;
      
              try {
                  //真正线程的开启是通过调用native方法start0实现的
                  this.start0();
                  var1 = true;
              } finally {
                  try {
                      if (!var1) {
                          this.group.threadStartFailed(this);
                      }
                  } catch (Throwable var8) {}     
              }   
           }
      }
      
    2. Runnable/Callable

      public class NewThread {
          private static class UserRunnable implements Runnable {
      
              @Override
              public void run() {
                  //do my work
                  System.out.println("this is User Runnable implements Runnable");
              }
          }
      
          public static void main(String[] args) {
              UserRunnable userRunnable = new UserRunnable();
              new Thread(userRunnable).start();
          }
      }
      
      • Thread和Runnable的区别(JAVA中)
        • Thread是线程的抽象
        • Runnable是对任务(业务逻辑)的抽象
  • 线程的终止

    • stop()suspend()…这些方法为什么不建议使用

      • 因为这些方法都是强制执行的,如stop()方法,当调用时,强制执行线程终止,不会去进行资源的释放,而这时候如果当前线程持有锁,就会极容易造成死锁的问题发生
    • interrupt

      • 要进行线程的终止,建议使用interrupt方法买这个方法有三个使用,分别是

        1. public void interrupt(){}
          • 对线程发起中断请求
        2. public static boolean interrupted(){}
          • 判断当前线程是否存在中断请求,并将中断标志位复位为false
        3. public boolean isInterrupted(){}
          • 判断当前线程是否存在中断请求
      • interrupt本质上只是一个中断标志位,只是针对当前线程发起了一个中断请求,线程可以选择执行相应中断,同样也可以完全不做理会;而这就带出了一个概念:在JDK中,线程是协作式的,而非抢占式的

      • 不推荐自己定义一个标志位去进行中断的判定,因为如果是自己定义的标志位,在sleep()或者wait()等情况下,完全无法进行判断

      • 当我们在线程中进行阻塞:即调用sleep\wait等方法时,中断仍旧可以被感知,可以捕获到中断异常

        public class InterruptThread {
        
            static class  Interrupt extends Thread {
        
        
                @Override
                public void run() {
        
                    while (!isInterrupted()) {
                        try {
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
                            //当我们的线程处在阻塞时,中断还是可以被感知到,但是在获取了中断异常后,该中断标志位会被复位为false
                            System.out.println("in try/catch this thread = " + this.getName() + ", isInterrupt = " + isInterrupted());
                            //如果我们想要线程正确的相应interrupt,则需要在catch识别到中断时,重新置位中断标志
                            interrupt();
                           e.printStackTrace();
                        }
                        System.out.println("out this thread = " + this.getName() + ", isInterrupt = " + isInterrupted());
        
                    }
                }
            }
        
        
            public static void main(String[] args) throws InterruptedException {
                Interrupt interrupt = new Interrupt();
                interrupt.start();
                Thread.sleep(200);
                interrupt.interrupt();
        
            }
        
        }
        
        • 为什么在sleep\wait这些阻塞方法捕捉中断异常之后要将中断标志复位?
          • 所有的中断指令的响应都必须是在线程运行状态下,而调用sleep\wait等方法,线程处于阻塞状态,如果线程从阻塞状态唤醒转为运行状态判别到中断请求马上进行终止,那么就和直接调用stop等方法一致,有可能会出现相关资源或者锁未被释放的情况!所以需要将中断标志位复位,留给程序反应的时间。
  • 线程中的runstart方法

    • run()

      • 本质上就是一个普通的成员函数,可以单独进行调用

        public class StartAndRun {
            private static class RunAndStratThred extends Thread {
        
                @Override
                public void run() {
                    int i = 0;
                    while (i < 90) {
                        System.out.println("this Thread name = " + Thread.currentThread().getName());
                    }
                }
            }
        
            public static void main(String[] args) {
                RunAndStratThred runAndStratThred = new RunAndStratThred();
                runAndStratThred.run();
            }
        }
        
  • start()

    • start方法才是真正开启线程的方法,且一个线程start只能调用一次

        /*
        * 查阅JDK中的Thread类中的start方法
        **/
        public synchronized void start() {
            //首先会判断当前Thread对象状态,如果调用过一次,则会抛出异常
        	if (this.threadStatus != 0) {
                throw new IllegalThreadStateException();
             } else {
                this.group.add(this);
                boolean var1 = false;
        
                try {
                    //真正线程的开启是通过调用native方法start0实现的
                    this.start0();
                    var1 = true;
                } finally {
                    try {
                        if (!var1) {
                            this.group.threadStartFailed(this);
                        }
                    } catch (Throwable var8) {}     
                }   
             }
        }
      
  • 线程中的各个状态

在这里插入图片描述

  • 初始状态

    • 当我们new出一个实例时
  • 运行状态

    • 调用start(),线程就进入就绪状态,等待CPU给其分配时间片
    • 等到相关线程被分配了CPU时间片,则进入运行状态,此时运行中的线程可通过yield()方法直接返回就绪状态
    • 需要注意,当线程调用**interrupt()**时,最终都是回到就绪状态,这是因为JAVA中线程是协作式的而非抢占式,**interrupt()**方法也只是置中断标志位
  • 等待状态/等待超时状态

    • 线程调用sleep/wait等方法,进入等待/等待超时状态
    • 调用Lock显示锁,其本质底层是通过LockSupport来实现的,本质上也是进入的等待/等待超时状态
  • 阻塞状态

    • 有且只有Synchronized能够使线程进入阻塞状态
  • 终止

    • 线程执行完毕、异常退出(比如异常)或者调用stop/suspend等方法进入死亡状态
  • 相关方法解析

    • yield()

      • 将线程从运行状态转换为就绪状态
      • yield方法让出时间不可设定,即无法通过调用yield方法延时释放CPU执行权
      • 执行yield方法只是让出CPU执行权,而不会让出锁、内存、磁盘IO
    • join()

      • 调用该方法的线程进入运行状态,而之前正在运行的线程挂起,等待调用了**join()**的方法执行结束后在执行
      • 可以保证线程顺序执行
    • setDaemon(boolean var1)

      • 将当前线程设置为守护线程,守护线程会在当前进程中的所有线程运行结束后,自动终止

      • 需要注意的是:在守护线程中,finally代码块中的内容不一定会被执行;原因是:在当前进程中的所有线程全部结束之后,守护线程会直接自动终止,是否执行finally中的代码,完全看操作系统是否有给他分配执行时间片

      • finally代码块中的内容在用户线程中肯定是会被执行的!

        class SetDamon {
            
            static class UserThread extends Thread {
                @Override
                public void run() {
                    System.out.println("enter Thread, name = " + Thread.currentThread().getName());
                    try {
                        System.out.println("enter try!!!");
                    } finally {
                        System.out.println("enter finally");
                    }
                }
            }
        
            public static void main(String[] args) throws InterruptedException {
                UserThread userSetDamon = new UserThread();
                //当将线程设置为守护线程之后,其余线程结束之后,守护线程也就直接结束了,即此时主线程结束之后,守护线程直接也就结束了,根本不会去执行run方法;
        //        userSetDamon.setDaemon(true);
                //如果不是守护线程,则启动后一定会去执行run,不论主线程是否结束
                userSetDamon.start();
        
            }
        }
        

Synchronized()内置锁

  • 对象锁

    public class SyncTest {
        public static Object o = new Object();
        
        public void Test1() {
            //这个就是对象锁,锁的是o这个static对象
            synchronized(o) {
                /*
                * 业务逻辑
                */
            }
        }
        
        public void Test2() {
            //这个也是对象锁,锁的是SyncTest实例对象
            synchronized(this) {
                /*
                * 业务逻辑
                */
            }
        }
        
        //这个也是对象锁,锁的也是SyncTest实例对象,等价于synchronized(this)
        public synchronized void Test3() {
            /*
            * 业务逻辑
            */
        }
    }
    
  • 类锁(class对象锁)

    • 每一个类在类加载的时候,他们都有一个class对象
    public class SyncTest {
        
        public void Test1() {
            //这个就是类锁,锁的是Object这个类
            //实际上锁的是类加载后,JVM中的Object这个类的class对象!
            synchronized(Object.class) {
                /*
                * 业务逻辑
                */
            }
        }
        
        public void Test2() {
            //这个也是类锁,锁的是SyncTest类
            //实际上锁的是类加载后,JVM中的SyncTest这个类的class对象
            synchronized(SyncTest.class) {
                /*
                * 业务逻辑
                */
            }
        }
        
        //这个也是类锁,锁的是这个static静态类
        //实际上锁的是类加载后,JVM中的这个static静态成员变量的class对象
        public synchronized static void Test3() {
            /*
            * 业务逻辑
            */
        }
    }
    

显示锁

  • 显示锁可通过声明定义ReentrantLock来进行使用,具体看代码示例:

    /**
     * 使用显示锁的范式
     */
    public class LockDemo {
    
    	private int count = 0;
    
    	// 内置锁 == this
    	private synchronized  void test() {
    
    	}
    
    	// 内置锁 == LockDemo.class
    	private static synchronized  void test2() {
    
    	}
    
    	private synchronized void test3() {
    		// 业务逻辑,无法被中断
    	}
    
    
    	// 声明一个显示锁之可重入锁  new 可重入锁
    	// 非公平锁
    	private Lock lock = new ReentrantLock();
    	
    	public void incr(){
    		// 使用 显示锁 的规范
    		lock.lock();
    		try{
    			count++;
    		} finally {   // 打死都要执行  最后一定会执行
    			lock.unlock();
    		}
    	}
    
    	// 可重入锁🔒  意思就是递归调用自己,锁可以释放出来
    	// synchronized == 天生就是 可重入锁🔒
    	// 如果是非重入锁🔒 ,就会自己把自己锁死
    	public synchronized void incr2(){
    		count++;
    		incr2();
    	}
    
    	public static void main(String[] args) {
    		LockDemo lockDemo = new LockDemo();
    	}
    
    }
    

锁的相关问题

  • 错误的加锁

    • 见上面的类锁和对象锁,当在实例化多个对象,而我们锁的又是对应类的实例对象时,该锁就是错误的加锁,起不到相应的效果
  • 死锁

    • 死锁的概念

      • 是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁。
      • 总结一下死锁形成的条件:
        1. 多操作线程(M >= 2),多资源(N >= 2),且操作者至少等于资源(M >= N)
        2. 多操作线程争夺资源的顺序不同
        3. 操作线程拿到资源后不释放
      • 学术化定义:
        1. 互斥条件:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。
        2. 请求和保持条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。
        3. 不剥夺条件:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。
        4. 环路等待条件:指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源。
    • 死锁的示例:

      package com.example.lib;
      
      public class deadSyncTest {
          static class Lock {
              public static Object lock1 = new Object();
              public static Object lock2 = new Object();
          }
      
          static class ThreadTest extends Thread {
              public boolean flag;
              public static int num = 0;
      
              public ThreadTest(boolean flag) {
                  this.flag = flag;
              }
      
              @Override
              public void run() {
                  //第一步:开始执行run
                  if (flag) {
                      while (num < 10000) {
                          //第二步:假设当前Thread1先抢到了锁,进入执行
                          synchronized (Lock.lock1) {  //使用了第一把锁
                              //第三步:此时CPU到期,Thread1停止执行
                              //第六步:假设当前Thread1又抢到了执行权,此时判断lock2是被Thread2使用了,无法持有lock2,阻塞;CPU时间到期,Thread1停止执行
                              synchronized (Lock.lock2) {  
                                  num++;
                                  System.out.println("this thread is : " + Thread.currentThread().getName() + ", num = " + num);
                                  System.out.println("---------------------------");
                              }
                          }
                      }
                  } else {
                      while (num < 10000) {
                          //第四步:假设此时Thread2抢到了CPU执行时间,开始执行
                          synchronized (Lock.lock2) { //使用了第二把锁
                              //第五步:此时CPU到期了,Thread2又停止执行了
                              //第七步:假设此时Thread2又抢到了CPU,此时判断lock1是被Thread1使用了,无法再持有lock1,阻塞;此时两把锁都被不同线程持有,无法解锁,死锁发生
                              synchronized (Lock.lock1) {
                                  num++;
                                  System.out.println("this thread is : " + Thread.currentThread().getName() + ", num = " + num);
                                  System.out.println("???????????????????????????");
                              }
                          }
                      }
                  }
              }
          }
      
          public static void main(String[] argd) {
              new ThreadTest(false).start();
              new ThreadTest(true).start();
      
          }
      
      }
      
    • 死锁的解决方案

      1. 内部通过顺序比较,确定拿锁的顺序;

        package com.example.lib;
        
        public class deadSyncTest {
            static class Lock {
                public static Object lock1 = new Object();
                public static Object lock2 = new Object();
            }
        
            static class ThreadTest extends Thread {
                public boolean flag;
                public static int num = 0;
        
                public ThreadTest(boolean flag) {
                    this.flag = flag;
                }
        
                @Override
                public void run() {
                    if (flag) {
                        while (num < 10000) {
                            synchronized (Lock.lock1) {  //使用了第一把锁
                                synchronized (Lock.lock2) {  //使用了第二把锁
                                    num++;
                                    System.out.println("this thread is : " + Thread.currentThread().getName() + ", num = " + num);
                                    System.out.println("---------------------------");
                                }
                            }
                        }
                    } else {
                        while (num < 10000) {
                            synchronized (Lock.lock1) { //使用第一把锁
                                synchronized (Lock.lock1) { //使用第二把锁
                                    num++;
                                    System.out.println("this thread is : " + Thread.currentThread().getName() + ", num = " + num);
                                    System.out.println("???????????????????????????");
                                }
                            }
                        }
                    }
                }
            }
        
            public static void main(String[] argd) {
                new ThreadTest(false).start();
                new ThreadTest(true).start();
        
            }
        
        }
        
      2. 采用尝试拿锁的机制。

        /**
         *类说明:演示尝试拿锁解决死锁
         */
        public class TryLock {
            private static Lock No13 = new ReentrantLock();//第一个锁
            private static Lock No14 = new ReentrantLock();//第二个锁
        
            //先尝试拿No13 锁,再尝试拿No14锁,No14锁没拿到,连同No13 锁一起释放掉
            private static void fisrtToSecond() throws InterruptedException {
                String threadName = Thread.currentThread().getName();
                Random r = new Random();
                while(true){
                    if(No13.tryLock()){ //如果能够拿到锁,就返回true,没有拿不到,就返回false
                        System.out.println(threadName
                                +" get 13");
                        try{
                            if(No14.tryLock()){
                                try{
                                    System.out.println(threadName
                                            +" get 14");
                                    System.out.println("fisrtToSecond do work------------");
                                    break;
                                }finally{ //finally中的代码一定会走
                                    No14.unlock();
                                }
                            }
                        }finally {
                            No13.unlock();
                        }
        
                    }
                    //Thread.sleep(r.nextInt(3));  //如果不加错峰延时,会造成活锁问题
                }
            }
        
            //先尝试拿No14锁,再尝试拿No13锁,No13锁没拿到,连同No14锁一起释放掉
            private static void SecondToFisrt() throws InterruptedException {
                String threadName = Thread.currentThread().getName();
                Random r = new Random();
                while(true){
                    if(No14.tryLock()){
                        System.out.println(threadName
                                +" get 14");
                        try{
                            if(No13.tryLock()){
                                try{
                                    System.out.println(threadName
                                            +" get 13");
                                    System.out.println("SecondToFisrt do work------------");
                                    break;
                                }finally{
                                    No13.unlock();
                                }
                            }
                        }finally {
                            No14.unlock();
                        }
        
                    }
                    //Thread.sleep(r.nextInt(3));
                }
            }
        
            private static class TestThread extends Thread{
        
                private String name;
        
                public TestThread(String name) {
                    this.name = name;
                }
        
                public void run(){
                    Thread.currentThread().setName(name);
                    try {
                        SecondToFisrt();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        
            public static void main(String[] args) {
                Thread.currentThread().setName("TestDeadLock");
                TestThread testThread = new TestThread("SubTestThread");
                testThread.start();
                try {
                    fisrtToSecond();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        
  • 活锁

    • 活锁的定义:
      • 两个线程在尝试拿锁的机制中,发生多个线程之间互相谦让,不断发生同一个线程总是拿到同一把锁,在尝试拿另一把锁时因为拿不到,而将本来已经持有的锁释放的过程。见上面tryLock代码示例
    • 活锁的解决方式
      • 每个线程休眠随机数,错开拿锁的时间
  • 线程饥饿

    • 低优先级的线程,总是拿不到执行时间

Volatile关键字

  • 作用:
    • 保证数据改变,可以立即可见,最轻量级的同步机制
    • 只能保证可见性,无法保证原子性
    • 适用于一写多读的场景
    • 抑制重排序

线程间的协作Wait()、notify()、notifyAll()

  • 标准范式

    //wait:
    sync(对象) {
        while (条件不满足) {
            对象.wait();
        }
        //业务逻辑
    }
    
    //notify/notifyall
    sync(对象) {
        //业务逻辑
        //do something
        对象.notify/notifyAll()
    }
    
    • 之所以需要加锁,是JDK的规定,如果不加锁,运行就会抛异常
    • wait()方法执行后,会对锁进行释放
    • notify/notifyAll()执行后,不会对锁进行释放,而是需要执行完代码块后,才会释放锁
    • wait()的逻辑,收到notify后,会从等待状态转变为就绪状态,重新竞争锁,当之前由wait()引起等待的线程抢到锁后,程序会从wait()之后的代码开始执行
  • notify()notifyAll()的区别

    • notify()
      • 随机唤醒相应对象上一个线程
        • 如果存在多个条件,使用notify()无法保证被唤醒的线程是条件满足的线程
    • notifyAll()
      • 唤醒相应对象上的所有线程

yield、sleep、wait、notify等方法对锁的影响

  1. yield()
    • 只是让出CPU的执行时间,不会释放锁
  2. sleep()
    • 不会释放锁
  3. wait()
    • 会释放锁,从wait()中苏醒时,会去重新竞争锁,竞争到锁之后会从wait()处开始继续执行
  4. notify()
    • 不会释放锁

等待超时模式实现一个连接池

/**
 *类说明:连接池的实现
 */
public class DBPool {
    private static LinkedList<Connection> pool = new LinkedList<Connection>();

    public DBPool(int initialSize) {
        if (initialSize > 0) {
            for (int i = 0; i < initialSize; i++) {
                pool.addLast(SqlConnectImpl.fetchConnection());
            }
        }
    }

    //释放连接池,每释放一个连接池,就需要通知相关的线程可以去连接了
    public void releaseConnection(Connection connection) {
        if (connection != null) {
            synchronized (pool) {
                pool.addLast(connection);
                pool.notifyAll();
            }
        }
    }

    // 在mills内无法获取到连接,将会返回null
    public Connection fetchConnection(long mills) throws InterruptedException {
        synchronized (pool) {
            //当超时为0,即不存在超时
            if (mills <= 0) {
                while (pool.isEmpty()) {
                    pool.wait();
                }
                return pool.removeFirst();
            } else {
                long feture = System.currentTimeMillis() + mills;
                long waittime = mills;
                while (pool.isEmpty()) {
                    pool.wait(waittime);
                    if (feture > System.currentTimeMillis()) {
                        waittime = feture - System.currentTimeMillis();
                    } else {
                        return null;
                    }
                }
                return pool.removeFirst();
            }
        }
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值