【基础】Java 并发编程(下)

不安全的集合类

List

import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;

public class ListDemo {

    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        /**
         * List<String> list = new ArrayList<>(); 并发不安全
         * 导致 java.util.ConcurrentModificationException 异常
         *
         * 解决方法:
         * 1. List<String> list1 = new Vector<>();
         * 2. List<String> list2 = Collections.synchronizedList(new ArrayList<>());
         * 3. List<String> list3 = new CopyOnWriteArrayList<>();
         */
        List<String> list1 = new Vector<>();
        List<String> list2 = Collections.synchronizedList(new ArrayList<>());
        List<String> list3 = new CopyOnWriteArrayList<>();
        for (int i = 0; i < 80; i++) {
            new Thread(()->{
                list3.add(UUID.randomUUID().toString().substring(0, 5));
                System.out.println(list3);
            }, "线程" + i).start();
        }
    }

}

Set

import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArraySet;

public class SetDemo {

    public static void main(String[] args) {
        Set<String> set = new HashSet<>();
        /**
         * Set<String> set = new HashSet<>(); 并发不安全
         * 导致异常 java.util.ConcurrentModificationException
         *
         * 解决方法:
         * 1. Set<String> set1 = Collections.synchronizedSet(new HashSet<>());
         * 2. Set<String> set2 = new CopyOnWriteArraySet<>();
         */
        Set<String> set1 = Collections.synchronizedSet(new HashSet<>());
        Set<String> set2 = new CopyOnWriteArraySet<>();
        for (int i = 0; i < 50; i++) {
            new Thread(()->{
                set2.add(UUID.randomUUID().toString().substring(0, 5));
                System.out.println(set2);
            }, "线程" + i).start();
        }
    }

}

Map

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class MapDemo {

    public static void main(String[] args) {
        Map<String, String> map = new HashMap<>();
        /**
         * Map<String, String> map = new HashMap<>(); 并发不安全
         * 导致异常 java.util.ConcurrentModificationException
         *
         * 解决方法:
         * 1. Map<String, String> map1 = Collections.synchronizedMap(new HashMap<>());
         * 2. Map<String, String> map2 = new ConcurrentHashMap<>();
         */
        Map<String, String> map1 = Collections.synchronizedMap(new HashMap<>());
        Map<String, String> map2 = new ConcurrentHashMap<>();
        for (int i = 0; i < 10; i++) {
            final int temp = i;
            new Thread(()->{
                map2.put("线程" + temp, "线程" + temp);
                System.out.println(map2);
            }, "线程" + i).start();
        }
    }

}

常用的辅助类

CountDownLatch

import java.util.concurrent.CountDownLatch;

public class CountDownLatchDemo {
    /**
     * 重要方法:
     * countDownLatch.countDown(); 计数 -1
     * countDownLatch.await(); 等待计数器归零再向下执行
     */

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(6);
        for (int i = 1; i <= 6; i++) {
            new Thread(()->{
                System.out.println(Thread.currentThread().getName() + "下机了");
                countDownLatch.countDown(); // 计数 -1
            }, String.valueOf(i)).start();
        }
        countDownLatch.await(); // 等待计数器归零再向下执行
        System.out.println("锁门!");
    }

}

CyclicBarrier

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

public class CyclicBarrierDemo {
    /**
     * 重要方法:
     * CyclicBarrier cyclicBarrier = new CyclicBarrier(num, ()->{
     *      计数达到 num 次后要执行的方法;
     * });
     * cyclicBarrier.await(); 等待计数器达到 num 再向下执行
     */

    public static void main(String[] args) {
        CyclicBarrier cyclicBarrier = new CyclicBarrier(7, ()->{
            System.out.println("集齐七颗龙珠召唤神龙!");
        });

        for (int i = 1; i <= 7; i++) {
            final int temp = i;
            new Thread(()->{
                System.out.println(temp + "龙珠,Get!");
                try {
                    cyclicBarrier.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }, String.valueOf(i)).start();
        }

    }

}

Semaphore

import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;

public class demo08_Semaphore {
    /**
     * 重要方法:
     * semaphore.acquire(); 抢夺资源。如果已经满了,则等待资源被释放
     * semaphore.release(); 释放资源
     * 作用:
     * 多个共享资源互斥的使用!并发限流,控制最大的线程数
     */

    public static void main(String[] args) {
        Semaphore semaphore = new Semaphore(3);
        for (int i = 1; i <= 6; i++) {
            new Thread(()->{
                try {
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName() + "抢到了电脑");
                    TimeUnit.SECONDS.sleep(2);
                    System.out.println(Thread.currentThread().getName() + "下机了");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    semaphore.release();
                }
            }, "线程" + i).start();
        }
    }

}

Future 模式

Future 模式主要适用于异步的执行一个非常耗时的任务,而该任务的结果在后续的执行流程中不立即需要。在此期间可以继续执行其他的操作,待该任务完成后可以获得并查看其返回的结果。

例如,在科研中,跑一个半导体仿真的程序是非常慢的,我们可以在跑仿真的同时,去阅读一些相关的文献等等,等待仿真程序跑完后对查看结果。

Java 为我们提供了 CompletableFuture 类,实现了 Future 模式,该类提供了一个回调机制,可以在任务完成之后自动的进行回调处理,如 Demo 所示:

public class CompletableFutureDemo {

    public static void main(String[] args) throws InterruptedException {
        CompletableFuture.supplyAsync(CompletableFutureDemo::process)
                .thenAccept(System.out::println)
                .exceptionally(e -> {
                    e.printStackTrace();
                    return null;
                });
        System.out.println(">>>继续向下执行任务...");
        Thread.sleep(20000);
        System.out.println(">>>程序执行结束...");
    }

    public static String process() {
        System.out.println(">>>开始执行处理...");
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        if (Math.random() < 0.2) {
            throw new RuntimeException(">>>处理出错...");
        }
        return ">>>处理完成!";
    }

}

Forkjoin 框架

ForkJoin 采用分治的思想,将大任务分拆解成一个个的小任务执行,最后汇总每一个小任务的处理结果,得到最终的任务结果。

其核心思想,是将一个规模为 N 的问题,分解成 K 个规模较小的子问题,这些子问题相互独立并且与原问题的解决思路相同。对子问题进行求解、合并,便可以得到原问题的解。

ForkJoin 的使用案例:求解 1 - 10000 的和:

public class ForkJoinDemo {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ForkJoinPool forkJoinPool = new ForkJoinPool();
        ForkJoinTask<Long> rootTask = forkJoinPool.submit(new SumForkJoinTask(1L, 10000L));
        System.out.println(rootTask.get());
    }

}

class SumForkJoinTask extends RecursiveTask<Long> {

    private final Long min;
    private final Long max;

    public SumForkJoinTask(Long min, Long max) {
        this.min = min;
        this.max = max;
    }

    @Override
    protected Long compute() {
        // 当两数只差小于某阈值时直接计算
        long threshold = 10L;
        if (max - min <= threshold) {
            long sum = 0L;
            for (Long i = min; i <= max; i++) {
                sum += i;
            }
            return sum;
        }
        long middle = (min + max) >> 1;
        // 拆分任务,将任务压入线程队列
        SumForkJoinTask leftTask = new SumForkJoinTask(min, middle);
        leftTask.fork();
        SumForkJoinTask rightTask = new SumForkJoinTask(middle + 1, max);
        rightTask.fork();
        return leftTask.join() + rightTask.join();
    }
}

ForkJoinTask

ForkJoinTask 是在 ForkJoinPool 中运行的任务的抽象类,其实现了 Future 接口,通过fork()方法安排异步任务执行,通过join() 方法等待任务执行的结果。

Java 为我们提供了三个继承于 ForkJoinTask 抽象类的方法,可以继承使用:

类名描述
RecursiveAction子任务无返回值
RecursiveTask子任务存在返回值
CountedCompleter任务完成后触发执行

在使用 ForkJoinTask 处理大量的任务时,需要注意:

  • 拆分任务中避免出现同步方法和同步代码块;

  • 拆分任务中避免出现阻塞 IO 操作;

  • 拆分任务中不允许抛出受检异常;

ForkJoinPool

ForkJoinPool 是用于运行 ForkJoinTasks 的线程池,其实现了 Executor 接口

ForkJoinPool 的构造方法如下:

    /**
    * parallelist: 期望并发数
    * factory: 创建 ForkJoin 工作线程的工厂,默认为 defaultForkJoinWorkerThreadFactory
    * handler: 执行任务出错时的处理程序,默认为 null
    * asyncMode: 工作线程获取任务的模式(FIFO/LIFO(默认)
    */
    public ForkJoinPool(int parallelism,
                        ForkJoinWorkerThreadFactory factory,
                        UncaughtExceptionHandler handler,
                        boolean asyncMode) {
        this(parallelism, factory, handler, asyncMode,
             0, MAX_CAP, 1, null, DEFAULT_KEEPALIVE, TimeUnit.MILLISECONDS);
    }

ForkJoinPool 与 ThreadPoolExecutor 的差异

如果使用 ThreadPoolExecutor 来实现分治的思想,那么每一个子任务都要创建一个线程去执行,当子任务数量过大时,需要创建的线程数过多。

ForkJoinPool 在处理子任务时,只会按照指定的期望并发数创建线程,线程工作时若需要拆分子任务,则会将当前任务放入 ForkJoinWorkerThread 的任务队列中,进行递归处理。

ForkJoinWorkerThread 继承于 Thread 类,是用于执行 ForkJoinTask 的线程。

ForkJoinPool 工作窃取算法

在 ForkJoinPool 中,各个工作线程都会维护一个自身的任务队列。每一个线程优先执行本线程任务队列内的任务,当自己的任务执行完毕后,线程会查看其他线程的任务队列中是否有未完成的任务,如果有该线程则会协助其他线程进行处理。

为减少上述过程中线程之间对于任务的竞争,任务队列采用双端队列实现。

Callable 与 Runnable

Callable 与 Runnable 均为多线程中的重要接口,编写类实现上述接口并将实例化的对象传入线程池即可创建新线程完成相应的任务,两个接口的基本使用如下:

Callable

public class CallableDemo {

    public static void main(String[] args) {
        ExecutorService threadPool = Executors.newFixedThreadPool(1);
        Future<String> future = threadPool.submit(new CallableTask());
        threadPool.shutdown();
        if (future.isDone()) {
            try {
                System.out.println(future.get());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

}

class CallableTask implements Callable<String> {
    @Override
    public String call() throws Exception {
        return "This is CallableTask...";
    }
}

Runnable

public class RunnableDemo {

    public static void main(String[] args) {
        ExecutorService threadPool = Executors.newFixedThreadPool(1);
        threadPool.submit(new RunnableTask());
        threadPool.shutdown();
    }

}

class RunnableTask implements Runnable {
    @Override
    public void run() {
        System.out.println("This is CallableTask...");
    }
}

比较 Callable 与 Runnable 的不同

CallableRunnable
提供 call() 方法,可以直接抛出异常提供 run() 方法,所有受检异常须在方法内处理
call() 方法可是存在返回值run() 方法不能提供返回值
Callable 只能通过线程池执行Runnable 既可以通过线程池执行,也可以作为 Thread 的参数创建新线程执行

ThreadLocal

ThreadLocal 用于在一个线程内进行状态的传递。

很多时候,我们在线程内调用的方法需要传入参数,而方法内部又调用很多方法,同样也需要参数,这样如果全部进行传参的话就会导致某些参数传递到所有地方。 像这种在一个线程中横跨若干个方法调用,需要传递的对象,我们称为上下文 Context,它时一种状态,可以是用户身份、任务信息等等。 Java 库为我们提供了 ThreadLocal 用于在同一个线程中传递同一个对象。

实际上,可以把 ThreadLocal 看作一个全局的 Map<Thread, Object>,每个线程获取变量时,总是以 Thread 自身作为 key: Object threadLocalValue = threadLocalMap.get(Thread.currentThread()); 因此,ThreadLocal 相当于为每一个线程开辟了独立的存储空间,各个线程的 ThreadLocal 变量互不影响。

最后需要注意的是,ThreadLocal 一定要在 finally 代码块中清除。 因为当前线程执行完相关代码后,很可能会被重新放入线程池中,如果ThreadLocal没有被清除,该线程执行其他代码时,会把上一次的状态带进去。

public class ThreadLocalDemo {

    static ThreadLocal<User> threadLocalUser = new ThreadLocal<>();

    public static void main(String[] args) {
        User zhangsan = new User("zhangsan");
        processUser(zhangsan);
    }

    public static void processUser(User user) {
        try {
            threadLocalUser.set(user);
            log();
            // 各种处理流程
        } finally {
            threadLocalUser.remove();
        }
    }

    public static void log() {
        User user = threadLocalUser.get();
        System.out.println(">>>操作记录:" + user.username);
    }


}

@Data
@AllArgsConstructor
class User {
    public String username;
}

ThreadLocal 内部实现机制

  • 每个线程内部都会维护一个类似 HashMap 的对象,称为 ThreadLocalMap,里边会包含若干了 Entry (K-V键值对),相应的线程被称为这些 Entry 的属主线程
  • Entry 的 Key 是一个 ThreadLocal 实例,Value 是一个线程特有对象。Entry 的作用是为其属主线程建立起一个 ThreadLocal 实例与一个线程特有对象之间的对应关系
    • 当执行 set() 方法时,ThreadLocal 首先获取当前线程对象,然后获取当前线程的 ThreadLocalMap 对象。在以当前 ThreadLocal 对象为 Key,将 value 存储禁 ThreadLocalMmap 对象中。
    • 当执行 get() 方法时,ThreadLocal 首先会获取当前线程对象,然后获取当前线程 ThreadLocalMap 对象。在以当前ThreadLocal对象为 Key,获取对应的值 value
  • Entry 对 Key 的引用是弱引用;Entry 对 Value 的引用是强引用

ThreadLocal 并发安全

Java 中常用两种机制来解决多线程的并发问题:

  • synchronized 方式,通过锁机制,使一个线程在执行时,另一个线程等待,是一种以时间换空间的方式保证多线程并发安全;
  • ThreadLocal 方式:通过创建线程局部变量,以空间换时间的方式保证多线程并发安全;

在 Spring 的源码中,就使用了 ThreadLocal 来管理连接。 在很多开源项目中,都经常使用 ThreadLocal 来控制多线程并发问题,因为它足够的简单,我们不需要关心是否有线程安全问题,因为变量是每个线程所特有的。

ThreadLocal 应用场景

  1. 线程数据隔离
  2. 在进行对象跨层传输时,使用 ThreadLocal 可以避免对此传递,打破层次间的约束
  3. 进行实物操作,用于存储线程事务信息(Spring 框架事务开始时会为当前线程绑定一个 JDBC Connection,就是使用 ThreadLocal 实现的)

ThreadLocal 内存泄漏

内存泄漏的原因

threadLocalMap 使用 ThreadLocal 的弱引用作为 key,如果一个 ThreadLocal 不存在外部强引用时,ThreadLocal 势必会被 GC 回收,这样就会导致 ThreadLocalMap 中 key 为 null, 而 value 还存在着强引用,只有 thead 线程退出以后,value 的强引用链条才会断掉。

但如果当前线程再迟迟不结束的话,这些 key 为 null 的 Entry 的 value 就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value,所以永远无法回收,造成内存泄漏。

避免内存泄漏

当 ThreadLocalMap 的 key 为弱引用回收 ThreadLocal 时,由于 ThreadLocalMap 持有 ThreadLocal 的弱引用,即使没有手动删除,ThreadLocal 也会被回收。当 key 为 null,在下一次 ThreadLocalMap 调用 set()、get()、remove() 方法的时候会被清除 value 值。

即使用弱引用可以多一层保障:弱引用 ThreadLocal 不会内存泄漏,对应的 value 在下一次 ThreadLocalMap 调用 set()、get()、remove() 的时候会被清除。

ThreadLocal正确的使用方法

  • 每次使用完 ThreadLocal 都调用它的 remove() 方法清除数据
  • 将 ThreadLocal 变量定义成 private static,这样就一直存在 ThreadLocal 的强引用(延长 ThreadLocal 的生命周期),也就能保证任何时候都能通过 ThreadLocal 的引用访问到 Entry 的 value 值,进而清除掉

CAS

CAS 是 compare and swap 的缩写,其含义是比较并且交换。CAS 操作主要包含三个操作数,即内存原值,预期值以及要设置的新值。

当需要改变某内存的值时,首先比较该内存原值与预期值,如果两个值相等,则将原值设置为新值,否则则不进行任何操作。

例如变量 X = 1,现在要采用 CAS 对其进行加一的操作。首先将 A 的值 1 读取到内存当中,并在内存中执行 +1 的计算,然后重新比较变量 X 的当前值与之前内存中读入的值,如果相等,说明没有该变量没有被其他线程修改过,则直接将计算后的新值赋予 X。

原子类 AtomicInteger 利用 CAS 进行原子自增:

public class AtomicIntegerDemo {

    public static int count = 0;

    private static final int MAX_THREAD = 10;

    public static AtomicInteger atomicInteger = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(MAX_THREAD);
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 1000; i++) {
                    count ++;
                    atomicInteger.getAndIncrement();
                }
                latch.countDown();
            }
        };
        for (int i = 0; i < MAX_THREAD; i++) {
            new Thread(runnable).start();
        }
        latch.await();
        System.out.println("理论值>>> " + 1000 * MAX_THREAD);
        System.out.println("count>>> " + count);
        System.out.println("atomicInteger>>> " + atomicInteger);
    }

}

CAS 的实现

Java 中的原子类都是通过 CAS 实现的,例如 AtomicInteger 的getAndIncrement()源码如下:

    public final int getAndIncrement() {
        return U.getAndAddInt(this, VALUE, 1);
    }

其中getAndAddInt()方法的源码如下:

    public final int getAndAddInt(Object o, long offset, int delta) {
        int v;
        do {
            v = getIntVolatile(o, offset);
        } while (!weakCompareAndSetInt(o, offset, v, v + delta));
        return v;
    }

该方法属于 Unsafe 类,Unsafe 类提供了 CAS 的方法,其直接通过 native 方法调用了底层 CPU 指令cmmpxchg,其提供的三个”比较并交换“的原子方法如下:

/*
@param o 包含要修改的字段的对象
@param offset 字段在对象内的偏移量
@param expected 期望值(旧的值)
@param update 更新值(新的值)
@return true 更新成功 | false 更新失败
*/
public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object update);
public final native boolean compareAndSwapInt( Object o, long offset, int expected,int update);
public final native boolean compareAndSwapLong( Object o, long offset, long expected, long update);

CAS 的 ABA 问题

所谓 ABA 问题,是指某一个线程无法判断变量是否存在变化的中间过程的问题。例如线程 A 将 X = 1 读取进内存进行操作时,线程 B 同样读取到 X = 1 并首先将其变为 2,然后重新设置为 1,而此时线程 A 完成操作并比较进行赋值时,不会发现 X 之前已经被修改过。

Java 提供了 AtomicStampedReference 原子类用于解决 ABA 问题,该类中引入了”版本号“,用于记录变量的每一次更改。

public class AtomicStampedReferenceDemo {

    public static void main(String[] args) {
        AtomicInteger v1 = new AtomicInteger(100);
        AtomicInteger v2 = new AtomicInteger(101);
        AtomicStampedReference<AtomicInteger> stampedRef = new AtomicStampedReference<>(v1, 0);
        // 获取原版本号,用于原线程后续操作
        int stamp = stampedRef.getStamp();
        // 模拟 ABA 过程
        stampedRef.compareAndSet(v1, v2, stampedRef.getStamp(), stampedRef.getStamp() + 1);
        stampedRef.compareAndSet(v2, v1, stampedRef.getStamp(), stampedRef.getStamp() + 1);
        // ABA 之后的结果
        System.out.println("new value = " + stampedRef.getReference()); // new value = 100
        // 模拟原线程后续操作
        System.out.println(stampedRef.compareAndSet(v1, v2, stamp, stamp + 1)); // false
    }

}

AQS

AQS 全称 Abstract Queued Synchronizer,是 Java 提供的一个同步框架:抽象队列同步器。其内部维护一个 FIFO 的 CLH 双向同步队列,AQS 依赖这个同步队列来管理同步状态 state。ReentrantLock、Semaphore、CountDownLatch、CyclicBarrier 等并发类均是基于 AQS 来实现的。

当线程获取同步状态 state 失败时,当前线程信息和等待信息等将会被封装成一个节点,并尝试将节点放入阻塞队列当中,同步阻塞当前线程。当 state 被释放时,队列中的头节点会被唤醒并获取 state。

AQS 的内部实现

  • 线程状态 State:当一个线程想要获取锁时,就需要通过 CAS 的方式来修改 state 的值,0 代表无锁;1 代表有锁;

  • Node 静态内部类:当线程 CAS 修改 state 失败时,就会创建一个包含线程信息的 Node 放入队列当中;

  • 获取锁的过程:

    • acquire(int arg):该方法调用tryAcquire(int arg)方法尝试获取锁,若获取失败,则调用acquireQueued(addWaiter(Node.EXCLUSIVE), arg);若获取锁失败并成功添加到队列,则调用selfInterrupt()

    • tryAcquire(int arg):尝试获取锁

    • addWaiter(Node mode):添加一个等待的线程,该方法成功后会将该线程封装成 Node 添加到队列的尾部,并返回 Node

    • acquireQueued(final Node node, int arg):循环执行该方法,若线程对应的节点不是头节点的后置节点,则进入等待状态;若其前置节点是头节点,则有资格去竞争锁。其调用shouldParkAfterFailedAcquire()方法进行判断

    • shouldParkAfterFailedAcquire(Node pred, Node node):该方法用来确定线程是否可以安心阻塞,主要是判断其前置节点是不是头节点

    • selfInterrupt():尝试获得锁失败,并且添加结点到队列中成功,就会进行该方法

AQS 部分源码

acquire()方法:

    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

Node 静态内部类:

    static final class Node {
        // 共享锁
        static final Node SHARED = new Node();
        // 独占锁
        static final Node EXCLUSIVE = null;

        // 节点状态
        static final int CANCELLED =  1;
        static final int SIGNAL    = -1;
        static final int CONDITION = -2;
        static final int PROPAGATE = -3;

        // 节点等待状态
        volatile int waitStatus;

        // 前置节点
        volatile Node prev;
        // 后置节点
        volatile Node next;

        // 线程信息
        volatile Thread thread;

        // 节点方法等省略...
    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值