Java多线程

一.线程的生命周期

在这里插入图片描述

  • 新建状态:
    使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。

  • 就绪状态:
    当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。

  • 运行状态:
    如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。

  • 阻塞状态:
    如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:

    • 等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。

    • 同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。

    • 其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。

  • 死亡状态:
    一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。

二.创建多线程的三种方式

1.继承Thread类创建多线程

(1)首先放一个单线程例子作为比较
public class Example01 {
	public static void main(String[] args) {
		MyThread myThread = new MyThread();//创建MyThread类对象
		myThread.run();//调用run()方法
		while(true) {
			System.out.println("main方法在运行");
		}
	}
}
class MyThread{
	public void run() {
		while(true) {//死循环打印语句
			System.out.println("MyThread类的run()方法在运行");
		}
	}
}

在这里插入图片描述

(2)通过继承Thread类的方法来实现多线程
public class Example02 {
	public static void main(String[] args) {
		MyThread myThread = new MyThread();//创建MyThread类对象
		myThread.start();//调用run()方法
		while(true) {
			System.out.println("Main方法在运行");
		}
	}
}
class MyThread extends Thread{
	public void run() {
		while(true) {//死循环打印语句
			System.out.println("MyThread类的run()方法在运行");
		}
	}
}

在这里插入图片描述
       在类Example02中通过继承Thread类实现了多线程,但是这种方式有一定的局限性。因为Java中只支持单继承,一个类一旦继承了某个父类就无法再继承Thread类,比如学生类Student继承了Person类,就无法通过继承Thread类创建线程。
       为了克服这种弊端,Thread类提供了另外一个构造方法Thread(Runnable target),其中Runnable是一个接口,它只有一个run()方法。当通过Thread(Runnable target))构造方法创建线程对象时,只需为该方法传递一个实现了Runnable接口的实例对象,这样创建的线程将调用实现了Runnable接口的类中的run()方法作为运行代码,而不需要调用Thread类中的run()方法。

2.通过实现Runnable接口的方法来实现多线程

public class Example03 {
	public static void main(String[] args) {
		MyThread myThread = new MyThread();//创建MyThread类对象
		Thread thread = new Thread(myThread);//创建线程对象
		thread.start();//开启线程,执行线程中的run方法
		while(true) {
			System.out.println("Main方法在运行方法在执行");
		}

	}
}
//线程的任务类
class MyThread implements Runnable{
	@Override
	public void run() {//线程的代码段,当调用strat()方法时,线程从此处开始执行
		while(true) {
			System.out.println("MyThread类的run()方法在执行");
		}
	}
}

在这里插入图片描述

3.通过Callable和Future创建线程

  1. 创建 Callable 接口的实现类,并实现 call() 方法,该 call() 方法将作为线程执行体,并且有返回值。

  2. 创建 Callable 实现类的实例,使用 FutureTask 类来包装 Callable 对象,该 FutureTask 对象封装了该 Callable 对象的 call() 方法的返回值。

  3. 使用 FutureTask 对象作为 Thread 对象的 target 创建并启动新线程。

  4. 调用 FutureTask 对象的 get() 方法来获得子线程执行结束后的返回值。

public class CallableThreadTest implements Callable<Integer> {
    public static void main(String[] args)  
    {  
        CallableThreadTest ctt = new CallableThreadTest();  
        FutureTask<Integer> ft = new FutureTask<>(ctt);  
        for(int i = 0;i < 100;i++)  
        {  
            System.out.println(Thread.currentThread().getName()+" 的循环变量i的值"+i);  
            if(i==20)  
            {  
                new Thread(ft,"有返回值的线程").start();  
            }  
        }  
        try  
        {  
            System.out.println("子线程的返回值:"+ft.get());  
        } catch (InterruptedException e)  
        {  
            e.printStackTrace();  
        } catch (ExecutionException e)  
        {  
            e.printStackTrace();  
        }  
  
    }
    @Override  
    public Integer call() throws Exception  
    {  
        int i = 0;  
        for(;i<100;i++)  
        {  
            System.out.println(Thread.currentThread().getName()+" "+i);  
        }  
        return i;  
    }  
}

4.以上三种方式的对比分析

通过一个应用场景来分析,假设通过四个窗口售卖100张票,100张票为共享资源,窗口为线程。

public class Example04 {
	public static void main(String[] args) {
		// 创建四个线程对象
		new TicketWindow().start();
		new TicketWindow().start();
		new TicketWindow().start();
		new TicketWindow().start();
	}
}
class TicketWindow extends Thread {
	private int tickets = 100;
	public void run() {// 线程的代码段,当调用strat()方法时,线程从此处开始执行
		while (true) {
			if (tickets > 0) {
				Thread th = Thread.currentThread();// 获取当前run()方法的线程
				String th_name = th.getName();// 获取当前线程的名称
				System.out.println(th_name + "正在发售第" + tickets-- + "张票");
			}
		}
	}
}

![在这里插入图片描述](https://img-blog.csdnimg.cn/20210109003538203.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQxNTE1MjEw,size_16,color_FFFFFF,t_70)
可以看到每个窗口都有各自的100张票,通过继承Thread类实现多线程的这种方法,并没有实现共享资源。

public class Example05 {
	public static void main(String[] args) {
		TicketWindow task = new TicketWindow();//创建线程的任务类对象
		new Thread(task,"窗口1").start();//创建线程,并命名
		new Thread(task,"窗口2").start();
		new Thread(task,"窗口3").start();
		new Thread(task,"窗口4").start();
	}
}
class TicketWindow implements Runnable {
	private int tickets = 100;
	@Override
	public void run() {
		while (true) {
			if (tickets > 0) {
				Thread th = Thread.currentThread();// 获取当前run()方法的线程
				String th_name = th.getName();// 获取当前线程的名称
				System.out.println(th_name + "正在发售第" + tickets-- + "张票");
			}
		}
	}
}

在这里插入图片描述
通过实现Runnable接口和Callable接口实现多线程的方法,实现了资源的共享。
通过上面的两种结果可以看出,实现Runnable接口和Callable接口相对于继承Thread类来说,有如下显著的好处:
    1.适合多个相同程序代码的线程去处理同一个资源的情况,把线程同程序代码、数据有效的分离,很好的体现了面向对象的设计思想。
    2.可以避免由于Java的单继承带来的局限性。在开发中经常碰到这样一种情况,就是使用一个已经继承了某一个类的子类创建线程,由于一个类不能同时有两个父类,所以不能用继承Thread类的方式,那么就只能采用实现Runnable接口的方式。
事实上,大部分的多线程应用都会采用第二种方式,即实现Runnable接口。

三.线程的状态转换

![在这里插入图片描述](https://img-blog.csdnimg.cn/20210109213916788.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQxNTE1MjEw,size_16,color_FFFFFF,t_70
图中展示了线程各种状态的转换关系,箭头表示可转换的方向,其中,单箭头表示状态只能单向的转换,例如线程只能从新建状态转换到就绪状态,反之贝不能;双箭头表示两种状态可以互相转换,例如就绪状态和运行状态可以互相转换。通过一张图还不能完全描述清楚线程各状态之间的区别,接下来针对线程生命周期中的五种状态分别进行详细讲解,具体如下:

1.新建状态(New)

  • 创建一个线程对象后,该线程对象就处于新建状态,此时它不能运行,和其他Java对象一样,仅仅由Java虚拟机为其分配了内存,没有表现出任何线程的动态特征。

2.就绪状态(Runnable)

  • 当线程对象调用了start()方法后,该线程就进入就绪状态。处于就绪状态的线程位于线程队列中,此时它只是具备了运行的条件,能否获得CPU的使用权并开始运行,还需要等待系统的调度。

3.运行状态(Running)

  • 如果处于就绪状态的线程获得了CPU的使用权,并开始执行run()方法中的线程执行体,则该线程处于运行状态。一个线程启动后,它可能不会一直处于运行状态,当运行状态的线程使用完系统分配的时间后,系统就会剥夺该线程占用的CPU资源,让其他线程获得执行的机会。需要注意的是,只有处于就绪状态的线程才可能转换到运行状态。

4.阻塞状态(Blocked)

  • 一个正在执行的线程在某些特殊情况下,如被人为挂起或执行耗时的输入/输出操作时,会让出CPU的使用权并暂时中止自己的执行,进入阻塞状态。线程进入阻塞状态后,就不能进入排队队列。只有当引起阻塞的原因被消除后,线程才可以转入就绪状态。

下面就列举一下线程由运行状态转换成阻塞状态的原因,以及如何从阻塞状态转换成就绪状态。

  • 当线程试图获取某个对象的同步锁时,如果该锁被其他线程所持有,则当前线程会进入阻塞状态,如果:想从阻塞状态进入就绪状态必须得获取到其他线程所持有的锁。
  • 当线程调用了一个阻塞式的IO方法时,该线程就会进入阻塞状态,如果想进入就绪状态就必须要等到这个阻塞的Io方法返回。
  • 当线程调用了某个对象的wait()方法时,也会使线程进入阻塞状态,如果想进入就绪状态就需要使用notify()方法唤醒该线程。
  • 当线程调用了Thread的sleep(long millis)方法时,也会使线程进入阻塞状态,在这种情况下,只需等到线程睡眠的时间到了以后,线程就会自动进入就绪状态。
  • 当在一个线程中调用了另一个线程的join()方法时,会使当前线程进入阻塞状态,在这种情况下,需要等到新加入的线程运行结束后才会结束阻塞状态,进入就绪状态。

需要注意的是,线程从阻塞状态只能进入就绪状态,而不能直接进入运行状态,也就是说结束阻塞的线程需要重新进入可运行池中,等待系统的调度。

5.死亡状态(Terminated)

当线程调用stop()方法或run()方法正常执行完毕后,或者线程抛出一个未捕获的异常(Exception)、错误(Error),线程就进入死亡状态。一旦进入死亡状态,线程将不再拥有运行的资格,也不能再转换到其他状态。

四.线程的优先级

程序中的多个线程是并发执行的,某个线程若想被执行必须要得到CPU的使用权。Java虚拟机会按照特定的机制为程序中的每个线程分配CPU的使用权,这种机制被称作线程的调度。
在计算机中,线程调度有两种模型,分别是分时调度模型和抢占式调度模型。所谓分时调度模型是指让所有的线程轮流获得CPU的使用权,并且平均分配每个线程占用的CPU的时间片。抢占式调度模型是指让可运行池中优先级高的线程优先占用CPU,而对于优先级相同的线程,随机选择一个线程使其占用CPU,当它失去了CPU的使用权后,再随机选择其他线程获取CPU使用权。Java虚拟机默认采用抢占式调度模型,通常情况下程序员不需要去关心它,但在某些特定的需求下需要改变这种模式,由程序自己来控制CPU的调度。本节将围绕线程调度的相关知识进行详细地讲解。

public class Example06 {
	public static void main(String[] args) {
		//创建两个线程
		Thread minPriority = new Thread(new Task(),"优先级较低的线程");
		Thread maxPriority = new Thread(new Task(),"优先级较高的线程");
		minPriority.setPriority(Thread.MIN_PRIORITY);//设置线程的优先级为1
		maxPriority.setPriority(Thread.MAX_PRIORITY);//设置线程的优先级为10
		minPriority.start();
		maxPriority.start();
	}
}
//定义一个线程的任务类
class Task implements Runnable {
	@Override
	public void run() {
		for(int i = 0; i<10;i++) {
			System.out.println(Thread.currentThread().getName() +"正在输出"+i);
		}
	}
}

在这里插入图片描述

五.线程休眠

在前面已经讲过线程的优先级,优先级高的程序会先执行,而优先级低的程序会后执行。如果希望人为地控制线程,使正在执行的线程暂停,将CPU让给别的线程,这时可以使用静态方法sleep(long millis),该方法可以让当前正在执行的线程暂停一段时间,进入休眠等待状态。当前线程调用sleep(long millis)方法后,在指定时间(参数millis)内该线程是不会执行的,这样其他的线程就可以得到执行的机会了。
sleep(long millis)方法声明会抛出InterruptedException异常,因此在调用该方法时应该捕获异常,或者声明抛出该异常。

public class Example07 {
	public static void main(String[] args) throws Exception {
		//创建一个线程
		new Thread(new Task()).start();
		for(int i = 0; i<=10;i++) {
			if(i == 5) {
				Thread.sleep(2000);//当前main主线程 休眠2秒
			}else {
				Thread.sleep(500);
			}
			System.out.println("main主线程正在输出:"+i);
		}
	}
}

//定义一个线程的任务类
class Task implements Runnable {
	@Override
	public void run() {
		for (int i = 0; i < 10; i++) {
			try {
				if (i == 3) {
					Thread.sleep(2000);// 当前线程休眠2秒
				} else {
					Thread.sleep(500);
				}
				System.out.println("线程一正在输出" + i);
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
	}
}

在这里插入图片描述

六.线程让步

在篮球比赛中,我们经常会看到两队选手互相抢篮球,当某个选手抢到篮球后就可以拍一会,之后他会把篮球让出来,其他选手重新开始抢篮球,这个过程就相当于Java程序中的线程让步。所谓的线程让步是指正在执行的线程,在某些情况下将CPU资源让给其他线程执行。
线程让步可以通过yield()方法来实现,该方法和sleep()方法有点相似,都可以让当前正在运行的线程暂停,区别在于yield()方法不会阻塞该线程,它只是将线程转换成就绪状态,让系统的调度器重新调度一次。当某个线程调用yield()方法之后,只有与当前线程优先级相同或者更高的线程才能获得执行的机会。

public class Example08 {
	public static void main(String[] args) throws Exception {
		// 创建两个线程
		Thread t1 = new YieldThread("线程A");
		Thread t2 = new YieldThread("线程B");
		t1.start();
		t2.start();
	}
}
class YieldThread extends Thread {
	public YieldThread(String name) {
		super(name);// 调用父类的构造方法,完成线程名称的指定
	}
	public void run() {
		for (int i = 0; i < 6; i++) {
			System.out.println(Thread.currentThread().getName() + "---" + i);
			if (i == 3) {
				System.out.println(Thread.currentThread().getName() + "让步");
				Thread.yield();// 运行至此,线程让步
			}
		}
	}
}

在这里插入图片描述

七.线程插队

现实生活中经常能碰到“插队”的情况,同样,在Thread类中也提供了一个join()方法来实现这个“功能”。当在某个线程中调用其他线程的join()方法时,调用的线程将被阻塞,直到被join()方法加入的线程执行完成后它才会继续运行。

public class Example09 {
	public static void main(String[] args) throws Exception {
		// 创建线程
		Thread t = new Thread(new Task(), "线程一");
		t.start();
		for (int i = 1; i <= 6; i++) {
			System.out.println(Thread.currentThread().getName() + "输出" + i);
			if(i == 2) {
				System.out.println("插队开始");
				t.join();//调用join()方法
			}
			Thread.sleep(500);
		}
	}
}
class Task implements Runnable {
	@Override
	public void run() {
		for (int i = 0; i <= 6; i++) {
			System.out.println(Thread.currentThread().getName() + "输出" + i);
			try {
				Thread.sleep(500);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}
}

在这里插入图片描述

八.线程安全

前面小节讲解过多线程的并发执行可以提高程序的效率,但是,当多个线程去访问同一个资源时,也会引发一些安全问题。例如,当统计一个班级的学生数目时,如果有同学进进出出,则很难统计正确。为了解决这样的问题,需要实现多线程的同步,即限制某个资源在同一时刻只能被一个线程访问。接下来将详细讲解多线程中出现的问题以及如何使用同步来解决。

public class Example10 {
	public static void main(String[] args) {
		TicketWindow task = new TicketWindow();// 创建线程的任务类对象
		new Thread(task, "窗口1").start();// 创建线程,并命名
		new Thread(task, "窗口2").start();
		new Thread(task, "窗口3").start();
		new Thread(task, "窗口4").start();
	}
}
class TicketWindow implements Runnable {
	private int tickets = 10;
	@Override
	public void run() {
		while (tickets > 0) {
			try {
				Thread.sleep(10);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			System.out.println(Thread.currentThread().getName() + "正在发售第" + tickets-- + "张票");
		}
	}
}

在这里插入图片描述

九.同步代码块

从类Example10中,了解到线程安全问题其实就是由多个线程同时处理共享资源所导致的。要想解决类Example10中的线程安全问题,必须得保证下面用于处理共享资源的代码在任何时刻只能有一个线程访问。
在这里插入图片描述

为了实现这种限制,Java中提供了同步机制。当多个线程使用同一个共享资源时,可以将处理共享资源的代码放在一个使用synchronized关键字来修饰的代码块中,这个代码圩被称作同步代码块,其语法格式如下,
在这里插入图片描述

上面的代码中,lock是一个锁对象,它是同步代码块的关键。当某一个线程执行同步代码块时,其它线程将无法执行当前同步代码块,会发生阻塞,等当前线程执行完同步代码块后,所有的线程开始抢夺线程的执行权,抢到执行权的线程将进入同步代码块,执行其中的代码。循环往复,直到共享资源被处理完为止。这个过程就好比一个公用电话亭,只有前一个人打完电话出来后,后面的人才可以打。

public class Example11 {
	public static void main(String[] args) {
		TicketWindow task = new TicketWindow();// 创建线程的任务类对象
		new Thread(task, "窗口1").start();// 创建线程,并命名
		new Thread(task, "窗口2").start();
		new Thread(task, "窗口3").start();
		new Thread(task, "窗口4").start();
	}
}
class TicketWindow implements Runnable {
	private int tickets = 10;
	Object lock = new Object();
	@Override
	public void run() {
		while (true) {
			synchronized (lock) {// 锁必须是一个对象
				try {
					Thread.sleep(1000);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
				if (tickets > 0) {
					System.out.println(Thread.currentThread().getName() + "正在发售第" + tickets-- + "张票");
				} else {
					break;
				}
			}
		}
	}
}

在这里插入图片描述

十.同步方法

通过对类Example11的学习,了解到同步代码块可以有效解决线程的安全问题,当把共享资源的操作放在synchronized定义的区域内时,便为这些操作加了同步锁。在方法前面同样可以使用synchronized关键字来修饰,被修饰的方法为同步方法,它能实现和同步代码块同样的功能,具体语法格式如下:
在这里插入图片描述
被synchronized修饰的方法在某一时刻只允许一个线程访问,访问该方法的其他线程都会发生阻塞,直到当前线程访问完毕后,其他线程才有机会执行该方法。

public class Example12 {
	public static void main(String[] args) {
		TicketWindow task = new TicketWindow();// 创建线程的任务类对象
		new Thread(task, "窗口1").start();// 创建线程,并命名
		new Thread(task, "窗口2").start();
		new Thread(task, "窗口3").start();
		new Thread(task, "窗口4").start();
	}
}

class TicketWindow implements Runnable {
	private int tickets = 10;
	Object lock = new Object();

	@Override
	public void run() {
		while (true) {
			sendTicket();
		}
	}

	public synchronized void sendTicket() {
		try {
			Thread.sleep(100);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		if (tickets > 0) {
			System.out.println(Thread.currentThread().getName() + "正在发售第" + tickets-- + "张票");
		} else {
			System.exit(0);
		}
	}
}

在这里插入图片描述
思考:

  • 读者可能会有这样的疑问:同步代码块的锁是自己定义的任意类型的对象,那么同步方法是否也存在锁?如果有,它的锁是什么呢?答案是肯定的,同步方法也有锁,它的锁就是当前调用该方法的对象,也就是this指向的对象。这样做的好处是,同步方法被所有线程所共享,方法所在的对象相对于所有线程来说是唯一的,从而保证了锁的唯一性。当一个线程执行该方法时其他的线程就不能进入该方法中,直到这个线程执行完该方法为止。从而达到了线程同步的效果。
  • 有时候需要同步的方法是静态方法,静态方法不需要创建对象就可以直接用“类名.方法名()”的方式调用。这时候读者就会有一个疑问,如果不创建对象,静态同步方法的锁就不会是this ,那么静态同步方法的锁是什么? Java中静态方法的锁是该方法所在类的class对象,该对象在装载该类时自动创建,该对象可以直接用类名.class的方式获取。
  • 同步代码块和同步方法解决多线程问题有好处也有弊端。同步解决了多个线程同时访问共享数据时的线程安全问题,只要加上同一个锁,在同一时间内只能有一条线程执行。但是线程在执行同步代码时每次都会判断锁的状态,非常消耗资源,效率较低。

十一.死锁

在这里插入图片描述
结果可想而知,两个人都吃不到饭。这个例子中的中国人和美国人相当于不同的线程,筷子和刀叉就相当于锁。两个线程在运行时都在等待对方的锁,这样便造成了程序的停滞,这种现象称为死锁。

public class Example13 {
	public static void main(String[] args) {
		// 创建两个任务对象
		Task task1 = new Task(true);
		Task task2 = new Task(false);
		// 创建两个线程对象
		Thread t1 = new Thread(task1, "Chinese");
		Thread t2 = new Thread(task2, "American");
		t1.start();
		t2.start();
	}
}
class Task implements Runnable {
	static Object chopsticks = new Object();// 定义Object类型的锁对象 chopsticks 筷子
	static Object knifeAndFort = new Object();// 定义Object类型的锁对象 knifeAndFort 刀叉
	private boolean flag;
	// 构造方法
	public Task(boolean flag) {
		this.flag = flag;
	}
	@Override
	public void run() {
		// TODO Auto-generated method stub
		if (flag) {
			while (true) {
				synchronized (chopsticks) {// chopsticks锁对象的同步代码块
					System.out.println(Thread.currentThread().getName() + "------if------chopsticks");
					synchronized (knifeAndFort) {// knifeAndFort锁对象上的同步代码块
						System.out.println(Thread.currentThread().getName() + "------if------knifeAndFort");
					}
				}
			}
		} else {
			while (true) {
				synchronized (knifeAndFort) {// chopsticks锁对象的同步代码块
					System.out.println(Thread.currentThread().getName() + "------else------knifeAndFort");
					synchronized (chopsticks) {// knifeAndFort锁对象上的同步代码块
						System.out.println(Thread.currentThread().getName() + "------else------knifeAndFortchopsticks");
					}
				}
			}
		}
	}
}

在这里插入图片描述
如果两个锁对象,不加static,每个线程都会new出来chopsticks锁和knifeAndFort锁,他们就会无休止的执行下去,但当锁唯一的时候,就会出现死锁现象;

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值