JUC并发与源码分析

多线程相关概念

一把锁、两个并、三个程

六个线程状态

同步锁

并发、并行

进程、线程、管程

管程相当于锁(synchronized监视器)

执行线程需要先获得管程才能进入synchronized的代码块

线程

java的线程区分为用户线程和守护线程

一般情况下都是使用的用户线程

守护线程在全部用户线程结束后结束

CompletableFuture

Future接口定义了异步的操作方法,比如异步方法的结果,取消异步行为等等

Attempts to cancel execution of this task. This attempt will fail if the task has already completed, has already been cancelled, or could not be cancelled for some other reason. If successful, and this task has not started when cancel is called, this task should never run. If the task has already started, then the mayInterruptIfRunning parameter determines whether the thread executing this task should be interrupted in an attempt to stop the task.
After this method returns, subsequent calls to isDone will always return true. Subsequent calls to isCancelled will always return true if this method returned true.
Params:
mayInterruptIfRunning – true if the thread executing this task should be interrupted; otherwise, in-progress tasks are allowed to complete
Returns:
false if the task could not be cancelled, typically because it has already completed normally; true otherwise

FutureTask

异步/有返回值/多线程

public class CompletableFutureDemo
{
    public static void main(String[] args) throws ExecutionException, InterruptedException
    {
        FutureTask<String> futureTask = new FutureTask<>(new MyThread());

        Thread t1 = new Thread(futureTask,"t1");
        t1.start();

        System.out.println(futureTask.get());
    }
}

class MyThread implements Callable<String>
{
    @Override
    public String call() throws Exception
    {
        System.out.println("-----come in call() " );
        return "hello Callable";
    }
}

有时候使用异步可以提高程序执行效率

因为线程是CPU运算的最小单位,可以充分发挥多核CPU的优势

缺点

线程阻塞

futureTask的get()方法,可以获得异步的运行结果,但是如果futureTask没有运算完成的时候执行get方法会导致线程阻塞,所以一般情况下都把get放到最后面

还可以设置futureTask的等待时间,如果超过这个时间就会强制停止异步线程,通过这个方法额可以减少线程阻塞,get(3,TimeUnit.Second) //停顿3s


线程空转

 在生产环境中一般都会用while不断寻味futureTask是否完成任务

while{
    if(futureTask.isDone()){

        //完成任务
        break;
        
    {

}

CompletableFuture可以解决这个问题

Future能干的,CompletableFuture都能干

CompletationStage接口

代表异步运行的某一个阶段

四大静态方法

package com.bilibili.juc.cf;

import java.util.concurrent.*;

/**
 * @auther zzyy
 * @create 2022-01-16 16:27
 */
public class CompletableFutureBuildDemo
{
    public static void main(String[] args) throws ExecutionException, InterruptedException
    {
        ExecutorService threadPool = Executors.newFixedThreadPool(3);
        
        //无返回值
        
        
//        CompletableFuture<Void> completableFuture = CompletableFuture.runAsync(() -> {
//            System.out.println(Thread.currentThread().getName());
//            //暂停几秒钟线程
//            try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
//        },threadPool);
//
//        System.out.println(completableFuture.get());
        
        //有返回值
        CompletableFuture<String> completableFuture = CompletableFuture.supplyAsync(() -> {
            System.out.println(Thread.currentThread().getName());
            //暂停几秒钟线程
            try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
            return "hello supplyAsync";
        },threadPool);
        System.out.println(completableFuture.get());


        threadPool.shutdown();
    }
}

相关函数

java多线程的锁


悲观锁

synchronized 和 Lock都属于悲观锁

乐观锁

版本号机制 和 CAS算法

synchronized

练习类

class Phone //资源类
{
    public static synchronized void sendEmail()
    {
        try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
        System.out.println("-----sendEmail");
    }

    public synchronized void sendSMS()
    {
        System.out.println("-----sendSMS");
    }

    public void hello()
    {
        System.out.println("-------hello");
    }
}

public class Lock8Demo
{
    public static void main(String[] args)//一切程序的入口
    {
        Phone phone = new Phone();
        Phone phone2 = new Phone();

        new Thread(() -> {
            phone.sendEmail();
        },"a").start();

        //暂停毫秒,保证a线程先启动
        try { TimeUnit.MILLISECONDS.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); }

        new Thread(() -> {
            //phone.sendSMS();
            //phone.hello();
            phone2.sendSMS();
        },"b").start();
    }
}

/**
* 题目:谈谈你对多线程锁的理解,8锁案例说明
* 口诀:线程 操作 资源类
* 8锁案例说明:
* 1 标准访问有ab两个线程,请问先打印邮件还是短信
* 2 sendEmail方法中加入暂停3秒钟,请问先打印邮件还是短信
* 3 添加一个普通的hello方法,请问先打印邮件还是hello
* 4 有两部手机,请问先打印邮件还是短信
* 5 有两个静态同步方法,有1部手机,请问先打印邮件还是短信
* 6 有两个静态同步方法,有2部手机,请问先打印邮件还是短信
* 7 有1个静态同步方法,有1个普通同步方法,有1部手机,请问先打印邮件还是短信
* 8 有1个静态同步方法,有1个普通同步方法,有2部手机,请问先打印邮件还是短信
*
* 笔记总结:
* 1-2
* * * 一个对象里面如果有多个synchronized方法,某一个时刻内,只要一个线程去调用其中的一个synchronized方法了,
* * * 其它的线程都只能等待,换句话说,某一个时刻内,只能有唯一的一个线程去访问这些synchronized方法
* * * 锁的是当前对象this,被锁定后,其它的线程都不能进入到当前对象的其它的synchronized方法
* 3-4
* * 加个普通方法后发现和同步锁无关
* * 换成两个对象后,不是同一把锁了,情况立刻变化。
*
* 5-6 都换成静态同步方法后,情况又变化
* 三种 synchronized 锁的内容有一些差别:
* 对于普通同步方法,锁的是当前实例对象,通常指this,具体的一部部手机,所有的普通同步方法用的都是同一把锁——>实例对象本身,
* 对于静态同步方法,锁的是当前类的Class对象,如Phone.class唯一的一个模板
* 对于同步方法块,锁的是 synchronized 括号内的对象
*
* * 7-8
* * 当一个线程试图访问同步代码时它首先必须得到锁,正常退出或抛出异常时必须释放锁。
* * *
* * * 所有的普通同步方法用的都是同一把锁——实例对象本身,就是new出来的具体实例对象本身,本类this
* * * 也就是说如果一个实例对象的普通同步方法获取锁后,该实例对象的其他普通同步方法必须等待获取锁的方法释放锁后才能获取锁。
* * *
* * * 所有的静态同步方法用的也是同一把锁——类对象本身,就是我们说过的唯一模板Class
* * * 具体实例对象this和唯一模板Class,这两把锁是两个不同的对象,所以静态同步方法与普通同步方法之间是不会有竞态条件的
* * * 但是一旦一个静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁后才能获取锁。
*/

普通方法锁的是对象

静态方法锁的是类

同步代码块锁的是括号内的对象

编码分析

使用monitor(管程)进行监视

两次eonitorexit是为什么呢?

第一次正常退出

第二次异常退出

为了避免死锁

一定是一个enter对应两个exit吗?

一般情况下是的,除非估计写的有问题(主动在同步代码块中抛出异常)

flags声明了该方法的标识符,如果有ACC_SYNCHRONIZED 表示该方法是一个同步方法

为什么任何对象都可以称为一个锁

Monitor可以理解为一个同步工具或一种同步机制,通常被描述为一个对象。每一个Java对象就有一把看不见的锁,称为内部锁或者Monitor锁。

通常所说的对象的内置锁,是对象头Mark Word中的重量级锁指针指向的monitor对象,该对象是在HotSpot底层C++语言编写的(openjdk里面看),简单看一下代码:

公平锁和非公平锁

公平锁,雨露均沾

非公平锁,进行抢夺

Lock lock = new ReentrantLock();

ReentrantLock默认就是非公平锁

Lock lock = new ReentrantLock(true);

设置为公平锁

可重入锁

递归调用时,避免死锁

 private static void reEntryM1()
    {
        final Object object = new Object();

        new Thread(() -> {
            synchronized (object){
                System.out.println(Thread.currentThread().getName()+"\t ----外层调用");
                synchronized (object){
                    System.out.println(Thread.currentThread().getName()+"\t ----中层调用");
                    synchronized (object){
                        System.out.println(Thread.currentThread().getName()+"\t ----内层调用");
                    }
                }
            }
        },"t1").start();
    }

synchronized默认就是可重入锁 隐式可重入锁(不需要释放)

ReentrantLock()也是可重入锁 显式重入锁(需要释放)

如何实现可重入锁

小结

线程中断

如何停止运行中的线程?

如何中断运行中的线程?

若要中断运行中的线程需要显式的调用interupt方法

每个线程中有个中断标识位,ture表示未中断,false表示已经中断

一个线程不应该由其他线程进行中断或者停止应该由自己进行中断或者停止

三种停止线程的方法

volatile变量

AutomicBoolean变量

线程interrupt方法中断线程

作者:Intopass
链接:https://www.zhihu.com/question/41048032/answer/89431513
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
 

一个线程不应该由其他线程来强制中断或停止,而是应该由线程自己自行停止。

所以,Thread.stop, Thread.suspend, Thread.resume 都已经被废弃了。

而 Thread.interrupt 的作用其实也不是中断线程,而是「通知线程应该中断了」,

具体到底中断还是继续运行,应该由被通知的线程自己处理。

具体来说,当对一个线程,调用 interrupt() 时,

① 如果线程处于被阻塞状态(例如处于sleep, wait, join 等状态),那么线程将立即退出被阻塞状态,并抛出一个InterruptedException异常。仅此而已。

② 如果线程处于正常活动状态,那么会将该线程的中断标志设置为 true,仅此而已。被设置中断标志的线程将继续正常运行,不受影响。

interrupt() 并不能真正的中断线程,需要被调用的线程自己进行配合才行。

也就是说,一个线程如果有被中断的需求,那么就可以这样做。

① 在正常运行任务时,经常检查本线程的中断标志位,如果被设置了中断标志就自行停止线程。

② 在调用阻塞方法时正确处理InterruptedException异常。(例如,catch异常后就结束线程。)

Thread thread = new Thread(() -> {
    while (!Thread.interrupted()) {
        // do more work.
    }
});
thread.start();

// 一段时间以后
thread.interrupt();

具体到你的问题,Thread.interrupted()清除标志位是为了下次继续检测标志位。

如果一个线程被设置中断标志后,选择结束线程那么自然不存在下次的问题,

而如果一个线程被设置中断标识后,进行了一些处理后选择继续进行任务,

而且这个任务也是需要被中断的,那么当然需要清除标志位了。

  1. isInterrupted(): 这个方法是一个实例方法,可以在一个线程对象上调用,用于检查线程的中断状态。它不会清除线程的中断状态。如果线程被中断,isInterrupted() 返回 true,否则返回 false,但不会改变线程的中断状态。

  2. Thread.interrupted(): 这是一个静态方法,用于检查当前执行线程是否被中断。与isInterrupted() 不同,它会清除线程的中断状态,将中断状态重新设置为 false。如果当前线程被中断,Thread.interrupted() 返回 true 并清除中断状态;如果当前线程没有被中断,它返回 false

所以,Thread.interrupted() 方法会清除线程的中断状态,而isInterrupted() 方法只会检查中断状态而不清除它。这两个方法的不同之处在于是否清除中断状态。我希望这一点更加清楚了。

interrupt不能向wait join sleep 状态的线程发送中断请求,

如果发送了,将会把中断状态清除true->false,取消中断。

需要在catch代码块中在设置一次中断状态

LockSupport

wait和notify

wait等待

notify唤醒

异常1:notify放在wait前面

异常2:wait和notify不在同步代码块(或者lock代码块里面)里面

park和unpark

LockSupport使用了一个permit的一个概念,做到线程阻塞和唤醒的

park和unpark解决了上面两个问题

异常1:notify放在wait前面

异常2:wait和notify不在同步代码块(或者lock代码块里面)里面

许可证的最大数量是1

调用一次unpark增加凭证,但是凭证最大就是1

但是调用几次park就需要消耗几次凭证

JMM三定理

原子性 有序性 可见性

可见性

这种情况下会造成线程脏读

有序性

代码的顺序在多线程高并发场景下不一定是执行引擎的执行顺序

原子性

原子性的操作是不可被中断的一个或一系列操作。

个人理解,严格的原子性的操作,其他线程获取操作的变量时,只能获取操作前的变量值和操作后的变量值,不能获取到操作过程中的中间值,在操作过程中其他操作需要获取变量值,需要进入阻塞状态等待操作结束。

happends-before

我理解的就是

个别的变量,在别的线程修改的时候,或者修改之前就已经知道了,这个变量会不会改变

为了避免编译器重排序导致的并发错误

比如

线程A        new 对象                                                                                        

线程B        对象回收

线程AB并发执行,但是线程B知道线程A还没有执行new方法,自己(线程B)无法调用线程回收的方法

总结

Happens-before保证了,代码的逻辑顺序和jvm执行的逻辑顺序是一样的,不会发生语句错乱导致的错误

volatile

有序性和可见性

volatile变量存在两个特性 有序和可见性

当写一个volatile变量的时候,立刻刷新到内存中

当读一个volatile变量的时候,从主内存中读取

volatile为什么保证有序和可见呢?

内存屏障

可见:修改后通知所有线程变量已被修改

有序:禁止重排序

内存屏障的功能就是防止代码重排序

读写

在内存屏障之前的写操作需要先写入到内存中

在内存屏障之前的读操作被阻塞,需要等写操作执行完成之后才会执行

小结

不允许内存屏障之后的指令重排序到内存屏障之前(get不能在set之前)保证了有序性,这样的结果就是读的数据永远是写入后的数据

可见性volatile里面有个及时通知的功能

四大内存屏障

粗分两种:读屏障        写屏障

细分四种:

练习例子

public class VolatileSeeDemo
{
    static boolean flag = true;
    //static volatile boolean flag = true;

    public static void main(String[] args)
    {
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName()+"\t -----come in");
            while(flag)
            {

            }
            System.out.println(Thread.currentThread().getName()+"\t -----flag被设置为false,程序停止");
        },"t1").start();

        //暂停几秒钟线程
        try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }

        flag = false;

        System.out.println(Thread.currentThread().getName()+"\t 修改完成flag: "+flag);


    }
}

主内存的flag并没有刷入到主内存,所以线程并不会停止

但是当加入volatile关键字,就会让main主线程的修改变得可见,所以t1线程会立马去主内存中读取变量

volatile不保证原子性

所以对于多段代码的执行最好不要使用volatile

package com.bilibili.juc.volatiles;

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

class MyNumber
{
    volatile int number;

    public void addPlusPlus()
    {
        number++;
    }
}

/**
 * @auther zzyy
 * @create 2022-02-23 16:54
 */
public class VolatileNoAtomicDemo
{
    public static void main(String[] args)
    {
        MyNumber myNumber = new MyNumber();
        ReentrantLock reentrantLock = new ReentrantLock();
        for (int i = 1; i <=10; i++) {
            new Thread(() -> {
                for (int j = 1; j <=1000; j++) {

                    //reentrantLock.lock();
                    myNumber.addPlusPlus();
                    //reentrantLock.unlock();
                }
            },String.valueOf(i)).start();
        }

        //暂停几秒钟线程
        try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }

        System.out.println(myNumber.number);

    }
}

使用volatile变量控制变量n增加到10000是不可靠的

需要依靠Lock或者sychrnized实现原子性

重排序

重排序的分类

CAS

JDK提供的CAS机制,在汇编层级会禁止变量两侧的指今优化,然后使用cmpxchg指令比较并更新变量值(原子性)
Atomic*:cmpxcha(x. addr. el)== e!

底层由硬件保证原子性可见性

使用cmpxchg汇编指令

里面使用了volatile保证了有序性

缺点

优点是避开了线程阻塞,轻量级的锁

那么缺点呢?

1. CPU开销过大

不断自旋,浪费cpu资源

2. ABA问题

容易改值冲突

解决方法AtomicStampedReference

原子类

原子类可以理解为对CAS(Compare and Swap)机制的高级封装。这些原子类提供了一组方法,这些方法执行的操作是原子的,这意味着在多线程环境中,这些操作不会被中断或交错执行,从而确保线程安全。这些原子类内部使用了CAS机制来实现原子操作,而开发人员无需显式地编写CAS代码

java的java.util.concurrent包提供了一系列原子类,用于执行各种原子操作。以下是一些常见的原子类和它们的主要API:

  1. AtomicInteger:用于原子地操作整数值。

    • get():获取当前值。
    • set(int newValue):设置为新值。
    • getAndIncrement():获取当前值并增加1。
    • incrementAndGet():增加1并获取新值。
    • getAndAdd(int delta):获取当前值并增加指定的增量。
    • addAndGet(int delta):增加指定的增量并获取新值。
  2. AtomicLong:用于原子地操作长整数值,与AtomicInteger类似,但操作的是长整数。

  3. AtomicBoolean:用于原子地操作布尔值。

    • get():获取当前值。
    • set(boolean newValue):设置为新值。
    • getAndSet(boolean newValue):获取当前值并设置为新值。
  4. AtomicReference:用于原子地操作引用类型。

    • get():获取当前引用。
    • set(V newValue):设置为新引用。
    • getAndSet(V newValue):获取当前引用并设置为新引用。
  5. AtomicReferenceArray:用于原子地操作引用类型的数组。

    • get(int index):获取指定索引位置的元素。
    • set(int index, E newValue):设置指定索引位置的元素为新值。
    • getAndSet(int index, E newValue):获取指定索引位置的元素并设置为新值。
  6. AtomicIntegerFieldUpdaterAtomicLongFieldUpdaterAtomicReferenceFieldUpdater:这些类允许原子地更新指定类的字段。它们通常用于将原子性应用于非volatile字段。

  7. AtomicIntegerArrayAtomicLongArray:用于原子地操作整数数组和长整数数组。

  8. AtomicStampedReference:用于解决ABA问题,它在原子引用上维护一个标记(stamp)来检测引用变化。

这些原子类提供了一种方式来执行线程安全的、无锁的操作,可以用于多线程环境中的共享变量。通过使用这些类,开发人员可以避免显式地使用锁或synchronized关键字,从而提高并发性能和减少竞态条件的可能性。

AtomicMarkableReference与AtomicStampedReference

传入一个boolean判断是否进行修改过,相比AtomicStampedReference,只能判断一次

Automic.....FieldUpdater

AtomiclntegerFieldUpdater
AtomicLongFieldUpdater
AtomicReferenceFieldUpdater

以一种线程安全的方式操作非线程安全对象内的字段

使用方法

1. 将线程安全的字段加public volatile

2. 需要new 更新器

CountDownLatch

CountDownLatch 的主要思想是设置一个初始计数值,当线程完成一个特定任务时,会将计数值减一。当计数值达到零时,等待中的线程会被释放,可以继续执行。

CountDownLatch 提供了两个主要方法:

  1. await(): 当一个线程调用这个方法时,如果计数值不为零,它将被阻塞,直到计数值变为零。
  2. countDown(): 当某个线程完成了特定任务时,它会调用这个方法来将计数值减一。

CountDownLatch 在多线程编程中非常有用,它可以用于等待多个线程都完成某个任务之后再执行下一步操作,或者等待一组任务都完成后执行某个汇总操作。这可以帮助实现线程间的协调和同步。

class MyVar //资源类
{
    public volatile Boolean isInit = Boolean.FALSE;

    AtomicReferenceFieldUpdater<MyVar,Boolean> referenceFieldUpdater =
            AtomicReferenceFieldUpdater.newUpdater(MyVar.class,Boolean.class,"isInit");

    public void init(MyVar myVar)
    {
        if (referenceFieldUpdater.compareAndSet(myVar,Boolean.FALSE,Boolean.TRUE))
        {
            System.out.println(Thread.currentThread().getName()+"\t"+"----- start init,need 2 seconds");
            //暂停几秒钟线程
            try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
            System.out.println(Thread.currentThread().getName()+"\t"+"----- over init");
        }else{
            System.out.println(Thread.currentThread().getName()+"\t"+"----- 已经有线程在进行初始化工作。。。。。");
        }
    }
}


/**
 * @auther zzyy
 * 需求:
 * 多线程并发调用一个类的初始化方法,如果未被初始化过,将执行初始化工作,
 * 要求只能被初始化一次,只有一个线程操作成功
 */

LongAddder与LongAccumulator

LongAddder只能算加法

LongAccumulator可以算更多

实战

class ClickNumber //资源类
{
    int number = 0;
    public synchronized void clickBySynchronized()
    {
        number++;
    }

    AtomicLong atomicLong = new AtomicLong(0);
    public void clickByAtomicLong()
    {
        atomicLong.getAndIncrement();
    }

    LongAdder longAdder = new LongAdder();
    public void clickByLongAdder()
    {
        longAdder.increment();
    }

    LongAccumulator longAccumulator = new LongAccumulator((x,y) -> x + y,0);
    public void clickByLongAccumulator()
    {
        longAccumulator.accumulate(1);
    }

}

四个线程安全策略哪个最省时间呢?

public static void main(String[] args) throws InterruptedException
    {
        ClickNumber clickNumber = new ClickNumber();
        long startTime;
        long endTime;

        CountDownLatch countDownLatch1 = new CountDownLatch(threadNumber);
        CountDownLatch countDownLatch2 = new CountDownLatch(threadNumber);
        CountDownLatch countDownLatch3 = new CountDownLatch(threadNumber);
        CountDownLatch countDownLatch4 = new CountDownLatch(threadNumber);

        startTime = System.currentTimeMillis();
        for (int i = 1; i <=threadNumber; i++) {
            new Thread(() -> {
                try {
                    for (int j = 1; j <=100 * _1W; j++) {
                        clickNumber.clickBySynchronized();
                    }
                } finally {
                    countDownLatch1.countDown();
                }
            },String.valueOf(i)).start();
        }
        countDownLatch1.await();
        endTime = System.currentTimeMillis();
        System.out.println("----costTime: "+(endTime - startTime) +" 毫秒"+"\t clickBySynchronized: "+clickNumber.number);
    }

结果如下

LongAccumulator的性能是最好的

LongAdder为什么这么快呢?

Striped64类

两个策略底层都使用了CAS,但是LongAdder在超多线程的情况下会将value的更新压力分散给cell,每一个cell负责几个线程,到最后再把cell加在一起

LongAdder的value就是使用base加上cell[ ? ]的全部单元组成的

简单一句话:用空间换时间

源码分析

add方法是自增的主要方法

casBase里面执行了CAS操作

如果返回了false说明CAS自旋,需要进行cell辅助

第一次创建cell为2个,最大的值是cpu的核心数量

当cell槽位又不行了,返回false,从而进行扩容,从2个编程4个

ThreadLocal

一个线程中可以有多个threadlocal

为什么threadlocalmap的key为弱引用,因为key指向了threadlocal,当进行GC的时候回收s变量,因为key是弱引用,所以GC可以回收Threadlocal对象

为什么threadlocalmap的value为强引用,因为我们需要使用value的值,不能一获取线程的值就返回null

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值