juc并发编程

1、什么是JUC

JUC指的是:Java里的三个包
java.util.concurrent
java.util.concurrent.atomic:原子性
java.util.concurrent.locks:lock锁

2、线程和进程

进程:程序执行的一次过程,一个进程包含一个或多个线程。进程是资源分配的单位。

线程:可以指程序执行过程中,负责实现某个功能的单位。线程是CPU调度和执行的单位。

并发和并行

并发:同一时刻,多个线程交替执行。(一个CPU交替执行线程)
并行:同一时刻,多个线程同时执行。

# 获取cpu的核数(cpu密集型;io密集型)
System.out.println(Runtime.getRuntime().availableProcessors());

线程的状态:

 public enum State {
         // 新生
        NEW,
         // 运行
        RUNNABLE,
         // 阻塞
        BLOCKED,
         // 等待,死死的等待
        WAITING, 
         // 超时等待
        TIMED_WAITING,     
         // 终止
        TERMINATED;
    }

wait/sleep

1.来自不同的类
wait——Object
sleep——Thread

2.关于锁的释放
wait会、sleep不会

3.使用范围
wait必须在同步代码块中

4.sleep不需要被唤醒(时间到了自动退出阻塞);wait需要被唤醒

3、锁

公平锁和非公平锁(锁的底层)
公平锁:十分公平,不能插队。
非公平锁:十分不公平,可以插队。(默认非公平锁

Lock锁

Lock锁是一个接口,他有三个实现类:
ReentrantLock类
ReentrantReadWriteLock.ReadLock
ReentrantReadWriteLock.WriteLock

Lock锁 & synchronized

1、Synchronized是内置Java关键字;Lock是一个Java类
2、Synchronized无法判断获取锁的状态;Lock可以判断是否获取到了锁。(boolean b = lock.tryLock();)
3、Synchronized会自动释放锁;Lock必须要手动释放锁,如果不释放锁,死锁
4、Synchronized线程1获得锁阻塞时,线程2会一直等待下去;Lock锁线程1获得锁阻塞时,线程2不一定会一直等待下去。
5、Synchronized可重入锁,不可以中断的,非公平;Lock,可重入锁,可以判断锁,非公平(可以自己设置)。
6、Synchronized适合锁少量的代码同步问题;Lock适合锁大量的同步代码

生产者 & 消费者

传统的synchronized

// 判断等待,业务,通知
class Data {
 private int i = 0;
 // +1
 public synchronized void increment() throws InterruptedException {
     if (i != 0) {
         this.wait();
     }
     i++;
     System.out.println(Thread.currentThread().getName() + "=>" + i);
     // 通知其他线程我+1完成
     this.notifyAll();
 }
 // -1
 public synchronized void decrement() throws InterruptedException {
     if (i==0){
         this.wait();
     }
     i--;
     System.out.println(Thread.currentThread().getName() + "=>" + i);
     // 通知其他线程,我-1完毕
     this.notifyAll();
 }
}

if改成while解决虚假唤醒

JUC版

class Data {
 private int i = 0;
 Lock lock = new ReentrantLock();
 Condition condition = lock.newCondition();
 // +1
 public  void increment() throws InterruptedException {
     lock.lock();
     try {
         while (i != 0) {
             condition.await();
         }
         i++;
         System.out.println(Thread.currentThread().getName() + "=>" + i);
         // 通知其他线程我+1完成
         condition.signalAll();
     } catch (InterruptedException e) {
         e.printStackTrace();
     } finally {
         lock.unlock();
     }
 }
 // -1
 public void decrement() throws InterruptedException {
     lock.lock();
     try {
         while (i==0){
             condition.await();
         }
         i--;
         System.out.println(Thread.currentThread().getName() + "=>" + i);
         // 通知其他线程,我-1完毕
         condition.signalAll();
     } catch (InterruptedException e) {
         e.printStackTrace();
     } finally {
         lock.unlock();
     }
 }
}

Condition实现精准通知唤醒

condition2.signal();

关于锁的八个问题

staic修饰符锁的是类
无static修饰符时锁的是方法的调用者

4、集合类不安全

java.util.ConcurrentModificationException 并发修改异常

ArryList

/**
         * 并发下arrayList不安全
         * 解决方法:
         * 1.List<String> list = new Vector<>();\\
         * 2.List<String> list= Collections.synchronizedList(new ArrayList<>());
         * 3.List<String> list= new CopyOnWriteArrayList<>();
         */
        //CopyOnWrite 写入时复制 一种优化策略

HashSet

//Hashset不安全
Set<String> set= Collections.synchronizedSet(new HashSet<>());
Set<String> set= new CopyOnWriteArraySet<>();

hashset集合的底层是hashmap的key

HashMap

Map<String, String> map=new ConcurrentHashMap<>();

5.Callable

Callable接口类似于Runnable接口,线程第三种创建方式。

可以抛出异常。
可以有返回值。
方法不同与Runnable接口。Call方法

futureTask

FutureTask类实现了RunnableFuture接口,继承了Runnable接口和Future接口。它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值。

//两个构造器
public FutureTask(Callable<V> callable) {
}
public FutureTask(Runnable runnable, V result) {
}
public class CallableTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        MyThread thread=new MyThread();
        FutureTask futureTask=new FutureTask<>(thread);
        new Thread(futureTask,"A").start();
        new Thread(futureTask,"B").start();// 打印一个Call,结果会被缓存,提高效率

        System.out.println(futureTask.get());  //获取callable返回接口,get方法可能会产生阻塞
    }
}

class MyThread implements Callable<String>{

    @Override
    public String call() throws Exception {
        return "1024";
    }
}

6.常用辅助类

CountDownLatch 减法计数器

在这里插入图片描述

public static void main(String[] args) throws InterruptedException {
        //总数6
        CountDownLatch countDownLatch = new CountDownLatch(6);
        for (int i = 0; i < 6; i++) {
            new  Thread(()->{
                System.out.println(Thread.currentThread().getName() + "GO out");
                countDownLatch.countDown();
            },String.valueOf(i)).start();
        }
        countDownLatch.await();
        System.out.println("close door");
    }

原理:countDownLatch.countDown(); //数量-1

countDownLatch.await(); //等待计数器归零,再向下执行

CyclicBarrier 加法计数器

在这里插入图片描述
例子:七龙珠唤神龙

public static void main(String[] args) {
        CyclicBarrier cyclicBarrier = new CyclicBarrier(3,()->{
            System.out.println("召唤神龙成功");// 如果计数器为7,线程只有6个,则会等待,不进行召唤神龙
        });

        for (int i = 1; i <= 7; i++) {
            final int temp=i;
            new Thread(()->{
                System.out.println(Thread.currentThread().getName()+"收集"+ temp +"个龙珠");
                try {
                    cyclicBarrier.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName());
            }).start();

        }
    }

Semaphore:信号量

 public static void main(String[] args) {
        Semaphore semaphore = new Semaphore(3);
        for (int i = 0; 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();
                }
            }).start();
        }
    }

原理:
semaphore.acquire(); //获取,
semaphore.release(); //释放,唤醒在等待的线程
作用:多个共享资源的互斥使用。并发限流,控制最大的线程数

7.读写锁

独占锁(写锁):一次只能被一个线程占有,不能读和写
共享锁(读锁):多个线程可以同时占有,可以同时读

public static void main(String[] args) {
        MyCacheLock myCache = new MyCacheLock();
        // 5个线程写
        for (int i = 1; i <= 5; i++) {
            final int temp = i;
            new Thread(() -> {
                myCache.put(temp + "", temp + "");
            }, String.valueOf(i)).start();
        }
        // 5个线程读
        for (int i = 1; i <= 5; i++) {
            final int temp = i;
            new Thread(() -> {
                myCache.get(temp + "");
            }, String.valueOf(i)).start();
        }
    }
}
    /**
     * 自定义缓存
     */
    class MyCacheLock {
        private volatile Map<String, Object> map = new HashMap<>();
        // 读写锁,更加细粒度的控制
        private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();

        // 写,同时只有一个线程写
        public void put(String key, Object obj) {
            readWriteLock.writeLock().lock();
            try {
                System.out.println(Thread.currentThread().getName() + "写入");
                map.put(key, obj);
                System.out.println(Thread.currentThread().getName() + "写入OK");
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                readWriteLock.writeLock().unlock();
            }
        }

        // 读,所有线程都可以读
        public void get(String key) {
            readWriteLock.readLock().lock();
            try {
                System.out.println(Thread.currentThread().getName() + "读取");
                map.get(key);
                System.out.println(Thread.currentThread().getName() + "读取OK");
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                readWriteLock.readLock().unlock();
            }
        }
    }

8.阻塞队列BlockingQueue

使用场景:多线程并发处理,线程池
在这里插入图片描述
在这里插入图片描述

// 阻塞,超时等待
    public static void test4() throws InterruptedException {
        // 队列的大小为3
        ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3);
        // offer返回boolean值
        boolean flag1 = blockingQueue.offer("a");
        boolean flag2 = blockingQueue.offer("b");
        boolean flag3 = blockingQueue.offer("c");
        // offer添加元素超过队列的长度会返回false;并且等待指定时间后推出,向下执行
        boolean flag4 = blockingQueue.offer("d", 2, TimeUnit.SECONDS);
        System.out.println("=========");
        // poll()返回本次移除的元素
        Object poll1 = blockingQueue.poll();
        Object poll2 = blockingQueue.poll();
        Object poll3 = blockingQueue.poll();
        // 队列中没有元素仍继续移除元素会打印出null,等待指定之间后退出。
        Object poll4 = blockingQueue.poll(2,TimeUnit.SECONDS);
    }

SynchronousQueue同步队列
进去一个元素,必须等待取出这个元素后,才能放下一个元素。put()、take()

 public static void main(String[] args) {
        BlockingQueue<String> blockingQueue = new SynchronousQueue<>();

        new Thread(()->{

            try {
                System.out.println(Thread.currentThread().getName()+"put 1");
                blockingQueue.put("1");
                System.out.println(Thread.currentThread().getName()+"put 2");
                blockingQueue.put("2");
                System.out.println(Thread.currentThread().getName()+"put 3");
                blockingQueue.put("3");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        },"T1").start();

        new Thread(()->{
            try {
                TimeUnit.SECONDS.sleep(3);
                System.out.println(Thread.currentThread().getName()+blockingQueue.take());
                TimeUnit.SECONDS.sleep(3);
                System.out.println(Thread.currentThread().getName()+blockingQueue.take());
                TimeUnit.SECONDS.sleep(3);
                System.out.println(Thread.currentThread().getName()+blockingQueue.take());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        },"T2").start();
    }

9.线程池

池化技术:事先准备好一些资源,有人使用来我这里拿,用完归还。

好处:
1、降低资源的消耗
2、提高响应的速度
3、方便管理

3大方法

//        ExecutorService threadPool = Executors.newSingleThreadExecutor();//单个线程
//        ExecutorService threadPool =Executors.newFixedThreadPool(5);    //固定线程池大小
        ExecutorService threadPool =Executors.newCachedThreadPool();

7大参数

public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }

三大方法的本质ThreadPoolExecutor()

public ThreadPoolExecutor(int corePoolSize,//核心线程池大小
                          int maximumPoolSize,//最大核心线程池大小
                          long keepAliveTime,//超时释放
                          TimeUnit unit,//超时单位
                          BlockingQueue<Runnable> workQueue,//阻塞队列
                          ThreadFactory threadFactory,//创建线程的工厂
                          RejectedExecutionHandler handler//拒绝策略) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.acc = System.getSecurityManager() == null ?
                null :
                AccessController.getContext();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

自定义创建线程池

ExecutorService threadPool=new ThreadPoolExecutor(2,5,
                3, TimeUnit.SECONDS,
                            new LinkedBlockingQueue<>(3),
                            Executors.defaultThreadFactory(),
                            new ThreadPoolExecutor.AbortPolicy());//银行满了,有人进,抛出异常

4大拒绝策略

在这里插入图片描述

new ThreadPoolExecutor.AbortPolicy(); // 抛出异常
new ThreadPoolExecutor.CallerRunsPolicy();// 哪来的去哪(主线程来的,就回去让主线程执行)
new ThreadPoolExecutor.DiscardPolicy();// 丢掉任务,不抛出异常
new ThreadPoolExecutor.DiscardOldestPolicy();// 尝试和最早的竞争,竞争失败了也丢掉任务,也不抛出异常

最大线程定义

CPU密集型

最大线程数,CPU几核的就是几,可以保持CPU效率最高。

IO密集型

判断程序中十分耗IO的线程数量,大于这个数,一般是这个数的两倍。

10.四大函数式接口

新时代程序员必须掌握:lambda表达式、链式编程、函数式接口、stream流式计算
函数式接口:只有一个方法的接口。

Function函数型接口

有一个输入参数,有一个输出(返回值)。

Function<String,String> function = (str)->{return str;};

predicate断定型接口

有一个输入参数,返回值只能是boolean值。

Predicate<String> predicate = (str)->{return str.isEmpty(); };

consumer消费型接口

有一个输入参数,没有返回值。

Consumer<String> consumer = (str)->{System.out.println(str);};

supplier供给型接口

没有输入参数,有一个输出(返回值)。

Supplier supplier = ()->{ return 1024; };

11.流计算

大数据:存储 + 计算
集合、MySQL 本质就是存储东西的;
计算都应该交给流来操作

/*** 题目要求:一分钟内完成此题,只能用一行代码实现!
* 现在有5个用户!筛选: 
* * 1、ID 必须是偶数 
* * 2、年龄必须大于23岁 
* * 3、用户名转为大写字母 
* * 4、用户名字母倒着排序 
* * 5、只输出一个用户! 
* */
 public class Test { 
 public static void main(String[] args) {
  User u1 = new User(1,"a",21); 
  User u2 = new User(2,"b",22); 
  User u3 = new User(3,"c",23); 
  User u4 = new User(4,"d",24); 
  User u5 = new User(6,"e",25); 
  // 集合就是存储 
  List<User> list = Arrays.asList(u1, u2, u3, u4, u5); 
  // 计算交给Stream流 
  // lambda表达式、链式编程、函数式接口、Stream流式计算 
  list.stream() .filter(u->{return u.getId()%2==0;}) 
  .filter(u->{return u.getAge()>23;}) 
  .map(u->{return u.getName().toUpperCase();}) 
  .sorted((uu1,uu2)->{return uu2.compareTo(uu1);}) 
  .limit(1) 
  .forEach(System.out::println); 
  }
}

12.ForkJoin

ForkJoin 在 JDK 1.7 , 并行执行任务!提高效率。大数据量!
ForkJoin 特点:工作窃取、里面维护的都是双端队列
在这里插入图片描述
在这里插入图片描述

/**
* * 求和计算的任务! * 3000 6000(ForkJoin) 9000(Stream并行流) 
* *  如何使用 forkjoin 
* *  1、forkjoinPool 通过它来执行
* *  2、计算任务 forkjoinPool.execute(ForkJoinTask task) 
* *  3. 计算类要继承 ForkJoinTask
* */
public class ForkJoinDemo extends RecursiveTask<Long> { 

private Long start; 
private Long temp = 10000L; 

public ForkJoinDemo(Long start, Long end) { 
this.start = start; 
this.end = end; }

// 计算方法 
@Override 
protected Long compute() { 
if ((end-start)<temp){ 
	Long sum = 0L; 
	for (Long i = start; i <= end; i++) { sum += i; }
	return sum; }
	else { // forkjoin 递归 
	long middle = (start + end) / 2; // 中间值 
	ForkJoinDemo task1 = new ForkJoinDemo(start, middle); 
	task1.fork(); // 拆分任务,把任务压入线程队列 
	ForkJoinDemo task2 = new ForkJoinDemo(middle+1, end); 
	task2.fork(); // 拆分任务,把任务压入线程队列
	return task1.join() + task2.join(); 
	} 
} 
}

stream并行流

public static void test3(){ 
	long start = System.currentTimeMillis(); 
	// Stream并行流 () (] 
	long sum = LongStream.rangeClosed(0L, 10_0000_0000L).parallel().reduce(0, Long::sum); 
	long end = System.currentTimeMillis();
	System.out.println("sum="+"时间:"+(end-start)); 
}

13、异步回调

/*** 异步调用: CompletableFuture 
* * 异步执行 
* * 成功回调 
* * 失败回调 
* */
CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(()->{ 
	System.out.println(Thread.currentThread().getName()+"supplyAsync=>Integer"); 
	int i = 10/0; 
	return 1024; 
	}); 
	System.out.println(completableFuture.whenComplete((t, u) -> { 	
			System.out.println("t=>" + t); // 正常的返回结果 
			System.out.println("u=>" + u); // 错误信息: java.util.concurrent.CompletionException: java.lang.ArithmeticException: / by zero 
	}).exceptionally((e) -> { 
		System.out.println(e.getMessage()); 
		return 233; // 可以获取到错误的返回结果 
	}).get());

14、JMM

Volatile 是 Java 虚拟机提供轻量级的同步机制

1、保证可见性
2、不保证原子性
3、禁止指令重排

JMM : Java内存模型
在这里插入图片描述
lock&unlock、read&load、use&assign、store&write

15、volatile

① 保证可见性

public class JMMDemo { 
// 不加 volatile 程序就会死循环! 
// 加 volatile 可以保证可见性 
private volatile static int num = 0;

public static void main(String[] args){ 

 new Thread(()->{ // 线程 1 对主内存的变化不知道的 
 while (num==0){ 
 		} 
 }).start(); 

num = 1; 
System.out.println(num); } }

不保证原子性

// 原子类的 Integer
private volatile static AtomicInteger num = new AtomicInteger();

public static void add(){
 	// num++;  不是一个原子性操作
 	num.getAndIncrement(); // AtomicInteger + 1 方法, CAS }

禁止指令重排

处理器在进行指令重排的时候,考虑:数据之间的依赖性!
Volatile 由于内存屏障,可以保证避免指令重排的现象产生!

16、彻底玩转单例

①饿汉式

public class Hungry {
    // 可能会浪费空间
    private byte[] data1 = new byte[1024*1024];
    private byte[] data2 = new byte[1024*1024];
    private byte[] data3 = new byte[1024*1024];
    private byte[] data4 = new byte[1024*1024];
    
    private Hungry(){
        
    }
    private  final static Hungry hungry=new Hungry();
    public static Hungry getInstance(){
        return hungry;
    }
}

②懒汉式

public class LazyMan {
    private static boolean qinjiang = false;

    private LazyMan() {
    }

    private volatile static LazyMan lazyMan;

    public static LazyMan getInstance() {
        if (lazyMan == null) {
            lazyMan = new LazyMan();  //不是一个原子性操作
        }
        return lazyMan;
    }
}

多线程下不安全,修改

//双重检测
if (lazyMan == null) {
    synchronized (LazyMan.class){
        if (lazyMan == null) {
            lazyMan = new LazyMan();  //不是一个原子性操作
        }
    }
}

避免指令重排
private volatile static LazyMan lazyMan;

③静态内部类

但由于反射,①②③都不安全

Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);    //无视私有构造器
        LazyMan lazyMan = declaredConstructor.newInstance();

解决方法

private LazyMan() {
        synchronized (LazyMan.class){
                if (lazyMan != null) {
                    throw new RuntimeException("不要试图使用反射破坏单例");
                } 
            }
    }

但仍然不安全,反射不能破坏枚举的单例:
在这里插入图片描述

public enum EnumSingle {
    INSTANCE;

    public EnumSingle getInstance() {
        return INSTANCE;
    }
}

17、深入理解CAS

比较并交换:compare and swap !
比较当前内存中的值和主内存中的值,如果主内存中的值是期望的,就执行操作,否则不执行操作。

 AtomicInteger atomicInteger = new AtomicInteger(2020);
// 期望,更新:public final boolean compareAndSet(int expect, int update)
// 如果我期望的值达到了(2020)就更新,否则不更新。
System.out.println(atomicInteger.compareAndSet(2020, 2021));// true
System.out.println(atomicInteger.get());// 2021
System.out.println(atomicInteger.compareAndSet(2020, 2021));// false
System.out.println(atomicInteger.get());// 2021

在这里插入图片描述

Unsafe类
正常情况下Java只能通过调用c++操作内存
Java的后门,可以通过这个类操作内存

缺点:
1、 循环会耗时
2、一次性只能保证一个共享变量的原子性
3、ABA问题

18、原子引用解决ABA

ABA 问题(狸猫换太子)

注意:
Integer 使用了对象缓存机制,默认范围是 -128 ~ 127 ,推荐使用静态工厂方法 valueOf 获取对象实
例,而不是 new,因为 valueOf 使用缓存,而 new 一定会创建新的对象分配新的内存空间;

//注意,如果泛型是一个包装类,注意对象的引用问题
static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(1,1);
atomicStampedReference.compareAndSet(1,2,
					atomicStampedReference.getStamp(),
                    atomicStampedReference.getStamp()+1);
                    
int stamp = atomicStampedReference.getStamp(); // 获得版本号

19、各种锁

公平&非公平

公平锁: 非常公平, 不能够插队,必须先来后到!
非公平锁:非常不公平,可以插队 (默认都是非公平)

public ReentrantLock() { 
	sync = new NonfairSync(); 
}

public ReentrantLock(boolean fair) {
	sync = fair ? new FairSync() : new NonfairSync(); 
}

可重入锁

某个线程已经获得某个锁,可以再次获取锁而不会出现死锁。(加锁的方法可以相互调用)

public class Demo01 {
    public static void main(String[] args) {
        Phone phone = new Phone();
        new Thread(()->{phone.sms();},"A").start();
        new Thread(()->{phone.sms();},"B").start();
    }
}

class Phone {
    public synchronized void sms() {
        System.out.println(Thread.currentThread().getName() + "发短信");
        call();// 这个方法也有锁
    }
    private synchronized void call() {
        System.out.println(Thread.currentThread().getName() + "打电话");
    }
}

在这里插入图片描述

自旋锁

不断尝试,直到成功
在这里插入图片描述

死锁

线程A持有锁A,想要获得锁B;线程B持有锁B,想要获得锁A。

排查问题:
1、日志

2、堆栈:查看堆栈信息
使用jps -l命令查看进程号。(该命令在JDK的bin目录下)
使用jstack+进程号,找到死锁问题。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值