JavaSE_多线程

1、基本概念(重点)

1.1 进程和线程

1.1.1程序:指令和数据的有序集合,其本身没有任何运行的含义,程序是一个静态的概念。

1.1.2 进程:是执行程序的一次执行过程,是一个动态的概念。是系统资源分配的单位。进程是一个动态的概念。另外每个进程都有一个独立的内存空间,数据不可与其他进程共享。

​ 一个进程可以包含有多个线程(如视频中同时听到声音、看到图像,还可以看弹幕)

​ 一个进程至少有一个线程,否则无存在的意义。

1.1.3 线程:CPU调度和执行的单位。一个进程中还可以有多个执行单元同时运行,这些执行单元可以看作程序执行的一天天线索,被称为线程。同一条进程中的多条线程,共享一个内存空间,线程之间可以自由切换,并发执行。

1.1.4 线程分类:守护线程与用户线程。

守护线程:是个服务线程,准确地来说就是服务其他的线程,当用户线程全部死亡时,守护线程自动死亡。
用户线程:平时用到的普通线程均是用户线程,当在Java程序中创建一个线程,它就被称为用户线程。

注意:很多多线程是模拟出来,真正的多线程是指有多个CPU,即多核,如服务器。如果是模拟出来的多线程,即在一个CPU的情况下,在同一个时间点,cpu只能执行一个代码,因为切换很快,所以就有同时执行的错觉。

面试题1:java中默认开启两条线程main线程和gc线程。
面试题2:提问?JAVA真的可以开启线程吗? 开不了的!

    public synchronized void start() {
        /**
         * This method is not invoked for the main method thread or "system"
         * group threads created/set up by the VM. Any new functionality added
         * to this method in the future may have to also be added to the VM.
         *
         * A zero status value corresponds to state "NEW".
         */
        if (threadStatus != 0)
            throw new IllegalThreadStateException();

        /* Notify the group that this thread is about to be started
         * so that it can be added to the group's list of threads
         * and the group's unstarted count can be decremented. */
        group.add(this);

        boolean started = false;
        try {
            start0();
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
                /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
            }
        }
    }
	//这是一个C++底层,Java是没有权限操作底层硬件的
    private native void start0();

Java是没有权限去开启线程、操作硬件的,这是一个native的一个本地方法,它调用的底层的C++代码。

1.2 同步与异步

同步:就是在发出一个功能调用时,在没有得到结果之前,该调用就不返回。也就是必须一件一件事做,等前一件做完了才能做下一件事。

异步:当一个异步过程调用发出后,调用者不能立刻得到结果,可以往下执行。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者。

1.3 并发与并行

并发: 某个时间段,多条线程操作同一个资源。

CPU 只有一核,模拟出来多条线程,天下武功,唯快不破。那么我们就可以使用CPU快速交替,来模拟多线程。

并行: 某个时间点,多个条线程并行执行。

CPU多核,多个线程可以同时执行。 我们可以使用线程池!

2、 线程的创建

2.1 基础Thread类

步骤:

① 定义类继承Thread;

② 复写Thread类中的run方法;

目的:将自定义代码存储在run方法,让线程运行

③ 调用线程的start方法:

该方法有两步:启动线程,调用run方法

 
public class Test extends Thread{
	public Test(String n){
		super(n);
	}
 
	/**
	 * 重写run方法
	 */
	@Override
	public void run() {
		for(int i = 0;i<10;i++){
			System.out.println("猪八戒:大师兄,不好了,师傅被妖怪抓走了");
			try {
				sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			
		}		
	}
	
	public static void main(String [] args){
		Test test = new Test("猪八戒线程");
		test.start();
				
	}
}

2.2 实现Runnable接口

步骤:
① 定义类实现Runnable接口

② 覆盖Runnable接口中的run方法

将线程要运行的代码放在该run方法中。

③ 通过Thread类建立线程对象。

④ 将Runnable接口的子类对象作为实际参数传递给Thread类的构造函数。
自定义的run方法所属的对象是Runnable接口的子类对象。所以要让线程执行指定对象的run方法就要先明确run方法所属对象

⑤ 调用Thread类的start方法开启线程并调用Runnable接口子类的run方法。

public class TestRunnable extends Thread{
	String name;
	
	public TestRunnable(String name){
		this.name = name;
	}
	
	@Override
	public void run() {
		// TODO Auto-generated method stub
		for(int i = 0;i<10;i++){
			System.out.println(name+",沙师弟:大师兄,不好了,师傅被妖怪抓走了");
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			
		}
	}
	
	public static void main(String [] args){
		TestRunnable test = new TestRunnable("沙师弟线程");
		test.start();
				
	}
}

2.3 实现Callable接口

在这里插入图片描述

步骤:
① 创建Callable接口的实现类,并实现call()方法,改方法将作为线程执行体,且具有返回值。

② 创建Callable实现类的实例,使用FutrueTask类进行包装Callable对象,FutureTask对象封装了Callable对象的call()方法的返回值

③ FutureTask使Runnable接口的一个实现类,所以可以使用FutureTask对象作为参数启动新线程。

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

public class CallableTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        for (int i = 1; i < 10; i++) {
//            new Thread(new Runnable()).start();
//            new Thread(new FutureTask<>( Callable)).start();
            MyThread thread= new MyThread();
            //适配类:FutureTask
            FutureTask<String> futureTask = new FutureTask<>(thread);
            //放入Thread使用
            new Thread(futureTask,String.valueOf(i)).start();
            //获取返回值
            String s = futureTask.get();
            System.out.println("返回值:"+ s);
        }
    }
}

class MyThread implements Callable<String> {

    @Override
    public String call() throws Exception {
        System.out.println("Call:"+Thread.currentThread().getName());
        return "String"+Thread.currentThread().getName();
    }
}

3、线程的六种状态

在枚举类Thread.State中就有六种状态的详细介绍。
在这里插入图片描述

NEW

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

RUNNABLE

运行状态 (runnable):
该状态包含就绪(Runnable)和运行(Running)两个状态。
就绪状态:当线程对象调用statr()方法后,线程并不代表它马上运行起来,而是先进入就绪状态。处于就绪状态位于可运行池中,此时它只是具备了运行的额条件,能否获得CPU的使用权,还需要等待系统的调度。
运行状态:如果处于就绪状态的线程获得了CPU的使用权,开始执行run()方法中的线程执行体,则该线程处于运行状态。
注意:就绪+运行=运行

BLOCKED

阻塞状态 (blocked):
阻塞状态只有在等待进入synchronized方法(块)和 其他Thread调用notify()或notifyAll(),但是还未获得锁才会进入。注意Lock中,是会让线程属于等待状态而不是阻塞,只有synchronized是阻塞。

注意:线程从阻塞状态只能进入就绪状态,而不能直接进入运行状态。

WAITING

等待状态(waiting):
处于这种状态的线程不会被分配CPU执行时间,它们要等待被显式地唤醒,否则会处于无限期等待的状态。

TIMED_WAITING

超时等待(timed_waiting):
处于这种状态的线程不会被分配CPU执行时间,不过无须无限期等待被其他线程显示地唤醒,在达到一定时间后它们会自动唤醒。

TERMINATED

终止状态(terminated):
1.当线程的run()方法完成时,或者主线程的main()方法完成时,我们就认为它终止了。这个线程对象也许是活的,但是,它已经不是一个单独执行的线程。线程一旦终止了,就不能复生。
2.在一个终止的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。

线程状态关系图

在这里插入图片描述

4 、线程的调度

概念:Java虚拟机会按照特定的机制为程序中的每个线程分配CPU的使用权,这种机制被称作线程的调度。

在计算机中,线程调度有两种模式,分别使分时调度模型抢占式调度模型

分时调度:所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。

抢占式调度:优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性), Java使用的为 抢占式调度。

CPU使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核心而言,某个时刻, 只能执行一个线程,而 CPU的在多个线程间切换速度相对我们的感觉要快,看上去就是在同一时刻运行。 其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的使用率更高。

5、常用方法

5.1 线程休眠(sleep)

人为地控制线程,使正在执行的线程暂停,将CPU让给别的线程,这时可以使用静态方法sleep(long mills),该方法可以让当前正在执行的线程暂停一段时间,进入休眠等待状态。

线程睡眠的方法(两个):
sleep(long millis)在指定的毫秒数内让正在执行的线程休眠。
sleep(long millis,int nanos)在指定的毫秒数加指定的纳秒数内让正在执行的线程休眠。

public class SynTest {
    public static void main(String[] args) {
        new Thread(new CountDown(),"倒计时").start();
    }
}
 
class CountDown implements Runnable{
    int time = 10;
    public void run() {
        while (true) {
            if(time>=0){
                System.out.println(Thread.currentThread().getName() + ":" + time--);
                try {
                    Thread.sleep(1000);                                                    //睡眠时间为1秒
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}


//结果:
倒计时:10
倒计时:9
倒计时:8
倒计时:7
倒计时:6
倒计时:5
倒计时:4
倒计时:3
倒计时:2
倒计时:1
倒计时:0

注意:sleep使线程暂停时不释放已获取的锁资源,如果sleep方法在同步上下文中调用,那么其他线程是无法进入到当前同步块或者同步方法中的。

5.2 线程让步(yield)

该方法和sleep方法类似,也是Thread类提供的一个静态方法,可以让正在执行的线程暂停,但是不会进入阻塞状态,而是直接进入就绪状态。相当于只是将当前线程暂停一下,然后重新进入就绪的线程池中,让线程调度器重新调度一次。也会出现某个线程调用yield方法后暂停,但之后调度器又将其调度出来重新进入到运行状态。

public class SynTest {
    public static void main(String[] args) {
        yieldDemo ms = new yieldDemo();
        Thread t1 = new Thread(ms,"张三吃完还剩");
        Thread t2 = new Thread(ms,"李四吃完还剩");
        Thread t3 = new Thread(ms,"王五吃完还剩");
        t1.start();
        t2.start();
        t3.start();
    }
}
class yieldDemo implements Runnable{
    int count = 20;
    public void run() {
        while (true) {
                if(count>0){
                    System.out.println(Thread.currentThread().getName() + count-- + "个瓜");
                    if(count % 2 == 0){
                        Thread.yield();                  //线程让步
                    }
            }
        }
    }
}

sleep和yield的区别:

① sleep方法声明抛出InterruptedException,调用该方法需要捕获该异常。yield没有声明异常,也无需捕获。

② sleep方法暂停当前线程后,会进入阻塞状态,只有当睡眠时间到了,才会转入就绪状态。而yield方法调用后 ,是直接进入就绪状态。

5.3 线程插队(join)

当B线程执行到了A线程的.join()方法时,B线程就会等待,等A线程都执行完毕,B线程才会执行。

join可以用来临时加入线程执行。

public static void main(String[] args) throws InterruptedException {    
        yieldDemo ms = new yieldDemo();
        Thread t1 = new Thread(ms,"张三吃完还剩");
        Thread t2 = new Thread(ms,"李四吃完还剩");
        Thread t3 = new Thread(ms,"王五吃完还剩");
        t1.start();
        t1.join();
        
        t2.start();
        t3.start();
        System.out.println( "主线程");
    }

5.4 中断标记(interrupt)

当一个线程被阻塞的时候(io, sleep等),我们取消这种阻塞,这个时候就可以使用interrupt。

package thread;
 
class Test extends Thread {
    boolean stop=false;
    public static void main( String args[] ) throws Exception {
        Test thread = new Test();
        System.out.println( "Starting thread..." );
        thread.start();
        Thread.sleep( 3000 );
        System.out.println( "Interrupting thread..." );
        thread.interrupt();
        Thread.sleep( 3000 );
        System.out.println("Stopping application..." );
        //System.exit(0);
    }
    public void run() {
        while(!stop){
            System.out.println( "Thread is running..." );
            long time = System.currentTimeMillis();
            while((System.currentTimeMillis()-time < 1000)) {
            }
        }
        System.out.println("Thread exiting under request..." );
    }
}
/**结果
Starting thread...
Thread is running...
Thread is running...
Thread is running...
Thread is running...
Interrupting thread...
Thread is running...
Thread is running...*/

在Thread.interrupt()被调用后,线程仍然继续运行。

使用Thread.interrupt()中断线程 :
Thread.interrupt()方法不会中断一个正在运行的线程。这一方法实际上完成的是,在线程受到阻塞时抛出一个中断信号,这样线程就得以退出阻塞的状态。更确切的说,如果线程被Object.wait, Thread.join和Thread.sleep三种方法之一阻塞,那么,它将接收到一个中断异常(InterruptedException),从而提早地终结被阻塞状态。

6、多线程同步

同步:Java中的同步指的是通过人为的控制和调度,保证共享资源的多线程访问成为线程安全,来保证结果的准确。

线程安全:线程安全问题其实就是由多个线程同时处理共享资源所导致的。要想实现线程安全做到保证某一个共享资源在任何时刻 只能有一个线程访问即可。

为什么使用同步?

多线程给我们带来了很大的方便,但是同时也给我们带来了一个致命的问题,当我们对线程共享数据进行非原子操作时,会带来知名的错误。当多个线程同时操作一个可共享的资源变量时(如数据的增删改查),将会导致数据不准确,相互之间产生冲突,因此加入同步锁以避免在该线程没有完成操作之前,被其他线程的调用,从而保证了该变量的唯一性和准确性。这就是多线程同步提上议程的原因,解决多线程安全问题。

举个例子:假设银行里某一用户账户有1000元,线程A读取到1000,并想取出这1000元,并且在栈中修改成了0但还没有刷新到堆中,线程B也读取到1000,此时账户刷新到银行系统中,则账户的钱变成了0,这个时候也想去除1000,再次刷新到行系统中,账号的钱变成0,这个时候A,B都取出1000元,但是账户只有1000,显然出现了问题。针对上述问题,假设我们添加了同步机制,那么就可以很容易的解决。

6.1 synchronized同步

synchronized锁对象主要有如下三种:

  • 普通同步方法,锁的是当前的实例对象
  • 静态同步方法,锁的是当前的Class对象
  • 同步代码块锁的是括号里面的对象

这里举一个经典卖票的例子:

package thread;

//线程同步synchronized
public class Demo {
    public static void main(String[] args) {
        Object o = new Object();
        //线程不安全
        //解决方案2  同步方法
        Runnable run = new Ticket();
        new Thread(run).start();
        new Thread(run).start();
        new Thread(run).start();
    }

    static class Ticket implements Runnable{
        //总票数
        private int count = 10;
        @Override
        public void run() {
                sale();
            }
        }
        public void sale(){
            if (count > 0) {
                //卖票
                System.out.println("正在准备卖票");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                count--;
                System.out.println(Thread.currentThread().getName()+"卖票结束,余票:" + count);
               
            }
        }
    }
}
//结果会出现余票为-1 -2等正常现象

6.1.1 synchronized代码块

package thread;
//线程同步synchronized
public class Demo {
    public static void main(String[] args) {
        Object o = new Object();
        //线程不安全
        //解决方案1  同步代码块
        //格式:synchronized(锁对象){
        //
        //
        //      }
        Runnable run = new Ticket();
        new Thread(run).start();
        new Thread(run).start();
        new Thread(run).start();
    }

    static class Ticket implements Runnable{
        //总票数
        private int count = 10;
        private Object o = new Object();
        @Override
        public void run() {
            //Object o = new Object();    //这里不是同一把锁,所以锁不住
                while (true) {
                    synchronized (o) {
                        if (count > 0) {
                         //卖票
                            System.out.println("正在准备卖票");
                            try {
                            Thread.sleep(1000);
                            } catch (InterruptedException e) {
                            e.printStackTrace();
                            }
                            count--;
                            System.out.println(Thread.currentThread().getName()+"卖票结束,余票:" + count);
                        }else {
                            break;
                        }

                }
            }
        }
    }
}

6.1.2 synchronized方法

package thread;

//线程同步synchronized

public class Demo {
    public static void main(String[] args) {
        Object o = new Object();
        //线程不安全
        //解决方案2  同步方法
        Runnable run = new Ticket();
        new Thread(run).start();
        new Thread(run).start();
        new Thread(run).start();
    }

    static class Ticket implements Runnable{
        //总票数
        private int count = 10;
        @Override
        public void run() {
		//判断票是否卖完,卖完即结束线程
            while (true) {
                boolean flag = sale();
                if(!flag){
                    break;
                }
            }
        }
        public synchronized boolean sale(){
            if (count > 0) {
                //卖票
                System.out.println("正在准备卖票");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                count--;
                System.out.println(Thread.currentThread().getName()+"卖票结束,余票:" + count);
                return true;
            }
                return false;

        }
    }
}

sychronized可重入锁的实现

每个锁关联一个线程持有者和一个计数器。当计数器为0时表示该锁没有被任何线程持有,那么任何线程都都可能获得该锁而调用相应方法。当一个线程请求成功后,JVM会记下持有锁的线程,并将计数器计为1。此时其他线程请求该锁,则必须等待。而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增。当线程退出一个synchronized方法/块时,计数器会递减,如果计数器为0则释放该锁。

6.2 Lock同步

概念:

公平锁: 十分公平,必须先来后到;

非公平锁: 十分不公平,可以插队;(Lock默认为非公平锁)

lock三部曲
1、 Lock lock=new ReentrantLock();
2、 lock.lock() 加锁
3、 finally=> 解锁:lock.unlock();

package thread;

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


//同步代码块和同步方法都属于隐式锁
//线程同步lock

public class Demo {
    public static void main(String[] args) {
        Object o = new Object();
        //线程不安全
        //解决方案1   显示锁  Lock  子类 ReentrantLock

        Runnable run = new Ticket();
        new Thread(run).start();
        new Thread(run).start();
        new Thread(run).start();
    }

    static class Ticket implements Runnable{
        //总票数
        private int count = 10;
        //参数为true表示公平锁    默认是false 不是公平锁
        private Lock l = new ReentrantLock(true);
        @Override
        public void run() {
            while (true) {
                l.lock();
                    if (count > 0) {
                        //卖票
                        System.out.println("正在准备卖票");
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        count--;
                        System.out.println(Thread.currentThread().getName()+"卖票结束,余票:" + count);
                    }else {
                        break;
                    }
                    l.unlock();
            }
        }
    }
}

Synchronized 和 Lock区别(注意)

1、Synchronized 内置的Java关键字,Lock是一个Java类

2、Synchronized 无法判断获取锁的状态,Lock可以判断

3、Synchronized 会自动释放锁,lock必须要手动加锁和手动释放锁!可能会遇到死锁

4、Synchronized 线程1(获得锁->阻塞)、线程2(等待);

lock就不一定会一直等待下去,lock会有一个trylock去尝试获取锁,不会造成长久的等待。

5、Synchronized 是可重入锁,不可以中断的,非公平的;Lock,可重入的,可以判断锁,可以自己设置公平锁和非公平锁;

6、Synchronized 适合锁少量的代码同步问题,Lock适合锁大量的同步代码;

非公平锁和公平锁在 reetrantlock 里是如何实现的?

非公平实现:

1.调用lock()方法时,首先去通过CAS尝试设置锁资源的state变量,如果设置成功,则设置当前持有锁资源的线程为当前请求线程

2.调用tryAcquire方法时,首先获取当前锁资源的state变量,如果为0,则通过CAS去尝试设置state,如果设置成功,则设置当前持有锁资源的线程为当前请求线程

以上两步都属于插队现象,可以提高系统吞吐量

公平实现:

1.调用lock()方法时,不进行CAS尝试

2.调用tryAcuqire方法时,首先获取当前锁资源的state变量,如果为0,则判断该节点是否是头节点可以去获取锁资源,如果可以才通过CAS去尝试设置state

上面通过判断该线程是否是队列的头结点,从而保证公平性

6.3 死锁

进程A中包含资源A,进程B中包含资源B,A的下一步需要资源B,B的下一步需要资源A,所以它们就互相等待对方占有的资源释放,所以也就产生了一个循环等待死锁。

public class DeadLock {
 
    public static void main(String[] args) {
        Thread t1 = new Thread(new DeadLockTest(true));
        Thread t2 = new Thread(new DeadLockTest(false));
        t1.start();
        t2.start();
    }
}
 
class DeadLockTest implements Runnable{
    
    private boolean flag;
    static Object obj1 = new Object();
    static Object obj2 = new Object();
    public DeadLockTest(boolean flag) {
        this.flag = flag;
    }
    public void run(){
        if(flag){
            synchronized(obj1){
                System.out.println("if lock1");
                synchronized (obj2) {
                    System.out.println("if lock2");
                }
            }
        }else{
            synchronized (obj2) {
                System.out.println("else lock2");
                synchronized (obj1) {
                    System.out.println("else lock1");
                }
            }
        }
    }
 }  

死锁形成的必要条件总结(都满足之后就会产生):

① 互斥条件:资源不能被共享,只能被同一个进程使用;

② 请求与保持条件:已经得到资源的进程可以申请新的资源;

③ 非剥夺条件:已经分配的资源不能从相应的进程中强制剥夺;

④ 循环等待条件:系统中若干进程形成环路,该环路中每个进程都在等待相邻进程占用的资源。

7、线程通信

线程通信的目的是为了能够让线程之间相互发送信号。另外,线程通信还能够使得线程等待其它线程的信号,比如,线程B可以等待线程A的信号,这个信号可以是线程A已经处理完成的信号。

方法声明功能描述
void wait()wait()的作用是让当前线程进入等待状态,同时,wait()也会让当前线程释放它所持有的锁。直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法,当前线程被唤醒(进入“就绪状态”)
void notify()唤醒当前对象上的等待线程,notify()是唤醒单个线程。
void notifyAll()唤醒当前对象上的等待线程,notifyAll()是唤醒所有的线程。
public class A {
    public static void main(String[] args) {
        Data data = new Data();

        new Thread(()->{for(int i=0;i<10;i++) {
            try {
                data.increment();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        },"A").start();
        new Thread(()->{for(int i=0;i<10;i++) {
            try {
                data.decrement();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }},"B").start();
    }
}
class Data{
    //数字  资源类
    private int number = 0;

    //+1
    public synchronized void increment() throws InterruptedException {
        if(number!=0){
            //等待操作
            this.wait();
        }
        number++;
        System.out.println(Thread.currentThread().getName()+"=>"+number);
        //通知其他线程 我+1完毕了
        this.notifyAll();
    }

    //-1
    public synchronized void decrement() throws InterruptedException {
        if(number==0){
            //等待操作
            this.wait();
        }
        number--;
        System.out.println(Thread.currentThread().getName()+"=>"+number);
        //通知其他线程  我-1完毕了
        this.notifyAll();
    }

}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值