java多线程与线程同步

java多线程与线程同步

一个对象是否安全取决于是否被多个线程访问,最终依旧能获得预期的效果。

框架通过在框架线程中调用应用程序代码将并发性引入到程序中。在代码中将不可避免地访问应用程序状态,因此所有访问这些状态的代码路径都必须是线程安全的。

创建多线程的方法

有两种方法来创建一个新的执行线程。一是声明一个类是一类Thread。这类应重写类Threadrun方法。子类的一个实例可以被分配和启动。

(面试)使用那种创建线程更好?

  • Runnable

  • Thread

使用Runnable 更好

1.从代码架构角度

2.新建线程的损耗

3.java不支持双继承

  • 解耦的角度用Runnable更好
  • Thread会单独创建一个线程–节约资源
  • 使用Thread 不能多继承 --大大限制了扩展性
  • 框架可以进行 线程池优化

(面试)创建线程有几种方式?

通常来说我们分为两类:Oracle也是这么说的

准确来说,创建一个线程只有一种方法那就是使用Thread类实现线程的执行单元只有两种。

  • 方法一:实现Runnable接口的run方法,并把Runnable实例传给Thread类
  • 方法二:重写Thread的run()方法(继承Thread类)

错误启动线程的观点:

  • 1.线程池是一种线程创建方式
package xyz.amewin.thread;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @author Amewin
 * @author 匿名内部类方法
 * @date 2020/8/17 19:22
 */
public class TestThead2  {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();

        for (int i = 0; i <1000 ; i++) {
            executorService.submit(new Tack(){});
        }
    }


}
class Tack implements Runnable{

    @Override
    public void run() {
        System.out.println("--------");
        try{
            Thread.sleep(500);
        }catch (Exception e){

        }
        System.out.println(Thread.currentThread().getName());
    }
}

  • 2.定时器也是新的线程创建方式

  • 3.匿名内部类

  • 4.lambda表达式

/**
 * 描述:     lambda表达式创建线程
 */
public class Lambda {

    public static void main(String[] args) {
        new Thread(() -> System.out.println(Thread.currentThread().getName())).start();
    }
}


启动线程 Run()与Start()的区别?


启动主线程必须使用start()间接调用run方法

  1. Start()启动新线程 --请求jvm启动 --由线程调度器统一启动

  2. 准备工作

    1. 启动新线程检查线程状态
    2. 加入线程组
    3. 调用start0()
  3. 不能重复调用Start() --java.lang.IllegalThreadStateException

    1. 线程状态不符合规定

      1. 初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
      2. 可运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。
        线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。
      3. 阻塞(BLOCKED):表示线程阻塞于锁。
      4. 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
      5. 超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。
      6. 终止(TERMINATED):表示该线程已经执行完毕。

既然start()方法会调用run()方法,为什么我们选择调用start()方法,而不是直接调用run()方法呢?


run只是一个普通的方法 start才是真正意义上的创建一个新的线程


如何停止线程?

  • interruot


使用interrupt来通知,而不是强制

通常线程会在什么情况下停止普通情况?
  1. 线程可能被阻塞
    1. 阻塞报道异常
    2. java.lang.InterruptedException: sleep interrupted

使用try catch 进行异常处理

阻塞代码sleep被调用是会 抛出一个异常sleep interrupted sleep会检测并会清除

try {
while (num <= 3000 &&!Thread.currentThread().isInterrupted()) {
    if (num % 100 == 0) {
     System.out.println(num + "是100的倍数");
    }
    num++;
  }
  Thread.sleep(1000);
} catch (InterruptedException e) {
          e.printStackTrace();
   }
2.如果线程在每次迭代后都阻塞
正确的停止线程的方法(Interrupt)<通知中断>
  1. whlie 内try/catch的问题

    java设计时当线程出现异常会把java.lang.InterruptedException: sleep interrupted清除

    导致无法使用Thread.currentThread().isInterrupted()在whlie循环内无法停止线程

  2. 实际开发中两种最佳实践方法

    1. 最佳实践:catch了InterruptedExcetion之后的优先选择:在方法签名中抛出异常 那么在run()就会强制
    2. 佳实践2:在catch子语句中调Thread.currentThread().interrupt()来恢复设置中断状态,以便于在后续的执行中,依然能够检查到刚才发生了中断* 回到刚才RightWayStopThreadInProd补上中断,让它跳出
  3. 响应中断的方法总结

正确停止的方法:

package xyz.amewin.stopthreads;
/**
 * 描述:     最佳实践:catch了InterruptedExcetion之后的优先选择:在方法签名中抛出异常 那么在run()就会强制try/catch
 */
public class RightWayStopThreadInProd implements Runnable {

    @Override
    public void run() {
        //判断线程是否终止  !Thread.currentThread().isInterrupted()
while (true && !Thread.currentThread().isInterrupted()) {
            System.out.println("go");
            try {
                throwInMethod();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                //保存日志、停止程序
                System.out.println("保存日志");
                e.printStackTrace();
            }
        }
    }

    private void throwInMethod() throws InterruptedException {
            Thread.sleep(2000);
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new RightWayStopThreadInProd());
        thread.start();
        Thread.sleep(1000);
        //向jvm提出中断线程
        thread.interrupt();
    }
}

Thread常用方法
 		threadOne.start();//启动线程
        //设置中断标志 -- 中断这个线程。
        threadOne.interrupt();
        //测试当前线程是否已被中断。--测试当前线程是否已被中断。 
        threadOne.interrupted();
		/**
		 *注意 interrupted 该方法为静态方法
		 * --从属于当前类,无法响应别的线程--
		**/
        //获取中断标志
		threadOne.isInterrupted();
        //获取中断标志并重置
		threadOne.interrupted();
        //获取中断标志并重直 ---测试当前线程是否已被中断。
		Thread.interrupted();
        //获取中断标志
		threadOne.isInterrupted();
  • stop

会导致运行的线程突然停止,没办法完成一个基本的单位的操作,或造成脏数据

  • suspend(线程挂起)

如果别的线程不及时唤醒线程,容易造成锁未解放,容易造成死锁。

  • resume(线程挂起)

  • volatile 设置boolean标记位 --JMM方式理解

演示用volatile的局限part2 陷入阻塞时,volatile是无法线程的此例中,生产者的生产速度很快,消费者消费速度慢,长时间阻塞队列满了以后,生产者会阻塞,等待消费者进一步消费

介绍

  • 不保证原子性
  • 保证不发生重排序
  • 可见性 (线程之间同步,某一个线程进行修改时,则会则会使其他使用该变量的线程的属性失效)

使用场景

线程之间的某一属性赋值

不适合使用场景

i++ ;流程之间的判读

注意

double long 数据类型不需要保证volatile 其底层实现就已经,完成了原子性的实现

Sychronized的作用

同步方法支持一种简单的策略来防止线程干扰和内存一致性错误:如果一个
对象对多个线程可见,则对该对象变量的所有读取或写入都是通过同步方法
完成的。

能够保证在同一时刻最多只有一个线程执行该段代码,以达到保证并
发安全的效果。


Synchronized的地位

  1. Synchronized是Java的关键字,被Java语言原生支持

  2. 是最基本的互斥同步手段

  3. 是并发编程中的元老级角色,是并发编程的必学内容

Sychronized的用法

  • 1.对象锁

    • 类锁、方法锁和同步代码块锁(自己指定对象锁)

    使用方法:

    1. 使用this关键字
    2. 新建object = new object();
Object obj = new Object();

@Override
    public void run() {
        //对象锁
        //synchronized (obj){
        synchronized (object){
            for (int j = 0; j < 10000; j++) {
                i++;
            }
        }
    }
  • 方法锁
  //方法锁
    private synchronized void sumNum() {
        //打印线程名称
        System.out.println(Thread.currentThread().getName());
        for (int j = 0; j < 10000; j++) {
            i++;
        }
        System.out.println("线程结束");
    }
  • 2.类锁
    • 指sychronized修饰的类中的静态方法锁定的class

img


线程出现异常解决方案

  • 优先选择传递中断 – 放置在主函数中<最上层处理>
    • 保存日志、处理中断
  • 不想活无法传递:恢复中断
  • 不应屏蔽中断
    • 底层函数不因处理由顶层函数处理

synchronized面试题

1.两个线程同时访问一个对象的同步方法?

​ 会同步锁,访问同一个锁

2.两个线程访问的是两个对象的同步方法?

​ 互不干扰 --是两个锁

3.两个线程访问的是synchronized的静态方法?

​ 静态类方法从属于类 --所以该方法同步

4.同时访问同步方法与非同步方法?

​ 非同步方法不受影响

5.访问同一个对象的不同的普通同步方法

​ 会出现锁之间的串行情况

6.同时访问静态synchronized和非静态synchronized方法?

​ 依旧可以同时运行,因为这是两把不同锁。

7.方法抛异常会释放锁?

​ jvm会自己释放锁

总结
  1. 一把锁只能同时被一个线程获取,没有拿到锁的线程必须等待(对
    应第1、5种情况) ;
  2. 每个实例都对应有自己的一把锁,不同实例之间互不影响;例外:
    锁对象是*.class以及synchronized修饰的是static方法的时候, 所有
    对象共用同一把类锁(对应第2、3、4、6种情况) ;
  3. 无论是方法正常执行完毕或者方法抛出异常, 都会释放锁(对应第7
    种情况)
Synchromized 性质
  1. 可重入

什么是可重入?:

指的是同一线程的外层函数获得锁之后, 内层函
数可以直接再次获取该

同一线程的继承和接口有权访问被锁定的对象

在java内部,同一线程在调用自己类中其他synchronized方法/块或调用父类的synchronized方法/块都不会阻碍该线程的执行,就是说同一线程对同一个对象锁是可重入的,而且同一个线程可以获取同一把锁多次,也就是可以多次重入

好处:避免死锁,提升封装性

粒性:线程而非调用(用3种情况来说明和pthread的区别)

​ 2. 不可中断

一旦这个锁已经被别人获得了,如果我还想获得,我只能选择等待或者
阻塞,直到别的线程释放这个锁。如果别人永远不释放锁,那么我只能永
远地等下去。

原理

1.加锁和释放锁的原理: 现象、时机、深入JVM看字节码

2.可重入原理:加锁次数计数器

3.保证可见性的原理:内存模型

可重入原理:加锁次数计数器
◆JVM负责跟踪对象被加锁的次数
◆线程第一次给对象加锁的时候,计数变为1。每当这个相同的线程在此对象.上再次获得锁时,计数会递增
◆每当任务离开时 ,计数递减,当计数为0的时候,锁被完全释放

反编译说明

被synchronized修饰的代码块,会被JVM锁定,只有被解锁之后才能操作。

synchronized的缺陷

1.效率低

效率低:锁的释放情况少、试图获得锁时不能设定超时、不能中
断一个正在试图获得锁的线程

2.不够灵活(读写锁更灵活) :

加锁和释放的时机单一, 每个锁仅
有单一的条件(某个对象),可能是不够的

3.无法知道是否成功获取到锁

synchronized常见面试题

1.synchronized 使用注意点?

​ 锁对象不能为空,作用域不能过大,避免死锁

2.如何选择lock和synchronized关键字?

  • 最好都不使用
  • 使用juc并发类进行处理
  • 适用synchronized 优先选用
  • 减少代码编写

3.多线程访问同步线程的具体情况

总结

​ 一句话个绍synchronized
JVM会自动通过使用monitor来加锁和解锁,保证了同时只有一
​ 个线程可以执行指定代码,从而保证了线程安全,同时具有可重
​ 入和不可中断的性质。


Java异常体系

img

如何正确的停止发生异常的子线程?

  1. 方案一:(不推荐)手动在每个run 方法中进行try eatch
  2. 方案二:(推荐) 利用Uncaught ExceptionHandler

面试题

  • 如何正确停止线程?

    1.原理:用interrupt来请求、好处
    2.想停止线程,要请求方、被停止方、子方法被调用方相互配合
    3.最后再说错误的方法: stop/suspend已废弃,volatile的boolean
    无法处理长时间阻塞的情况

  • 如何处理不可中断的阻塞

    使用可以响应的中断

	thread.wait() 等待线程
    thread.notiy()唤醒当前线程
    thread.notiyAll()唤醒所有线程 
  • 生产者与消费者

package xyz.amewin.Consumer;
import java.util.Date;
import java.util.LinkedList;

/**
 * 描述:     用wait/notify来实现生产者消费者模式
 */
public class ProducerConsumerModelTeacher {
    public static void main(String[] args) {
        EventStorage eventStorage = new EventStorage();
        Producer producer = new Producer(eventStorage);
        Consumer consumer = new Consumer(eventStorage);
        new Thread(producer).start();
        new Thread(consumer).start();
    }
}

//生产者
class Producer implements Runnable {

    private EventStorage storage;
    public Producer(EventStorage storage) {
        this.storage = storage;
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            storage.put();
        }
    }
}

//消费者
class Consumer implements Runnable {

    private EventStorage storage;

    public Consumer(EventStorage storage) {
        this.storage = storage;
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            storage.take();
        }
    }
}

/**
 * 实现阻塞队列
 */
class EventStorage {

    private int maxSize;
    private LinkedList<Date> storage;

    public EventStorage() {
        maxSize = 10;
        storage = new LinkedList<>();
    }

    //放入方法<生产者>
    public synchronized void put() {
        /**
         * 当生产者的数量大于最大值时,停止生产
         */
        while (storage.size() == maxSize) {
            try {
                //停止生产
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        storage.add(new Date());
        System.out.println("仓库里有了" + storage.size() + "个产品。");
        notify();
    }

    //进行消费<消费者>
    public synchronized void take() {
        while (storage.size() == 0) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("拿到了" + storage.poll() + ",现在仓库还剩下" + storage.size());
        notify();
    }
}




线程状态


//查看线程状态
  Thread min = Thread.currentThread();
  min.getStackTrace();

面试题

用程序实现两个线程交替打印0~100的奇偶数?

  • 第一种:创建两个线程实现两个线程接口类

    • 单独进行判断
    • 可以使用 wait()、notify()进行优化
  • 第二种:创建一个接口类线程类(使用wait()、notify()、进行相互等待、唤醒,使程序变得有序)

public class PintNum100Two implements Runnable {

//    1.创建锁
    /**
     * 在一把锁内的线程,只能被某一条线程使用
     * 别的线程将会阻塞
     *
     */
    private static final Object lock = new Object();
    private static int count = 0;

    @Override
    public void run() {
        while (count <= 100) {
            synchronized (lock) {
                System.out.println(Thread.currentThread().getName() + ":" + count++);
                //换醒别线程
                lock.notify();
                //如果到达100 就不执行了 结束线程
                if(count<=100){
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
    public static void main(String[] args) {
        Thread thread1 = new Thread(new PintNum100Two());
        Thread thread2 = new Thread(new PintNum100Two());
        thread1.start();
        thread2.start();
    }
}


第二种

public class PintNum100 {

    private static int count;

    private static final Object lock = new Object();

    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (count <= 100) {
                    synchronized (lock) {
                        if ((count & 1) == 0) {
                            System.out.println(Thread.currentThread().getName() + ":" + count++);
                            try {
                                lock.notify();
                                lock.wait();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }

                    }
                }
            }
        }, "偶数").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                while (count <= 100) {
                    synchronized (lock) {

                        if ((count & 1) == 1) {
                            System.out.println(Thread.currentThread().getName() + ":" + count++);
                            try {
                                lock.notify();
                                lock.wait();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }

                    }
                }
            }
        }, "奇数").start();
        ;
    }
}


手写生产者消费者设计模式?


为什么wait()需要在同步代码块内使用,而sleep()不需要?

因为不在同步代码块中使用将出现巨大的线程错误,执行wait无法保证线程不被唤醒,在不加锁的情况下可能出现线程不安全。

sleep针对的方向是当前线程,所以不需要额外的代码保护

为什么线程通信的方法wait()与notify()和notifyAll()被定义在Object类里?而sleep定义在Thread类里?

锁是保存在对象中,而不是线程中。wait、notify 、notifyAll 针对的是锁级别的操作

wait方法是属于Object对象的,那调用Thread.wait会怎么样?

不要使用Thread 作为锁

如何选择用notify还是nofityAll ?

nofityAll 唤醒全部线程

notifyAll之后所有的线程都会再次抢夺锁,如果某线程抢夺失败怎么办?

会继续等待

用suspend()和resume()来阻塞线程可以吗?为什么?

该方法太过时,不适合用于线程阻塞

Thread和Object类中的重要方法详解

  • sleep 特点

    • 为不需要暂用cpu资源的线程降低cpu的占用

    • 不释放锁

      • synchronized 和 lock
      • 和wait 不同

      sleep优雅的写法

                    TimeUnit.DAYS.sleep(1);
                    TimeUnit.MINUTES.sleep(10);
                    TimeUnit.SECONDS.sleep(10);
    

    总结:

    ​ sleep方法可以让线程进入Waiting状态,并且不占用CPU资源,但是不释放锁,直到规定时间后再执行,休眠期间如果被中断,会抛出异常并清除中断状态。

面试题

wait/notify. sleep异同 (方法属于哪个对象?线程状态怎么切换? )

  • 相同点
    ◆阻塞
    ◆响应中断
  • 不同点
    ◆同步方法中
    ◆释放锁 wait<释放锁> sleep<不释放锁>
    ◆指定时间
    ◆所属类

join 方法

官方
​ 等待该线程是否死亡

作用

​ 因为新的线程加入了我们,所以我们要等他执行完再出发

用法

​ 主线程等待子线程完成,再执行工作

详解

​ 主线程在等待子线程完成工作的同时,主线程将会中断,是谁唤醒它的呢?

​ 是jvm底层实现唤醒他的

注意点:

​ ◆ CountDownL .atch或CyclicBarrier类

面试题

1.在join期间,线程处于哪种线程状态?

waiting

2.什么时候我们需要设置守护线程?

不需要,只有一个用户线程时(不管业务是否执行完,jvm都可能进行回收)

我们应该如何应用线程优先级来帮助程序运行?有哪些禁忌?

使用UncaughtExceptionHandler 进行全局异常处理

import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * 描述:  自己的MyUncaughtExceptionHanlder
 */
public class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {

    private String name;

    public MyUncaughtExceptionHandler(String name) {
        this.name = name;
    }

    @Override
    public void uncaughtException(Thread t, Throwable e) {
        Logger logger = Logger.getAnonymousLogger();
        logger.log(Level.WARNING, "线程异常,终止啦" + t.getName());
        System.out.println(name + "捕获了异常" + t.getName() + "异常");
    }
}


使用方法
public class ExceptionTest implements Runnable {

    public static void main(String[] args) {

        Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandlerTest("异常捕捉器"));
        try {
            new Thread(new ExceptionTest(), "t1").start();
            Thread.sleep(100);
            new Thread(new ExceptionTest(), "t2").start();
            Thread.sleep(100);
            new Thread(new ExceptionTest(), "t3").start();
            Thread.sleep(100);
            new Thread(new ExceptionTest(), "t4").start();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }


    @Override
    public void run() {
        throw new RuntimeException();
    }
}


面试题

1.如何全局处理异常?为什么要全局处理?不处理行不行?

​ Thread.UncaughtExceptionHandler 进行全局处理

​ 可以是引用Logger 进行打印日志
这样存在程序内部出现异常抛出前台的风险,导致被白帽或黑帽拿到信息

2.run方法是否可以抛出异常?如果抛出异常,线程的状态会怎么样?
不可以,终止运行( TERMINATED)
3.线程中如何处理某个末处理异常?

​ 使用全局处理

线程安全


什么情况下会出现线程安全问题,怎么避免?
  1. 运行结果错误: a+ +多线程”下出现消失的请求现象
  2. 活跃性问题:死锁、活锁、饥饿
  3. 对象发布初始化的时候的安全问题

什么是逸出?

private 了一个对象,不小心创建了getxxx方法,返回了这个对象,结果别的线程获得该对象,进行了修改,使得别方法操作了该对象获取不到值导致逸出。

​ 1.方法返回一个private对象 ( private的本意是不让外部访问)
​ 2.还未完成初始化 (构造函数没完全执行完毕)就把对象提供给外界,比如:
​ 3.在构造函数中未初始化完毕就this赋值

package xyz.amewin.background;

/**
 * 描述:     初始化未完毕,就this赋值
 */
public class MultiThreadsError4 {

    static Point point;

    public static void main(String[] args) throws InterruptedException {
        new PointMaker().start();
//        Thread.sleep(10);
        Thread.sleep(105);
        if (point != null) {
            System.out.println(point);
        }
    }
}

class Point {

    private final int x, y;

    public Point(int x, int y) throws InterruptedException {
        this.x = x;
        MultiThreadsError4.point = this;
        Thread.sleep(100);
        this.y = y;
    }

    @Override
    public String toString() {
        return x + "," + y;
    }
}

class PointMaker extends Thread {

    @Override
    public void run() {
        try {
            new Point(1, 1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
//1,1

​ ◆隐式逸出----- 注册监听事件

观察者模式逸出的原因:

观察者模式在观察某一个类对象时,已经进行了初始化导致,如果观察者创建类对象的时机,后于观察类会导致观察者观察失败。—

​ ◆在构造函数中运行线程<新建线程>

​ 数据连接池、工厂模式、会默认创建连接池

如何解决逸出?


​ 1.创建一个副本

​ 2.使用工厂模式

各种需要考虑线程安全的情况 (重点)

  • 访问共享的变量或资源,会有并发的风险
  • 依赖于业务顺序的操作,可能存在线程安全
  • 不同数据之间的捆绑关系

◆访问共享的变量或资源,会有并发风险,比如对象的属性、静态变量、
共享缓存、数据库等
所有依赖时序的操作,即使每一步操作都是线程安全的,还是存在并发
问题: read-modify-write、check-then-act
◆不同的数据之间存在捆绑关系的时候

◆我们使用其他类的时候,如果对方没有声明自己是线程安全的

多线程带来的问题

  • 调度:上下文切换

    1. 什么是上下文?–线程调度器进行调度 导致线程暂停

      1. 保存现场
      2. 缓存开销: <缓存失效>

      导致密集上下文切换的原因:

      竞争锁比较激烈 、 IO

    ​ b.缓存开销 --CPU重新缓存

  • 协同:内存同步

    • 为了数据正确性,同步手段往往会禁用编译器优化,是cpu内的缓存失效

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值