第21章并发

1、程序中的所有事物在任意时刻都只能执行一个步骤。
21.1并发的多面性
1、并发编程令人困惑的主要原因:使用并发时需要解决的问题有多个,而实现并发的方式也有多种,并且在这两者之间没有明显的映射关系(而且通常只具有模糊的界线)。
21.1.1更快的执行
1、并发通常是提高运行在单处理器上的程序的性能。
2、在单处理器上运行的并发程序开销确实应该比该程序的所有部分都顺序执行的开销大,因为其中增加了所谓上下文切换的代价(从一个任务切换到另一个任务)。表面上看,将程序的所有部分当作单个的任务运行好像是开销小一点,并且可以节省上下文切换的代价。使这个问题变得有些不同的是阻塞。如果程序中的某个任务因为该线程控制范围之外的某些条件(通常是I/O)而导致不能继续执行,就说这个任务阻塞了。如果没有并发,则整个程序都将停止下来,直至外部条件发生变化。但是,如果使用并发来编写程序,那么当一个任务阻塞的时候,程序中其他任务还可以继续执行,因此这个程序可以保持继续向前执行。事实上,从性能的角度看,如果没有任务会阻塞,那么在单处理器机器上使用并发就没有任何意义。
3、实现并发最直接的方式是在操作系统级别使用进程。进程是运行在它自己的地址空间内的自包容的程序。多任务操作系统可以通过周期性地将CPU从一个进程切换到另一个进程,来实现同时运行多个进程(程序)、尽管这使得每个进程看起来在其执行过程中都是歇歇停停。
4、每个任务都作为进程在其自己的地址空间中执行,因此任务之间根本不可能互相干涉。
5、线程机制是在由执行程序表示的单一进程中创建任务。
21.1.2改进代码设计
21.2基本的线程机制
1、并发编程时我们可以将程序划分为多个分离的、独立运行的任务。通过使用多线程机制,这些独立任务(也被称为子任务)中的每一个都将由执行线程来驱动。一个线程就是在进程中一个单一的顺序控制流,因此,单个进程可以拥有多个并发执行的任务,但是程序使得每个任务都好像有其自己的CPU一样。其底层机制是切分CPU时间。
2、线程模型为编程带来了便利,简化了在单一程序中同时交织在一起的多个操作的处理。在使用线程时,CPU将轮流给每个任务分配其占用时间。每个任务都觉得自己在一直占用CPU,但事实上CPU时间是划分成片段分配给了所有任务。
3、线程的一大好处是可以使程序员从这个层次抽身出来,即代码不必知道它是运行在具有一个还是多个CPU的机器上。所以,使用线程机制是一种建立透明的、可扩展的程序的方法,如果程序运行太慢,为机器增添一个CPU就能很容易地加快程序的运行速度。多任务和多线程往往是使用多处理器系统的最合理方式。
21.2.1定义任务
1、

package com21;

/**
 * Created by Panda on 2018/5/22.
 */
public class ListOff implements Runnable{
    protected int countDown=10;
    private static int taskCount=0;
    private final int id=taskCount++;
    public ListOff() {
    }

    public ListOff(int countDown) {
        this.countDown = countDown;
    }
    public String status(){
        return "#"+id+"("+(countDown>0?countDown:"Liftoff")+"),";
    }

    @Override
    public void run() {
      while (countDown-->0){
          System.out.println(status());
          //在run()中对静态方法Thread.yield()的调用时对线程调度器的一种建议(Java线程机制的一部分,可以将
          //CPU从一个线程转移到另一个线程)
          Thread.yield();
      }
    }

  /*  public static void main(String[] args) {
        ListOff listOff = new ListOff();
        listOff.run();
    }*/
  public static void main(String[] args) {
      Thread thread = new Thread(new ListOff());
      thread.start();
  }
}

21.2.3使用Executor
1、Executor在客户端和任务执行之间提供了一个间接层;与客户端直接执行任务不同,这个中介对象将执行任务。Executor允许你管理一部任务的执行,而无须显示地管理线程的生命周期。

package com21;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * Created by Panda on 2018/5/22.
 */
public class CacheThreadPool {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i <5 ; i++) {
            executorService.execute(new ListOff());
            executorService.shutdown();
        }
    }
}

21.2.4从任务中产生返回值
1、Runnable是执行工作的独立任务,不返回任何值。如果希望任务在完成时能够返回一个值,可以实现Callable接口而不是Runnable接口。类型参数表示的是从方法call()中返回的值,并且必须使用ExecutorService.submit()方法调用它。
2、///callableDemo
21.2.2休眠
21.2.6优先级
1、线程的优先级将该线程的重要性传递给了调度器。尽管CPU处理现有线程集的顺序是不确定的,但是调度器将倾向于让优先权最高的线程先执行。然而, 这并不是意味着优先权较低的线程将得不到执行(也就是说,优先权不会导致死锁)。优先级较低的线程仅仅是执行的频率较低。
2、//simpelPriorities
21.2.7让步
1、yield():建议具有相同优先级的其他线程可以运行。
21.2.8后台线程
1、后台线程:是指在程序运行的时候在后台提供一种通用服务的线程,并且这种线程并不属于程序中不可或缺的部分。因此,当所有的非后台线程结束时,程序也就终止了,同时会杀死进程中的所有后台线程。反过来说,只要有任何非后台线程还在运行,程序就不会终止。
2、

package com21;

import java.util.concurrent.TimeUnit;

/**
 * Created by Panda on 2018/5/22.
 */
public class SimpleDaemons implements Runnable {

    @Override
    public void run() {
        try{
            while (true){
                TimeUnit.MILLISECONDS.sleep(100);
                System.out.println(Thread.currentThread()+" "+this);
            }
        }catch (InterruptedException e){
            System.out.println("sleep() interrupted");
        }
    }

    public static void main(String[] args) throws Exception{
        for (int i = 0; i < 10; i++) {
            Thread daemon = new Thread(new SimpleDaemons());
            daemon.setDaemon(true);
            daemon.start();
        }
        System.out.println("all daemons started");
        TimeUnit.MILLISECONDS.sleep(175);
    }
}

3、

package com21;

import java.util.concurrent.TimeUnit;

/**
 * Created by Panda on 2018/5/22.
 */
class ADaemon implements Runnable{
    @Override
    public void run() {
        try{
            System.out.println("Starting ADaemon");
            TimeUnit.SECONDS.sleep(1);
        }catch (InterruptedException e){
            System.out.println("Exiting via InterruptedException");
        }finally {
            System.out.println("This should always run()?");
        }
    }
}
public class DaemosDontRunFinally {
    public static void main(String[] args) {
        Thread thread = new Thread(new ADaemon());
        thread.setDaemon(true);
        thread.start();
    }
    //finally 不会执行。当注释掉setDaemon()时候,finally就会执行。
    //当最后一个非后台线程终止时候,后台线程会“突然”终止。因为一旦main()退出,JVM就会立即关闭所有的后台线程
    //而不会有任何你希望出现的确认形式
}

21.2.9编码的变体

package com21;

import java.util.concurrent.TimeUnit;

/**
 * Created by Panda on 2018/5/22.
 */
public class SimpleDaemons implements Runnable {

    @Override
    public void run() {
        try{
            while (true){
                TimeUnit.MILLISECONDS.sleep(100);
                System.out.println(Thread.currentThread()+" "+this);
            }
        }catch (InterruptedException e){
            System.out.println("sleep() interrupted");
        }
    }

    public static void main(String[] args) throws Exception{
        for (int i = 0; i < 10; i++) {
            Thread daemon = new Thread(new SimpleDaemons());
            daemon.setDaemon(true);
            daemon.start();
        }
        System.out.println("all daemons started");
        TimeUnit.MILLISECONDS.sleep(175);
    }
}
package com21;

/**
 * Created by Panda on 2018/5/22.
 */
public class SimpleManaged implements Runnable {
    private int count=5;
    private Thread thread = new Thread(this);

    public SimpleManaged() {
        thread.start();
    }
    public String toString(){
        return  Thread.currentThread().getName()+"("+count+")";
    }

    @Override
    public void run() {
     while (true){
         System.out.println(this);
         if(--count==0){
             return;
         }
     }
    }

    public static void main(String[] args) {
        for (int i = 0; i <5 ; i++) {
            new SimpleManaged();
        }
    }
}

21.2.10术语
21.2.11加入一个线程
1、一个线程可以在其他线程之上调用join()方法,其效果是等待一段时间直到第二个线程结束才继续执行。如果某个线程在另一个线程t上调用t.join(),此线程将被挂起,直到目标线程t结束才恢复(即t.isAlive()返回为假)
2、调用join()时带上一个超时参数(单位可以是毫秒,或者毫秒和纳秒),这样如果目标线程在这段时间到期时还没有结束的话,join()方法总能返回。
3、join()方法的调用可以被中断,做法是在调用线程上调用interrupt()方法,这时候需要使用try-catch子句。

package com21;

/**
 * Created by Panda on 2018/5/22.
 */
class Sleeper extends Thread{
    private int duration;
    public Sleeper(String name,int sleepTime){
        super(name);
        duration=sleepTime;
        start();
    }
    public void run(){
        try{
            sleep(duration);
        }catch (InterruptedException e){
            System.out.println(getName()+" was interrupted. "+"isInterrupted(): "+isInterrupted());
            return;
        }
        System.out.println(getName()+" has awakened");
    }
}
class Joiner extends Thread{
    private Sleeper sleeper;

    public Joiner(String name,Sleeper sleeper) {
        super(name);
        this.sleeper = sleeper;
        start();
    }
    public void run(){
        try{
            sleeper.join();
        }catch (InterruptedException e){
            System.out.println("Interrupted");
        }
        System.out.println(getName()+" join completed");
    }
}
public class Joining {
    public static void main(String[] args) {
        Sleeper sleeper = new Sleeper("Sleepy",1500);
        Sleeper sleeper1 = new Sleeper("Grumpy",1500);
        Joiner joiner = new Joiner("Dopey",sleeper);
        Joiner joiner1 = new Joiner("Doc",sleeper1);
        sleeper1.interrupt();
    }
    /**
     * Grumpy was interrupted. isInterrupted(): false
     Doc join completed
     Sleepy has awakened
     Dopey join completed
     */
}

21.2.13线程组
1、线程组持有一个线程集合。
21.2.14捕获异常
1、由于线程的本质特性,使得不能捕获从线程中逃逸的异常。一旦异常逃出任务的run()方法,就会向外传播到控制台。
2

package com21;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;

/**
 * Created by Panda on 2018/5/23.
 */
class ExceptionThread2 implements Runnable{
    @Override
    public void run() {
        Thread thread = Thread.currentThread();
        System.out.println("run() by"+thread);
        System.out.println("eh= "+thread.getUncaughtExceptionHandler());
        throw new RuntimeException();
    }
}
class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler{
    @Override
    public void uncaughtException(Thread t, Throwable e) {
        System.out.println("caught "+e);
    }
}

class HandlerThreadFactory implements ThreadFactory{
    @Override
    public Thread newThread(Runnable r) {
        System.out.println(this + " creating new Thread");
        Thread thread = new Thread(r);
        System.out.println("created "+thread);
        thread.setUncaughtExceptionHandler(new MyUncaughtExceptionHandler());
        System.out.println("eh= "+thread.getUncaughtExceptionHandler());
        return thread;
    }
}

public class CaptureUncaughtException {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool(new HandlerThreadFactory());
        executorService.execute(new ExceptionThread2());
    }
    /**
     * com21.HandlerThreadFactory@1540e19d creating new Thread
     created Thread[Thread-0,5,main]
     eh= com21.MyUncaughtExceptionHandler@677327b6
     run() byThread[Thread-0,5,main]
     eh= com21.MyUncaughtExceptionHandler@677327b6
     com21.HandlerThreadFactory@1540e19d creating new Thread
     created Thread[Thread-1,5,main]
     eh= com21.MyUncaughtExceptionHandler@5c74f769
     caught java.lang.RuntimeException
     */
}

21.3 共享受限资源
21.3.1不正确地访问资源
2.3.2解决共享资源竞争
1、基本上所有的并发模式在解决线程冲突问题的时候,都是采用序列化访问共享资源的方案。这意味着在给定时刻只允许一个任务访问共享资源。通常这是通过在代码前面加上一条锁语句来实现的,这就使得在一段时间内只允许一个任务可以运行这段代码。因为锁语句产生了一种互相排斥的效果,所以这种机制常常称为互斥量。
2、一个任务可以多次获得锁对象的锁。如果一个方法在同一个对象上调用了第二个方法,后者又调用了同一个对象上的另一个方法,就会发生这种情况。JVN负责跟踪对象呗加锁的次数。如果一个对象被加锁(即锁被完全释放),其计数变为0.在任务第一次给对象加锁的时候,计数变为1.每当这个相同的任务在这个对象上获得锁时,计数都会递增。显然,只有首先获得了锁的任务才能允许继续获取多个锁。每当任务离开一个synchronized方法,计数递减,当计数为零的时候,锁被完全释放,此时别的任务就可以使用此资源。
3、使用显示的Lock对象。

package com21;



import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * Created by Panda on 2018/5/23.
 */
public class MutexEventGenerator extends IntGenerator{
    private int currentEvenValue=0;
    private Lock lock = new ReentrantLock();
    @Override
    public int next() {
        lock.lock();
        try{
            ++currentEvenValue;
            Thread.yield();
            ++currentEvenValue;
            return currentEvenValue;
        }finally {
          lock.unlock();
        }

    }

    public static void main(String[] args) {
        EvenChecker.test(new MutexEventGenerator());
    }
}

21.3.3原子性与易变性
1、原子性可以应用于除long和double之外的所有基本类型之上的“简单操作”。对于读取和写入除long和double之外的基本类型变量这样的操作,可以保证它们会被当做不可分(原子)的操作来操作内存。
21.3.4原子类

package com21;

import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * Created by Panda on 2018/5/23.
 */
public class AtomicIntegerTest implements Runnable {
    private AtomicInteger integer = new AtomicInteger(0);
    public int getValue(){
        return integer.get();
    }
    public void evenIncrement(){integer.addAndGet(2);}
    @Override
    public void run() {
        while (true){
            evenIncrement();
        }
    }

    public static void main(String[] args) {
        new Timer().schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("Aborting");
                System.exit(0);
            }
        },5000);
        ExecutorService executorService = Executors.newCachedThreadPool();
        AtomicIntegerTest atomicIntegerTest = new AtomicIntegerTest();
        executorService.execute(atomicIntegerTest);
        while (true){
            int val = atomicIntegerTest.getValue();
            if(val%2!=0){
                System.out.println(val);
                System.exit(0);
            }
        }
    }
}

21.3.5临界区
1、希望防止多个线程同时访问方法内部的部分代码而不是防止访问整个方法。通过这种方式分离出来的代码段被称为临界区,它也使用synchronized关键字建立。这里synchronized被用来指定某个对象,此对象的锁被用来对花括号内的代码进行同步控制。
2、通过使用同步控制块,而不是对整个方法进行同步控制,可以使多个任务访问对象的时间性能显著提高。
21.3.6在其他对象上同步
1、synchronized块必须给定一个在其上进行同步的对象,并且最合理的方式是,使用其方法正在被调用的当前对象:synchronized(this)。在这种方式中,如果获得了synchronized块上的锁,那么该对象其他的synchronized方法和临界区就不能被调用了。因此,如果在this上同步,临界区的效果就会直接缩小在同步的范围内。
21.3.7线程本地存储
1、线程本地存储是一种自动化机制,可以为使用相同变量的每个不同的线程都创建不同的存储。
2、

package com21;

import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/**
 * Created by Panda on 2018/5/23.
 */
//ThreadLocal 对象通常当作静态域存储。在创建ThreadLocal时,只能通过get()和set()方法来访问该对象的内容
    //get()方法将返回与其线程相关联的对象的副本
    //set()方法会将参数插入到为期线程存储的对象中,并返回存储中原有的对象。
class Accessor implements Runnable{
    private final int id;

    public Accessor(int id) {
        this.id = id;
    }

    @Override
    public void run() {
        while (!Thread.currentThread().isInterrupted()){
            ThreadLocalVariableHolder.increment();
            System.out.println(this);
            Thread.yield();
        }
    }
    public String toString(){
        return "#"+id+":"+ThreadLocalVariableHolder.get();
    }
}
public class ThreadLocalVariableHolder {
    private static ThreadLocal<Integer> value=new ThreadLocal<Integer>(){
      private Random random = new Random(47);
      protected synchronized Integer initialValue(){
          return random.nextInt(10000);
      }
    };
    public static void increment(){
        value.set(value.get()+1);
    }
    public static int get(){return value.get();}

    public static void main(String[] args) throws Exception{
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i <5 ; i++) {
            executorService.execute(new Accessor(i));
        }
        TimeUnit.SECONDS.sleep(3);
        executorService.shutdown();
    }

}

21.4终结任务
21.4.1装饰性花园
21.4.2在阻塞时终结
1、线程状态:
①新建(new):当线程被创建时,它只会短暂地处于这种状态。此时它已经分配了必需的系统资源,并执行了初始化。此刻线程已经有资格获得CPU时间了,之后调度器将把这个线程转变为可运行状态或阻塞状态。
②就绪(Runnable):在这种状态下,只要调度器把时间片分配给线程,线程就可以运行。也就是说,在任意时刻,线程可以运行也可以不运行。只要调度器能分配时间片给线程,它就可以运行,这不同于死亡和阻塞状态。
③阻塞(Blocked):线程能够运行,但有某个条件阻止它的运行。当线程处于阻塞状态时,调度器将忽略线程,不会分配给线程任何CPU时间。直到线程重新进入了就绪状态,它才有可能执行操作。
④死亡(Dead):处于死亡或终止状态的线程将不再是可调度的,并且再也不会得到CPU时间,它的任务已结束,或不再是可运行的。任务死亡的通常方式是从run()方法返回,但是任务的线程还可以被中断。
2、进入阻塞状态:
一个任务进入阻塞状态,可能有如下原因:
①通过调用sleep(milliseconds)使任务进入休眠状态,在这种情况下,任务在指定的时间内不会运行。
②通过调用wait()使线程挂起。直到线程得到了notify()或notifyAll()消息,线程才会进入就绪状态。
③任务在等待某个输入/输出完成。
④任务试图在某个对象上调用其他同步控制方法,但是对象锁不可用,因为另一个任务已经获取了这个锁。
21.4.3中断
1、Thread类包含interrupt()方法,因此可以终止被阻塞的任务,这个方法将设置线程的中断状态。如果一个线程已经被阻塞,或者试图执行一个阻塞操作,那么设置这个线程的中断状态将抛出InterruptedException。当掏出该异常或者该任务调用Thread.interrupted()时,中断状态将被复位。
2、

package com21;

import java.io.IOException;
import java.io.InputStream;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

/**
 * Created by Panda on 2018/5/23.
 */
class SleepBlocked implements Runnable{
    @Override
    public void run() {
        try{
            TimeUnit.SECONDS.sleep(100);
        }catch (InterruptedException e){
            System.out.println("interruptedException");
        }
        System.out.println("Exiting SleepBlocked.run()");
    }
}
class IOBlocked implements Runnable{
    private InputStream inputStream;

    public IOBlocked(InputStream inputStream) {
        this.inputStream = inputStream;
    }

    @Override
    public void run() {
        try{
            System.out.println("waiting for read() : ");
            inputStream.read();
        }catch (IOException e){
            if(Thread.currentThread().isInterrupted()){
                System.out.println("Interrupted from blocked I/O");
            }else {
                throw  new RuntimeException(e);
            }
        }
        System.out.println("Exiting IOBlocked.run()");
    }
}

class SynchronizedBlocked implements Runnable{
    public synchronized void f(){
        while (true) Thread.yield();
    }
    public SynchronizedBlocked(){
        new Thread(){
            public void run(){
                f();
            }
        }.start();
    }

    @Override
    public void run() {
        System.out.println("Trying to call f()");
        f();
        System.out.println("Exiting SynchronizedBlocked.run()");
    }
}
public class Interrupting {
    private static ExecutorService executorService= Executors.newCachedThreadPool();
    static void test(Runnable t) throws InterruptedException{
        Future<?> f=executorService.submit(t);
        TimeUnit.MILLISECONDS.sleep(100);
        System.out.println("Interrupting  "+t.getClass().getName());
        f.cancel(true);

        System.out.println("Interrupt sent to "+t.getClass().getName() );
    }

    public static void main(String[] args) throws Exception{
        test(new SleepBlocked());
        test(new IOBlocked(System.in));
        test(new SynchronizedBlocked());
        TimeUnit.SECONDS.sleep(3);
        System.out.println("Aborting with System.exit(0)");
        System.exit(0);
    }
    /**
     * Interrupting  com21.SleepBlocked
     Interrupt sent to com21.SleepBlocked
     interruptedException
     Exiting SleepBlocked.run()
     waiting for read() :
     Interrupting  com21.IOBlocked
     Interrupt sent to com21.IOBlocked
     Trying to call f()
     Interrupting  com21.SynchronizedBlocked
     Interrupt sent to com21.SynchronizedBlocked
     Aborting with System.exit(0)
     */

}

3、被互斥所阻塞。如果尝试着在一个对象上调用其synchronized方法,而这个对象的锁已经被其他任务获取,那么调用任务将被挂起(阻塞),直至这个锁可获得。
4、

package com21;

/**
 * Created by Panda on 2018/5/23.
 */
//尝试在一个对象上调用其synchronized方法,而这个对象的锁已经被其他任务获得,那么调用任务将被挂起(阻塞)
    //直至这个锁可获得。
    //同一个互斥如何能被同一个任务多次获得。
public class MultiLock {
    public synchronized void f1(int count){
        if(count-->0){
            System.out.println("f1() calling f2() with count "+count);
            f2(count);
        }
    }
    public synchronized void f2(int count){
        if(count-->0){
            System.out.println("f2() calling f1() count "+count);
            f1(count);
        }
    }

    public static void main(String[] args) {
        final MultiLock multiLock=new MultiLock();
        new Thread(){
            public void run(){
                multiLock.f1(10);
            }
        }.start();
    }

}
package com21;

import org.omg.CORBA.TIMEOUT;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * Created by Panda on 2018/5/23.
 */
class BlockedMutex{
    private Lock lock = new ReentrantLock();
    public BlockedMutex(){
        lock.lock();
    }
    public void f(){
        try{
            lock.lockInterruptibly();
            System.out.println("lock acquired in f()");
        }catch (InterruptedException e){
            System.out.println("Interrupted from lock acquisition in f()");
        }
    }
}
class Blocked2 implements Runnable{
    BlockedMutex blockedMutex = new BlockedMutex();
    @Override
    public void run() {
        System.out.println("Waiting for f() in BlockedMutex");
        blockedMutex.f();
        System.out.println("Broken out of blocked call");
    }
}
public class Interrupting2 {
    public static void main(String[] args) throws Exception {
        Thread thread = new Thread(new Blocked2());
        thread.start();
        TimeUnit.SECONDS.sleep(1);
        thread.isInterrupted();
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值