JUC( java.util .concurrent) 概述

相关概念介绍

  1. 串行是一次只能取得一个任务,并执行这个任务。
  2. 并行意味着可以同时取得多个任务,并同时去执行所取得的这些任务。并行模式相当于将长长的一条队列,划分成了多条短队列,所以并行缩短了任务队列的长度。并行的效率从代码层次上强依赖于多进程/多线程代码,从硬件角度上则依赖于多核 CPU。
  3. 并发是指两个任务都请求运行,而处理器只能接收一个任务,就是把这两个任务安排轮流进行,由于时间间隔较短,使人感觉两个任务都在运行(12306抢票的案例)。同一时刻多个线程在访问同一个资源,多个线程对一个点。并发编程的本质:充分利用CPU的资源!
  4. 进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。 在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。进程:指在系统中正在运行的一个应用程序;程序一旦运行就是进程;进程——资源分配的最小单位。
  5. 线程(thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。线程:系统分配处理器时间资源的基本单元,或者说进程之内独立执行的一个单元执行流。线程——程序执行的最小单位。
  6. 管程英文叫Monitor,翻译过来是监视器,就是我们平时说的锁。是保证了同一时刻只有一个进程在管程内活动,即管程内定义的操作在同一时刻只被一个进程调用(由编译器实现).(管程就是锁的外壳????);JVM 中同步是基于进入和退出管程(monitor)对象实现的,每个对象都会有一个管程(monitor)对象,管程(monitor)会随着 java 对象一同创建和销毁执行线程首先要持有管程对象,然后才能执行方法,当方法完成之后会释放管程,方法在执行时候会持有管程,其他线程无法再获取同一个管程;
  7. 用户线程:平时用到的普通线程,自定义线程;当主线程结束后,用户线程还在运行,JVM 存活;如果没有用户线程,都是守护线程,JVM 结束;
  8. 守护线程:运行在后台,是一种特殊的线程,比如垃圾回收
     

线程同步的方法

  1. 同步代码块
  2. 同步方法
  3. Lock锁
说明:
    a.同步监视器:俗称“锁”;任何一个类的对象。都可以充当“锁”。但是多个线程只能共用一把锁;意思是:有多个线程只能new一个锁;
    b.共享数据:多个线程共同操作的变量。比如:多个窗口卖一定数量的票,此时的票就是共享数据
synchronized的锁是什么?
    a.任意对象都可以作为同步锁。所有对象都自动含有单一的锁(监视器)。
    b.同步方法的锁:静态方法(类名.class)、非静态方法(this)
    c.同步代码块:自己指定,很多时候也是指定为this或类名.class
注意:
    a.必须确保使用同一个资源的多个线程共用一把锁,这个非常重要,否则就无法保证共享资源的安全
    b.一个线程类中的所有静态方法共用同一把锁(类名.class),所有非静态方法共用同一把锁(this),同步代码块(指定需谨慎);
优先使用顺序:
    a.Lock>同步代码块>同步方法
    b.对象锁优先于类锁

线程的相关介绍

        线程的生命周期 

 

       线程的中断

                A. 中断机制

1.首先:一个线程不应该由其他线程来强制中断或停止,而是应该由线程自己自行停止。所以,Thread.stop, Thread.suspend, Thread.resume 都已经被废弃了。
2.Java提供了一种用于停止线程的协商机制——中断。中断只是一种协作协商机制,Java没有给中断增加任何语法,中断的过程完全需要程序员自己实现。
3.每个线程对象中都有一个标识,用于表示线程是否被中断;该标识位为true表示中断,为false表示未中断;通过调用线程对象的interrupt方法将该线程的标识位设为true;可以在别的线程中调用,也可以在自己的线程中调用。若要中断一个线程,你需要手动调用该线程的interrupt方法,该方法也仅仅是将线程对象的中断标识设成true;接着你需要自己写代码不断地检测当前线程的标识位,如果为true,表示别的线程要求这条线程中断,此时究竟该做什么需要你自己写代码实现。
//案例:当前线程的中断标识为true,线程不是立刻停止
public class TestDemo {
    public static void main(String[] args) {
        Thread t1 = new Thread(()->{
            for(int i = 0;i < 300;i ++){
                System.out.println("---------" + i);
            }
            System.out.println("after t1.interrupt()---第2次----"+Thread.currentThread().isInterrupted());
        },"t1");
        t1.start();
        System.out.println("before t1.interrupt()----"+t1.isInterrupted());
        t1.interrupt();
        try {TimeUnit.MILLISECONDS.sleep(3);} catch (InterruptedException e) {e.printStackTrace();}
        System.out.println("after t1.interrupt()---第1次---"+t1.isInterrupted());
        try {TimeUnit.MILLISECONDS.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}
        System.out.println(t1.isAlive() ?"活着":"结束");
        System.out.println("after t1.interrupt()---第3次---"+t1.isInterrupted());

    }
}

                B. 中断相关API介绍

public void interrupt() 实例方法,实例方法interrupt()仅仅是设置线程的中断状态为true,发起一个协商而不会立刻停止线程(即使为true线程也不一定立即中断)
    1).如果线程处于正常活动状态,那么会将该线程的中断标志设置为 true,仅此而已。被设置中断标志的线程可能继续正常运行,不受影响。所以, interrupt() 并不能真正的中断线程,需要被调用的线程自己进行配合才行。
    2).如果线程处于被阻塞状态(例如处于sleep, wait, join 等状态),在别的线程中调用当前线程对象的interrupt方法,那么线程将立即退出被阻塞状态(中断状态将被清除),并抛出一个InterruptedException异常。
    3).中断不活动的线程不会产生任何影响
public static boolean interrupted()  静态方法,Thread.interrupted();判断线程是否被中断,并清除当前中断状态这个方法做了两件事:
    1).返回当前线程的中断状态
    2).将当前线程的中断状态设为false(这个方法有点不好理解,因为连续调用两次的结果可能不一样。)
public boolean isInterrupted()实例方法,判断当前线程是否被中断(通过检查中断标志位)
 

         

//如果线程处于被阻塞状态(例如处于sleep, wait, join 等状态),在别的线程中调用当前线程对象的interrupt方法,那么线程将立即退出被阻塞状态(中断状态将被清除),并抛出一个InterruptedException异常。
public class DemoThread {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            while (true) {
                if (Thread.currentThread().isInterrupted()) {
                    System.out.println(Thread.currentThread().getName() + "\t" + "中断标志位:" + Thread.currentThread().isInterrupted() + "程序终止");
                    break;
                }
                try {
                    TimeUnit.MILLISECONDS.sleep(1500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    // Thread.currentThread().interrupt();  假如加了这个,程序可以终止,只会爆异常
                }
                System.out.println("-----hello InterruptDemo03" + Thread.currentThread().isInterrupted());
            }
        }, "t1");
        t1.start();
        try {
            TimeUnit.MILLISECONDS.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(t1::interrupt).start();
    }
}

                C. 如何使用中断标识停止一个线程(下面介绍三种方式)

//1).通过一个volatile变量实现
public class interruptDemo {
    static volatile boolean isStop = false;
    public static void main(String[] args) {
        new Thread(()->{
            while(true){
                if(isStop){//如果这个标志位被其他线程改为true了
                    System.out.println(Thread.currentThread().getName()+"\t isStop被修改为true,程序终止");
                    break;
                }
                System.out.println("t1 ------hello volatile");//----------------------如果没停止,那就一直打印
            }
        },"t1").start();
 
        try {TimeUnit.MILLISECONDS.sleep(20);} catch (InterruptedException e) {e.printStackTrace();}
 
        new Thread(()->{
            isStop = true;
        },"t2").start();
    }
}
//2).通过AtomicBoolean(原子布尔型)2).通过AtomicBoolean(原子布尔型)
public class interruptDemo {
    static AtomicBoolean atomicBoolean = new AtomicBoolean(false);
    public static void main(String[] args) {
        m1_volatile();
    }
    public static void m1_volatile() {
        new Thread(()->{
            while(true){
                if(atomicBoolean.get()){//如果这个标志位被其他线程改为true了
                    System.out.println(Thread.currentThread().getName()+"\t isStop被修改为true,程序终止");
                    break;
                }
                System.out.println("t1 ------hello volatile");//----------------------如果没停止,那就一直打印
            }
        },"t1").start();
        try {TimeUnit.MILLISECONDS.sleep(20);} catch (InterruptedException e) {e.printStackTrace();}
        new Thread(()->{
            atomicBoolean.set(true);
        },"t2").start();
    }
}
//3).通过Thread类自带的中断api方法实现
public static void main(String[] args) {
        m1_volatile();
    }
    public static void m1_volatile() {
        Thread t1 = new Thread(() -> {
            while (true) {
                if (Thread.currentThread().isInterrupted()) {//一旦发现中断标志位被修改
                    System.out.println(Thread.currentThread().getName() + "\t isInterrupted()被修改为true,程序终止");
                    break;
                }
                System.out.println("t1 ------hello interrupt ");//----------------------如果没停止,那就一直打印
            }
        }, "t1");
        t1.start();
        try {TimeUnit.MILLISECONDS.sleep(20);} catch (InterruptedException e) {e.printStackTrace();}
        new Thread(()->{
            t1.interrupt();//把t1中断
        },"t2").start();
    }
}
 

                总结:中断只是一种协同机制,修改中断标识位仅此而已,而不是立刻stop打断 

        线程的优先级设置

1.线程有两种调度模型 [了解]
    A.分时调度模式:所有线程轮流使用CPU的使用权,平均分配每个线程占有CPU的时间片
    B.抢占式调度模型:优先让优先级高的线程使用CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的CPU时间片相对多一些 [ Java使用的是抢占式调度模型 ]
2.Thread类中设置和获取线程优先级的方法
    A.public final void setPriority(int newPriority):更改此线程的优先级
    B.public final int getPriority():返回此线程的优先级
    说明:
        a. 线程默认优先级是5;线程优先级范围是:1-10;(大于10或者小于1都会抛异常)
        b. 线程优先级高仅仅表示线程获取的CPU时间的几率高,但是要在次数比较多,或者多次运行的时候才能看到你想要的效果

        线程的控制(等待/唤醒/礼让)             

Thread类:
    1).static void sleep(long millis):该方法导致了程序暂停执行指定的时间,让出cpu该其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。在调用sleep()方法的过程中如果该线程持有锁,线程不会释放对象锁。
    2).void join():当前线程暂停,等待指定的线程执行结束后,当前线程再继续 (相当于插队加入)
    void join(int millis):可以等待指定的毫秒之后继续 (相当于插队,有固定的时间)
    3).void yield():让出cpu的执行权(礼让线程)
    4).void setDaemon​(boolean on):将此线程标记为守护线程,当运行的线程都是守护线程时,Java虚拟机将退出(守护线程)

Object类:
    1).public final native void notify();
    2).public final native void wait(long timeout) throws InterruptedException;当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备,获取对象锁进入运行状态。
    注意:说明要使用wait和notify必须加synchronized,而且notify和wait的执行顺序不能对换;

JUC包中的Condition
    1).void await() throws InterruptedException;
    2).void signal();
    说明:使用JUC包中的Condition的await()方法让线程等待,使用signal()方法唤醒线程(必须要lock中才能使用,且顺序不能颠倒;

LockSupport:
    1).public static void park();
    2).public static void unpark(Thread thread);
    说明:LockSupport可以阻塞当前线程以及唤醒指定被阻塞的线程;(无锁快的要求,且无顺序的要求)
//说明要使用wait和notify必须加synchronized,notify和wait的执行顺序不能对换; 
public class LockSupportDemo{
    public static void main(String[] args){
        Object objectLock = new Object();
        new Thread(() -> {
            synchronized (objectLock) {
                System.out.println(Thread.currentThread().getName()+"\t ---- come in");
                try {
                    objectLock.wait();//----------------------这里先让他等待
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName()+"\t"+"---被唤醒了");
        },"t1").start();
        //暂停几秒钟线程
        try { TimeUnit.SECONDS.sleep(3L); } catch (InterruptedException e) { e.printStackTrace(); }
        new Thread(() -> {
            synchronized (objectLock) {
                objectLock.notify();//-------------------------再唤醒它
                System.out.println(Thread.currentThread().getName()+"\t ---发出通知");
            }
        },"t2").start();
    }
}
 
//Condition的优势:精准的通知和唤醒的线程!如果我们要指定通知的下一个进行顺序怎么办呢? 我们可以使用Condition来指定通知进程;
public class LockSupportDemo{
    public static void main(String[] args){
        Lock lock = new ReentrantLock();
        Condition condition = lock.newCondition();
        new Thread(() -> {
            lock.lock();
            try{
                System.out.println(Thread.currentThread().getName()+"\t-----come in");
                condition.await();
                System.out.println(Thread.currentThread().getName()+"\t -----被唤醒");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        },"t1").start();
        try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }        //暂停几秒钟线程
        new Thread(() -> {
            lock.lock();
            try{
                condition.signal();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
            System.out.println(Thread.currentThread().getName()+"\t"+"我要进行唤醒");
        },"t2").start();
    }
}
//LockSupport可以阻塞当前线程以及唤醒指定被阻塞的线程;(无锁快的要求,且无顺序的要求)
public class LockSupportDemo {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "\t----------come in");
            LockSupport.park();
            System.out.println(Thread.currentThread().getName() + "\t----------被唤醒了");
        }, "t1");
        t1.start();
        new Thread(() -> {
            LockSupport.unpark(t1);
            System.out.println(Thread.currentThread().getName() + "\t-----发出通知,去唤醒t1");
        }, "t2").start();
    }
}
 

                虚假唤醒问题

//产生的原因:线程从等待到被唤醒它不会重新执行业务方法,而是接着执行等待之后的代码;
//结论:就是用if判断的话,唤醒后线程会从wait之后的代码开始运行,但是不会重新判断if条件,直接继续运行if代码块之后的代码,而如果使用while的话,也会从wait之后的代码运行,但是唤醒后会重新判断循环条件,如果不成立再执行while代码块之后的代码块,成立的话继续wait。
//这也就是为什么用while而不用if的原因了,因为线程被唤醒后,执行开始的地方是wait之后
 
class Three{
    private int num=0;
    public synchronized void delMethod() throws InterruptedException {
       while(num!=1){     //如果while变成if 就会产生虚假唤醒问题
           this.wait();
       }
       num--;
       System.out.println(Thread.currentThread().getName()+":"+num);
       notifyAll();

    }
    public synchronized void addMethod() throws InterruptedException {
        while(num!=0){
            this.wait();
        }
        num++;
        System.out.println(Thread.currentThread().getName()+":"+num);
        notifyAll();
    }
}
class Two{//创建资源类
    private int number=0;
    private Lock lock =new ReentrantLock();//创建可重入锁
    private Condition condition=lock.newCondition();
    public void delMethod(){
        try {
            lock.lock();
            while(number!=1){
                condition.await(); //
            }
            number--;
            System.out.println(Thread.currentThread().getName()+":"+number);
            condition.signalAll();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    public void addMethod(){
        try {
            lock.lock();
            while(number!=0){
                condition.await();  //
            }
            number++;
            System.out.println(Thread.currentThread().getName()+":"+number);
            condition.signalAll();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}
public class One {
    public static void main(String[] args) {
        Three two = new Three();
        //Two two = new Two();
        new Thread(()->{
            for (int i = 0; i <10 ; i++) {
                try {
                    two.addMethod();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
        new Thread(()->{
            for (int i = 0; i <10 ; i++) {
                try {
                    two.delMethod();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();

        new Thread(()->{
            for (int i = 0; i <10 ; i++) {
                try {
                    two.addMethod();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
        new Thread(()->{
            for (int i = 0; i <10 ; i++) {
                try {
                    two.delMethod();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();

    }
}
 
 

                LockSupport介绍(java.util.concurrent.locks.LockSupport)

Lock Support是用来创建锁和其他同步类的基本线程阻塞原语。
Lock Support是一个线程阻塞工具类, 所有的方法都是静态方法, 可以让线程在任意位置阻塞, 阻塞之后也有对应的唤醒方法。归根结底, Lock Support调用的Unsafe中的native代码。
Lock Support提供park() 和unpark() 方法实现阻塞线程和解除线程阻塞的过程
Lock Support和每个使用它的线程都有一个许可(permit) 关联。
    1.permit相当于1,0的开关,默认是0,调用一次unpark就变成1,调用一次park会消费permit,也就是1变成0,同时park立即返回;
    2.如再次调用park会变成阻塞(因为permit为0了会阻塞在这里,一直到permit变成1),这时调用unpark会把permit置为1;
每个线程都有一个相关的permit, permit最多只有一个, 重复调用unpark也不会积累凭证。
形象的理解:线程阻塞需要消耗凭证(permit) , 这个凭证最多只有1个。
当调用park()/park(Object blocker)方法时:
    1.如果有凭证,则会直接消耗掉这个凭证然后正常退出;
    2.如果无凭证,就必须阻塞等待凭证可用;
而LockSupport.unpark()则相反, 它会增加一个凭证, 但凭证最多只能有1个, 累加无效。

         保证多个线程有顺序的执行

                方式一:Condition

/*
    多个线程之间按顺序调用,实现A->B->C
三个线程启动,要求如下:
    AA打印5次,BB打印10次,CC打印15次
    接着
    AA打印5次,BB打印10次,CC打印15次
    ....来10轮
* */
//Condition的优势精准的通知和唤醒的线程!如果我们要指定通知的下一个进行顺序怎么办呢? 我们可以使用Condition来指定通知进程;
public class ThreadOrderAccess {
    public static void main(String[] args) {
        ShareResource shareResource=new ShareResource();

        new Thread(()->{ for (int i = 1; i <=10; i++)shareResource.print5(); },"线程A").start();
        new Thread(()->{ for (int i = 1; i <=10; i++)shareResource.print10(); },"线程B").start();
        new Thread(()->{ for (int i = 1; i <=10; i++)shareResource.print15(); },"线程C").start();
    }
}
class ShareResource{
    //设置一个标识,如果是number=1,线程A执行...
    private int number=1;
    Lock lock=new ReentrantLock();
    Condition condition1=lock.newCondition();
    Condition condition2=lock.newCondition();
    Condition condition3=lock.newCondition();
    public void print5(){
        lock.lock();
        try {
            //1.判断
            while(number!=1){
                condition1.await();
            }
            //2.干活
            for (int i = 1; i <=5; i++) {
                System.out.println(Thread.currentThread().getName()+":\t"+i);
            }
            //3.唤醒
            number=2;
            condition2.signal();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }

    public void print10(){
        lock.lock();
        try {
            //1.判断
            while(number!=2){
                condition2.await();
            }
            //2.干活
            for (int i = 1; i <=10; i++) {
                System.out.println(Thread.currentThread().getName()+":\t"+i);
            }
            //3.唤醒
            number=3;
            condition3.signal();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }

    public void print15(){
        lock.lock();
        try {
            //1.判断
            while(number!=3){
                condition3.await();
            }
            //2.干活
            for (int i = 1; i <=15; i++) {
                System.out.println(Thread.currentThread().getName()+":\t"+i);
            }
            //3.唤醒
            number=1;
            condition1.signal();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
}
 
 

                方式二:join()方法

 

public class JoinTest {
// 1.现在有T1、T2、T3三个线程,你怎样保证T2在T1执行完后执行,T3在T2执行完后执行
    public static void main(String[] args) throws InterruptedException {
        final Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    TimeUnit.SECONDS.sleep(4);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("t1");
            }
        });
        final Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
// 引用t1线程,等待t1线程执行完
                    t1.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("t2");
            }
        });
        Thread t3 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
// 引用t2线程,等待t2线程执行完
                    t2.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("t3");
            }
        });
       //这里三个线程的启动顺序可以任意,大家可以试下!
        t2.start();
      //  TimeUnit.SECONDS.sleep(4);
        t1.start();
        t3.start();
    }
}
 
 

                方式三

//定制化通信就是规定线程的执行顺序,我们可以给一个变量,每执行一个线程,改变一下变量的值,通过变量改变的值来判断下次是那个线程来执行;
public class Four {
    public static void main(String[] args) {
        Five five = new Five();
        new Thread(()->{
            try {
                five.print2();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        new Thread(()->{
            try {
                five.print3();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
    }
}
class Five{
    private int fla=2;
    private Lock lock=new ReentrantLock();
    private Condition condition=lock.newCondition();
    //你可以创建多个condition,多个condition相当于锁的多把钥匙,多个condition功能上没有区别;
    public void print2() throws InterruptedException {
        lock.lock();//上锁
        if(fla!=2){
            condition.await();//不满足条件等待
        }
        for (int i = 0; i <2; i++) {
            System.out.println(Thread.currentThread().getName());
        }
        System.out.println("++++++++++++++++");
        fla=3;//执行完了改变变量的值
        condition.signalAll();//执行完了唤醒其他线程
        lock.unlock();//解锁
    }
    public void print3() throws InterruptedException {
        lock.lock();//上锁
        if(fla!=3){
            condition.await();//不满足条件等待
        }
        for (int i = 0; i <3; i++) {
            System.out.println(Thread.currentThread().getName());
        }
        System.out.println("++++++++++++++++");
        fla=2;//执行完了改变变量的值
        condition.signalAll();//执行完了唤醒其他线程
        lock.unlock();//解锁
    }
}
 
 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值