一次搞懂并发编程

目录

1.基本概念

2.线程安全

3.锁

4.volatile

5.lock和reentrantlock

6synchronized

7.cas

8threadlocal

9.线程池


1.基本概念

并发,并行,串行 串行,在时间上不可能发生重叠,前一个任务没执行结束,后面的就得等着 ; 并行,在世界上可以重叠的,两个任务在同一时刻互不干扰的同时执行; 并发,允许两个任务彼此干扰,统一时间点,只有一个任务运行,交替执行;

1.基本概念 线程,本质就是函数的执行,是操作系统调度的最小单位。 进程,进程的计算机中的程序关于某数据集合上的一次活动运行,是操作系统分配资源的最小单位。

区别 1.一个进程包含多个线程,一个线程属于一个进程,

2.一个线程挂掉,对应的进程挂掉,一个进程挂掉,不会影响其他线程。

3.进程是系统资源调度点最短单位,线程是cpu调度的最小单位

4.进程系统开销显著大于线程系统开销,线程需要的系统资源更少

5.进程在执行时拥有独立的内存单元,多个线程共享进程的内存,如代码段,数据段,拓展段等,每个线程拥有自己的段栈和寄存器组。

守护线程就和保镖一样,为所有的非守护线程提供服务的线程 。 垃圾回收线程就是一个守护线程,当我们的程序不再有任何运行的线程,程序也不会再产生垃圾,垃圾回收线程就自己就结束了。始终在稍微低一个级别的状态中运行,用于时实监控和管理系统中的回收资源。 比如说要给其它线程提供服务,则可以使用守护线程。在任何情况下,程序结束了,这个线程必须立刻正常关闭。也可以看做是守护线程。

使用thread.setdamen(true)就可以吧这个thread设置为守护线程 ,这个设置必须在线程运行之前设置,不然会报异常 。不能吧正在运行的常规线程设施为守护线程

协程为也叫微线程,协助线程执行的特殊函数,既不是进程也不是线程。可以随时挂起,随时继续运行的 。

2.线程状态 线程的生命周期有五中状态,新建 就绪,运行,阻塞,死亡 新建就是调用new后,新创建点线程状态。 就绪就是线程在调用start方法后,不会马上进入运行状态,而是等待cpu调度分配资源,那个线程先抢到了资源,那个线程就优先执行。 运行,当就绪的线程被cpu调度并获得资源的时候,就进入了运行状态,run方法里面是线程执行体。里面有线程的操作和功能。 阻塞,当线程在运行状态时,可能因为某些原因导致线程由运行变为了阻塞,比如说io请求呀,或者执行sleep,wait方法等,线程就处于阻塞状态,需要其他机制将线程由阻塞状态唤醒,比如调用notify或者notifyall方法,唤醒的线程不会马上就是运行状态,而是要再次等待cpu调度分配资源。 死亡,线程执行完了或者发生异常,会导致线程死亡,并且释放资源。

3创建方式 继承thread类,实现runnable借口,实现callable接口。前面两个是重写run方法,最后一个是重写call方法。 runable和callable区别,callable可以有返回值,可以抛出异常,rinable不可以。

4.线程运行的原理 线程的运行主要依靠栈与栈帧 栈是一种基本的数据结构,后进先出,里面存放的是函数的调用 栈帧里面存放的数据,线程在栈帧里面怎么运行。 栈帧里面包含局部变量表,操作数栈,动态连接地址,返回地址等。每个栈帧都是线程私有的,每个线程都有一份,互相不通用,不干扰。局部变量表,存在的是变量的数据,例如main方法的变量是args。main方法调用其他的方法,那么会切换到其他方法的栈帧,操作数栈就是里面的变量,例如方法里面定义的属性a=10,等待。这些对象都是在堆中放着,这里只是一个引用而已。线程会有返回值,指向的就是对应的线程,也就是返回地址。动态链接地址就是那个引用。

5.线程的上下文切换

线程的上下文切换是cpu不再指向当前线程,转而执行别的线程。

发生线程上下文切换的场景:

1.线程的cpu时间片用完了

2.垃圾回收的时候

3.有更高的优先级的线程需要执行的时候

4.执行wait,sleep,yeild等的时候。

发生线程上下文切换的时候,需要操作系统保存当前线程的状态,并恢复另一个线程的状态,在java中也就是程序计数器,他需要直到这个线程执行到第几行了。

发生线程上下文切换会影响性能的。尽量少切换。

6.一些方法 run和start区别 1.线程中的start()方法和run()方法的主要区别在于,当程序调用start()方法,将会创建一个新线程去执行run()方法中的代码。但是如果直接调用run()方法的话,会直接在当前线程中执行run()中的代码,注意,这里不会创建新线程。这样run()就像一个普通方法一样。 2.另外当一个线程启动之后,不能重复调用start(),否则会报IllegalStateException异常。但是可以重复调用run()方法。 总结起来就是run()就是一个普通的方法,而start()会创建一个新线程去执行run()的代码。

sleep 1.调用sleep会让当前线程从Running状态进入Time Waitting状态。

2.其他线程可以使用interrupt方法打断正在睡眠的线程,此时sleep方法会抛出interruptException异常。

3.睡眠结束后的线程未必会马上被执行

join 因为新的线程加入了我们,所以我们要等他执行完再出发。Join 会让主线程等待子线程

yeild 1.调用yeild可以让线程从running状态进入Runnable状态,然后调度其他的相同优先级的线程,如果没有相同优先级的线程的化,可能线程不会暂停。 2.他是根据操作系统的任务调度器来实现的。 线程的优先级,就是分轻重缓急,优先级高的先执行,低的后执行。

wait和sleep区别 wait和sleep方法都是让当前线程放弃cpu的使用权进入阻塞状态。 wait方法的object类里面的方法,而sleep方法是thread类里面的方法。 执行wait(long)和sleep方法的线程都会在等待相应的时间后醒来,而wait方法没有参数会一直在等待,直至被notofy或者notifyall唤醒。 wait方法的调用必须先获取wait对象的锁,而sleep 。

2.线程安全

1.怎么理解线程安全

1.多个cpu,每个内核中都会有一个高速缓存,每个高速缓存数据不可见 可见性

2.多线程有IO操作,耗时较长,操纵系统需要切换线程执行 原子性

3.操纵系统对指令进行优化,会打乱指令的执行顺序。 有序性

缓存导致的可见一致问题

编译优化带来的有序性问题

线程切换带来的原子性问题

2.怎么保证线程安全

我知道的有,原子类,volatile,锁可以保证线程安全。 其中原子类主要的automic包下有一些方法 ,他们功能是原子更新数据 例如更新基本类型呀,引用类型呀 数组呀,属性等等。他们要遵从比较和替换原则,比较要替换的值是否等于预期值,如果是就更新 不是就不更新。

volatile 是关键字,是轻量级的synchronized,他可以保证多线程的可见性和有序性,不能保证原子性。可见性是由于cpu核心的缓存导致 每个核心分别执行线程,每个核心都有自己的缓存 ,而这些缓存都要与内存进行同步才行。 volatile修饰的属性在操作系统层面会进行处理。在读操作的时候会强制吧该线程的本地内存置为无效 迫使他去读内存中的共享变量,而在写操作的时候,该线程本地内存中共享变量的值会立刻刷新到内存中。

原子类和volatile只能保证单个共享变量的线程安全。 而锁可以保证临界区内多个变量的线程安全,加锁方式有synchronized关键字和lock接口 。

还有其他可以实现线程安全的 例如,1.首先是把数据设置为非共享的,就不存在线程安全,2.还可以吧数据设计为不可变的 例如string类呀 3还可以使用一些工具类,juc包里面有semaphore类,也就是信号量,可以控制同时访问某个恭喜资源的线程个数。

countdownlatch类,允许一个或者多个线程等待其它线程完成操作。 c

yclicbarrier,让一组线程到达屏障时被阻塞,直到最后一个线程到达屏障时,屏障才会打开,所有被屏障拦截的线程才会执行。

还有就是使用threadlocal存储变量,threadlocal可以很方便的为每一个线程单独存一份数据,也就是说需要并发访问的资源复制多份,这样可以避免多线程访问共享变量,他们访问的都是自己的那一份数据,隔离了多个线程直接到数据共享。

3.锁

Java中的锁:不全是指锁,有的指的是锁的特性,有的是锁的状态,有的是锁的设计方式。

乐观锁:采用CAS机制,乐观的认为不加锁是没有问题的 原子类就是乐观锁

悲观锁:就是真正的加锁实现,认为不加锁是会存在问题的 可重入锁:又名递归锁,当一个线程进入外层方法获取锁时,如果内存调用另一个需要获得该锁修饰的方法,那么线程是可以进入的。 读写锁:里面维护两个锁实现,一个是写锁,一个是读锁,如果是写锁,一次只能有一个线程获得锁,如果是读锁,则可以允许多个线程获得写锁,写锁优先于读锁。

分段锁:不是具体的锁,将锁的粒度分的更小,以提高并发效率。

自旋锁:不断重试去尝试获得锁,不会让线程进入阻塞状态,提高效率但是耗CPU

独占锁:ReentrantLock,synchronized读写锁中的写锁,或者叫互斥锁,一次只允许一个线程获得锁。

共享锁:读写锁中的读锁是共享的,可以有多个线程同时获得锁

公平锁:可以按照请求的顺序分配锁,ReentrantLock中有公平锁的实现,里面维护一个队列,按顺序排队获得锁

非公平锁:不按照请求顺序分配锁,Synchronized就是非公平锁,ReentrantLock中默认是非公平锁无所状态/偏向锁/轻量级锁/重量级锁:

偏向级锁:只有一个线程访问一段同步代码,此时会将线程的id存入对象头,下次该线程来的时候直接分配即可。

轻量级锁:当锁的状态为偏向级锁时,又有线程进行访问,那么锁状态升级为轻量级锁,不会让线程进入阻塞状态,而是自旋尝试获得锁以提高效率。

重量级锁:当锁的状态为轻量级锁时,如果线程太多,且自旋次数达到一定数量,则锁状态升级为重量级锁,线程进入阻塞,由操作系统调度分配。

2.怎么理解悲观锁与乐观锁

1)悲观锁的代表是synchronized和lock锁,其核心思想是线程只有占有了锁,才能去操作共享变量,每次只有一个线程占锁成功,获取锁失败的线程都得等待 。线程从运行到阻塞,再从阻塞到唤醒,涉及线程上下文切换,如果频发发生会影响性能。实际上线程在获取synchronized和lock锁时,如果锁已经被占用,都会做几次重试操作,减少阻塞的机会。

2)乐观锁的代表是AtomicInteger,使用cas来保证原子性。其核心思想是无需加锁,每次只有一个线程能成功修改共享变量,其他失败的线程不需要停止,不断重试直至成功。由于线程一直运行,不存在阻塞,也就没有上下文切换的情况。他需要多核cpu的支持,且线程数不应该超过cpu核数。

4.volatile

volatile能否保证线程安全 线程安全问题要保证三个方面,可见性,有序性和原子性。 可见性的话是指一个线程对共享变量的值进行修改,另一个线程能看到最新的结果。 有序性是指一个线程内的代码是按照编写顺序来执行的。 原子性是指一个线程内的多行代码以一个整体来运行,期间不能有其它的代码插队。 volatile能够保证共享变量的有序性和可见性,但并不能保证原子性。 volatile实现可见性 volatile修饰的属性在操作系统层面会进行处理。在读操作的时候会强制吧该线程的本地内存置为无效 迫使他去读内存中的共享变量,而在写操作的时候,该线程本地内存中共享变量的值会立刻刷新到内存中 这一块也是内存屏障实现的,一类是强制读取主内存,强制刷新主内存的内存屏障,叫做Load屏障和Store屏障。

volatile实现有序性 禁止指令重排序 volatile也是通过内存屏障来禁止指令冲排序的。jmm内存屏障策略是在volatile写操作之前插入一个storestore屏障,后面插入一个storeload屏障 。在volatile读操作之前插入一个loadload屏障,在之后插入一个loadstore屏障。例如storestore屏障,他会讲两边的store指令分割开,而loadload屏障他会将两边的load指令分割开。其他的同理

5.lock和reentrantlock

reentrantlock是基于aqs实现的一个可重入的互斥锁,,aqs的基础又是cas。

reentrantlock里面有三个静态内部类,分别继承了aqs的抽象内部类sync,以及sync的内部类nonfairsync和fairsync分别代表非公平锁和公平锁。默认是非公平锁的,可以在构造方法传入true参数来开启公平锁。

aqs是队列同步器,是用来构建锁的基础框架,lock实现类都素基于aqs实现的。 aqs是基于模板方法模式进行设计的,所以锁的实现需要继承aqs并重写它指定的方法,aqs内部定义了一个FIFO队列来实现线程的同步,同时还定义了同步状态来记录锁的信息。

aqs的模板方法将管理同步状态的逻辑提炼出来形成标准流程,这些方法包括,独占是获取同步状态,独占式释放同步状态,共享式获取同步状态,共享式释放同步状态 以独占式获取同步状态为例,流程:

1.尝试以独占式获取同步状态, 2.如果状态获取失败,则将当前线程加入到同步队列。 3.自旋处理同步状态,如果当前线程位于队列头部,则唤醒她并让他让出队列,否则使其进入阻塞状态。 其中有些步骤父类不能实现,留给子类根据实际情况区实现。

例如公平锁和非公平锁就是两种不同的方式。 aqs的同步队列,是一个双向链表,aqs则是持有链表的头尾节点,对于尾节点的设置是存在多线程竞争的,采用cas。头节点的设置,一定是拿到了同步状态的线程才能处理,不需要cas。 aqs的同步状态是一个int类型的整数,是被voiaitle修饰的,他表示状态的同时还能表示数量,为0表示无锁,大于0表示锁的重入次数。 在读写锁的场景中,这个状态标志既要记录读锁又要记录写锁,于是锁的实现者就将锁的状态分为高低两部分,高位存读锁,低位存写锁。

lock和synchronized区别 synchronized是同步锁,可以修饰静态方法、普通方法和代码块。

修饰静态方法时锁住的是类对象,修饰普通方法时锁住的是实例对象。

当一个线程获取锁时,其他线程想要访问当前资源只能等当前线程释放锁。

synchronized是java的关键字,Lock是一个接口。

synchronized可以作用在代码块和方法上,Lock只能用在代码里。

synchronized在代码执行完或出现异常时会自动释放锁,Locl不会自动释放,需要在finally中释放。

synchronized会导致线程拿不到锁一直等待,Lock可以设置获取锁失败的超时时间。 synchronized无法获知是否获取锁成功,Lock则可以通过tryLock判断是否加锁成功。

6synchronized

synchronized是什么

​​​​​​​ sync是关键字,可以修饰静态方法,普通方法,代码块,一次只允许一个线程进入。 synchronized默认是可重入锁并且是隐式锁,显式锁是Lock,Reentrantlock也是可重入锁。 他是隐私锁意思就是不用去调lock方法加锁 synchronized还是非公平锁。

synchronized怎么用 sync在修饰静态方法的时候,锁住的是当前类的class对象。 sync在修饰普通方法的时候,锁住的是当前的实例this sync在修饰代码块的时候,则需要在关键字后面的小括号里面指定一个对象作为锁住的东西。 作用域代码块,对括号里配置的对象进行加锁。作用于静态方法,进去同步代码前要获得当前类对象的锁。

synchronized怎么实现的

synchronized实现可重入锁:ObjectMonitor中有几个关键属性,里面有一个代表的是锁的重入次数,还有一个count,记录获得锁的数量。每个锁对象拥有一个锁计数器和一个指向改锁的线程的指针,当指向 monitorenter进入锁的时候,计数器为0,则代表没有被其他线程占用,虚拟机会将该锁对象持有线程设置为当前线程,并且锁计数器+1,再一次加锁就要重入了,直到持有的线程释放了这个锁,当指向monitorexit时,锁-1,为0时代表锁全部被释放。

sync是采用java对象头来存储锁的信息,而且还支持锁升级。 java的对象头分为三部分:分别是markword,classmetadata area,arraylenth。 markword是对象头,用来存储对象的hashcode以及锁信息等, classmetadata area是用来存储对象类型的指针,而array'length则用来存储数组对象的长度,如果对象不是数组,则没有arraylength。 sync的锁信息包含锁的标志和锁的状态,这些信息都在markword中。

sync锁升级机制: 为了减少锁的释放和获取锁带来的性能消耗,引入了偏向锁和轻量级锁,无锁和重量级锁四种状态。随着线程竞争的升级,锁会从无锁状态升级到重量级锁状态。锁可以升级不能降级。

偏向锁顾名思义就是锁偏向于某一个线程,当一个线程访问同步代码块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程id,以后该线程再次进入或者退出同步块时就不需要做加锁和解锁处理,只需要简单测试一下markword里面是否存储这对应的线程id即可。

轻量级锁:就是加锁的时候,jvm先在当前线程栈帧中创建用于存储锁记录的空间,并将markword复制到锁记录中,然后线程尝试以cas方式将markword替换为指向锁记录的指针,如何成功则当前线程获得锁,如果失败,则表示其他线程在竞争锁,此时当前线程会通过自旋来尝试获取锁。

锁升级的流程: 1.开始没有任何线程访问同步块,此时处于无锁状态。

2.然后,线程1访问同步块,他以cas的方式修改markword尝试添加偏向级锁,此时没有竞争,所以偏向级锁添加成功,此时markword里面存的是线程1的id。 

3.然后线程2访问同步块,他也以cas的方式添加偏向锁,此时存在竞争,片共享锁添加失败,于是线程2会发起撤销偏向锁的流程,也就是清除线程1的id,于是同步块从偏向线程1状态回复到公平竞争的状态。

4.线程1和线程2公平竞争,他们同时尝试添加轻量级锁,只有一个会成功,另一个失败了,但是不会放弃,他认为成功的马上会指向完,马上轮到他了,所以失败的继续自旋加锁。

5.最后,如果成功的很快指向完,失败的就会成功的加轻量级锁,不会晋级到重量级锁。如果成功的执行的慢,则失败的自旋一定次数后,发起锁膨胀的流程,失败的会加重量级锁,失败的线程会进入阻塞状态。成功的线程执行完了之后释放锁,唤醒失败的线程执行任务。

7.cas

CAS:比较和交换,是一种乐观锁(没有采用加锁的方式)实现,采用自旋的思想,一遍一遍的尝试,去进行比较。 在这里就是假如说内存有一个数据a=1 ,你第一次读了a=1,然后操作了之后a=2但是还没有保存操作,然后再查一下内存里的a是几,如果是1,则表示这段时间没人操作a ,不为1,则表示数据变了。

内部有三个值:V内存值,操作前先将内存读到工作内存A:预估值,在工作内存修改了变量值后,将要将修改后的值向主内存写入的时候再次读取的主内存数据。B:内部操作后的变量值当向主内存写入数据时,必须满足A==V,就V=B,否则就再次读入主内存值。

缺点:CAS是无锁的,采用自旋方式,线程不会阻塞,如果有大量的线程进行尝试,那么cpu的消耗较大,适用于并发量较小的情况。

CAS的缺点就是解决不了aba问题。 ABA问题:就是内存值由A变为B,再由B变为A,CAS不知道内存值已经发生过修改。解决ABA:操作值的时候顺便添加一个版本号即可。比较版本号即可

8threadlocal

threadlocal是线程变量,他将需要并发访问的资源复制多份,让每个线程拥有一份资源,由于每个线程都拥有自己的资源副本,从而也就没必要对改变量进行线程同步了。

threadlocal提供线程安全的共享机制,在编写多线程代码的时候,可以吧不安全的共享变量封装进threadlocal中。 在实现上,thread类中声明了threadlocals变量,用于存放当前线程独占的资源。threadlocal类中定义了改变量的类型threadlocalmap 了,也是一个类似于map的结构,用于存放键值对 。threadlocal中还提供了get和set方法,set方法会初始化threadlocalmap并将其刚那个threadlocals变量中,从而将传入的值绑定到当前线程中。在数据存储上,传入的值将作为键值对的value,而key是threadlocal对象本身也就是this。get方法没有任何参数,他会以当前threadlocal对象为key,拿到map中的value。

注意的是,threadlocal并不能代替同步机制,同步机制是为了同步多个线程对相同资源的并发访问,是多个线程之间通信的有效方式。而threadlocal是隔离了多个线程之间的数据共享,从根本上避免了多个线程对共享资源的竞争,也就是不需要对多个线程进行同步。一般情况下,如果多个线程需要共享资源达到线程通信的目的用同步机制。如果只是隔离数据共享 避免冲突,则可以使用threadlocal。

threadlocal可能会出现内存泄露,为什么出现内存泄露?解决? key被弱引用,value被强引用与threadlocal是强关联,key被回收掉了值还存在,导致内存泄漏

解决办法:用完变量之后,调用remove()方法,直接删除键值对象。

9.线程池

什么是线程池

线程池就是创建若干个可执行的线程放入一个池(容器)中,有任务需要处理时,会提交到线程池中的任务队列,处理完之后线程并不会被销毁,而是仍然在线程池中等待下一个任务。

使用线程池的好处?

重复利用线程,降低线程创建和销毁带来的资源损耗,统一管理线程,线程而创建和销毁都是由线程池来进行管理,提高响应速度,线程一但创建完成,就可以使用了。

线程池的创建方法 可以通过threadpoolexecute类中的newCachedThreadPool可以创建一个缓存线程池。newFixedThreadPool可以创建一个定长线程池,newSingleThreadExecutor创建一个单线程线程池,newScheduledThreadPool周期性任务定长线程池。

线程池的原理?提交过程 1.如果此时线程池中的数量小于corePoolSize,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务。 2.如果此时线程池中的数量等于corePoolSize,但是缓冲队列workQueue未满,那么任务被放入缓冲队列。 3.如果此时线程池中的数量大于等于corePoolSize,缓冲队列workQueue满,并且线程池中的数量小于maximumPoolSize,建新的线程来处理被添加的任务。 4.如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量等于maximumPoolSize,那么通过 handler所指定的策略来处理此任务。 5.当线程池中的线程数量大于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止。这样,线程池可以动态的调整池中的线程数。 总结即:处理任务判断的优先级为 核心线程corePoolSize、任务队列workQueue、最大线程maximumPoolSize,如果三者都满了,使用handler处理被拒绝的任务。

线程池的参数?

​​​​​​​ 1)corePoorSize核心线程数目,最多保留的线程数目,

2)maximumPoorSize最大线程数目,核心线程+救急线程,

3)keepAliveTime生存时间,针对救急线程,

4)unit时间单位,针对救急线程,

5)workQueue阻塞队列,

6)threadFactory线程工厂,可以为线程创建时取个好的名字

7)handler拒绝策略,四种。

拒绝策略。 1.报错 2.谁调用线程,谁执行 3.丢弃等待时间长的 4.直接丢弃

线程池有哪些状态? 1.RUNNING,线程池的初始化状态,可以添加待执行的任务。 2.SHUTDOWN,线程池处于关闭状态,不接受新的任务,仅处理正在接收的任务。

3.stop,线程池立即关闭,不处理新任务,放弃缓存队列的任务,中断正在执行的任务 4.TIDYING,线程池自主整理状态,调用 terminated() 方法进行线程池整理。

5.TERMINATED:线程池终止状态。

submit(和 execute两个方法有什么区别?

excute方法没有返回值,submit方法有返回值 shutdownNow() 和 shutdown() 两个方法有什么区别? shutdown不会马上停止,等待队列里面的缓存任务执行完了再退出。 shutdownnow立即停止,如果有正在执行的线程,会报sleep interrupted 异常

常见的线程池那些 cachedthreadpool缓存线程池,如果线程池的长度超过处理需要,可灵活回收空闲线程,若无可回收,则创建新的线程。

NewSingleThreadPool单线程线程池,线程池中心只有一个线程,线程执行完任务立即回收,只会用唯一的工作线程来执行任务,保证所有的任务按照指定顺序。

NewFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待 NewScheduleThreadPool 创建一个定长线程池(普通线程数量无线),适用于定时及周期性任务执行blockingqueue的理解 那些实现类

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值