Java锁机制,synchronized和lock详解。

Java锁机制详解

1.java各种锁详解

1.1 公平锁 vs 非公平锁

公平锁:是指多个线程按照申请锁的顺序来获取锁,线程直接进入队列中排队,队列中的第一个线程才能获得锁。类似排队打饭,先来后到。

非公平锁:是指多个线程获取锁的顺序并不是按照申请锁的顺序,假设有一个线程此时加锁后正准备释放,这时候刚好又有一个线程进来获取锁,则有可能改线程就能获取该锁。(跟插队一样),如果没有刚好线程释放锁的话,则需要乖乖往后面排队,先来后到。就变成公平锁了。(synchronized和lock默认是非公平锁)

lock非公平锁设置:Lock lock=new ReentrantLock(false)。 默认是false,设置为true则为公平锁。

1.2乐观锁 VS 悲观锁

  • 悲观锁是一种悲观思想,它总认为自己在使用数据的时候一定有别的线程来修改,所以悲观锁在持有数据的时候总会把资源或数据锁住,这样其他线程想要请求这个资源的时候就会阻塞,直到等到悲观锁把资源释放为止。传统的关系型数据库里边就用到了很多这种锁机制,**比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。**悲观锁的实现往往依靠数据库本身的锁功能实现。

  • Java 中,synchronized 关键字和 Lock 的实现类都是悲观锁。

  • 而乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作(例如报错或者自动重试)

  • 例如Java中的validate

比较

悲观锁:比较适合写入操作比较频繁的场景,如果出现大量的读取操作,每次读取的时候都会进行加锁,这样会增加大量的锁的开销,降低了系统的吞吐量。

乐观锁:比较适合读取操作比较频繁的场景,如果出现大量的写入操作,数据发生冲突的可能性就会增大,为了保证数据的一致性,应用层需要不断的重新获取数据,这样会增加大量的查询操作,降低了系统的吞吐量。

1.3可重入锁

就是加锁的次数和释放锁的次数要一样,可重入锁的意义在于防止死锁。

synchronized 和 ReentrantLock 都是可重入锁。

1.4读写锁

读写锁是一种技术: 通过ReentrantReadWriteLock类来实现。为了提高性能, Java 提供了读写锁,在读的地方使用读锁,在写的地方使用写锁,灵活控制,如果没有写锁的情况下,读是无阻塞的,在一定程度上提高了程序的执行效率。

读写锁分为读锁和写锁,多个读锁不互斥,读锁与写锁互斥,这是由 JVM 自己控制的。(只要有写锁就需要加锁)

1.5轻量级锁

轻量级锁在没有多线程竞争的前提下,把整个同步都消除掉,连CAS(Compare And Swap)操作都不去做了,优于轻量级锁。如果出现两条以上的线程争用同一个锁的情况,那轻量级锁将不会有效,必须膨胀为重量级锁

1.6重量级锁

同步、线程安全的方法时,是需要先获得这个方法的锁的,退出这个方法时,则会释放锁。如果获取不到这个锁的话,意味着有别的线程在执行这个方法,这时我们就会马上进入阻塞的状态,等待那个持有锁的线程释放锁,然后再把我们从阻塞的状态唤醒,我们再去获取这个方法的锁。这种获取不到锁就马上进入阻塞状态的锁,我们称之为重量级锁

1.7自旋锁

为了优化重量级锁,特意引入了自旋锁。自旋锁就是,如果此时拿不到锁,它不马上进入阻塞状态,而是等待一段时间,看看这段时间有没其他人把这锁给释放了。怎么等呢?这个就类似于线程在那里做空循环,如果循环一定的次数还拿不到锁,那么它才会进入阻塞的状态。

1.8偏向锁(当前场景不太适应)

这个线程退出这个方法的时候,它不会改变这个方法的状态,而是直接退出来,懒的去改,因为它认为除了自己这个线程之外,其他线程并不会来执行这个方法。

然后当这个线程想要再次进入这个方法的时候,会判断一下这个方法的状态,如果这个方法已经被标记为有人在执行了,并且线程的ID是自己,那么它就直接进入这个方法执行,啥也不用做。

2.synchronized

synchronized 实现原理

synchronized 是悲观锁,在字节码层被映射成两个指令:monitorenter 和 monitorexit,当一个线程遇到 monitorenter 指令时,会尝试去获取锁,如果获取成功,锁的数量 +1,(因为synchronized是一个可重入锁,需要使用锁计数来判断锁的情况),如果没有获取到锁,就会阻塞;当线程遇到 monitorexit 指令时,锁计数 -1,当计数器为 0 时,线程释放锁;如果线程遇到异常,也会释放锁。

例如查看下面👇代码的字节码:

public class APP {
    void test() {
        synchronized (this) {
            System.out.println("hello world");
        }
    }
}

首先 cd 到文件目录,然后执行 javac APP.java,可得到字节码文件:APP.class,再执行 javap -verbose APP.class,可看到字节码内容。

img

2.1synchronized修饰普通方法,锁对象默认为this

package com.hjt.synchronizedDemo;



public class SynchronizedObjectLock implements Runnable {
    static SynchronizedObjectLock instence = new SynchronizedObjectLock();

    @Override
    public void run() {
        // 同步代码块形式——锁为this,两个线程使用的锁是一样的,线程1必须要等到线程0释放了该锁后,才能执行
        synchronized (this) {
            System.out.println("我是线程" + Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "结束");
        }
    }

    public static void main(String[] args) {
        Thread t1 = new Thread(instence);
        Thread t2 = new Thread(instence);
        t1.start();
        t2.start();
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xEWEnurp-1660630716001)(Java%E9%94%81%E6%9C%BA%E5%88%B6%E8%AF%A6%E8%A7%A3.assets/image-20220816104119238.png)]

2.2 两把不同的锁

package com.hjt.synchronizedDemo;



public class SynchronizedObjectLock1 implements Runnable {
    static SynchronizedObjectLock1 instence = new SynchronizedObjectLock1();
    // 创建2把锁
    Object block1 = new Object();
    Object block2 = new Object();

    @Override
    public void run() {
        // 这个代码块使用的是第一把锁,当他释放后,后面的代码块由于使用的是第二把锁,因此可以马上执行
        synchronized (block1) {
            System.out.println("block1锁,我是线程" + Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("block1锁,"+Thread.currentThread().getName() + "结束");
        }

        synchronized (block2) {
            System.out.println("block2锁,我是线程" + Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("block2锁,"+Thread.currentThread().getName() + "结束");
        }
    }

    public static void main(String[] args) {
        Thread t1 = new Thread(instence);
        Thread t2 = new Thread(instence);
        t1.start();
        t2.start();
    }
}

输出结果:

在这里插入图片描述

2.3synchronize修饰静态方法

package com.hjt.synchronizedDemo;

public class SynchronizedObjectLock2 implements Runnable {
    static SynchronizedObjectLock2 instence1 = new SynchronizedObjectLock2();
    static SynchronizedObjectLock2 instence2 = new SynchronizedObjectLock2();

    @Override
    public void run() {
        method();
    }

    // synchronized用在普通方法上,默认的锁就是this,当前实例
    public static  synchronized void method() {
        System.out.println("我是线程" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "结束");
    }

    public static void main(String[] args) {
        // t1和t2对应的this是两个不同的实例,所以代码不会串行
        Thread t1 = new Thread(instence1);
        Thread t2 = new Thread(instence2);
        t1.start();
        t2.start();
    }
}

输出结果:

在这里插入图片描述

2.4synchronized指定锁对象为Class对象

package com.hjt.synchronizedDemo;


public class SynchronizedObjectLock3 implements Runnable {
    static SynchronizedObjectLock3 instence1 = new SynchronizedObjectLock3();
    static SynchronizedObjectLock3 instence2 = new SynchronizedObjectLock3();

    @Override
    public void run() {
        // 所有线程需要的锁都是同一把
        synchronized(SynchronizedObjectLock3.class){
            System.out.println("我是线程" + Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "结束");
        }
    }

    public static void main(String[] args) {
        Thread t1 = new Thread(instence1);
        Thread t2 = new Thread(instence2);
        t1.start();
        t2.start();
    }
}

运行结果:

在这里插入图片描述

3.lock

  • lock(): 加锁
  • unlock(): 解锁
  • tryLock(): 尝试获取锁,返回一个boolean值
  • tryLock(long,TimeUtil): 尝试获取锁,可以设置超时

Lock一般使用的例子,注意ReentrantLock是Lock接口的实现。

package com.hjt.lock;


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

public class LockTest {
    private Lock lock = new ReentrantLock();

    //需要参与同步的方法
    private void method(Thread thread){
        try {
            lock.lock();
            System.out.println("线程名"+thread.getName() + "获得了锁");
        }catch(Exception e){
            e.printStackTrace();
        } finally {
            lock.unlock();
            System.out.println("线程名"+thread.getName() + "释放了锁");

        }
    }

    public static void main(String[] args) {
        LockTest lockTest = new LockTest();

        //线程1
        Thread t1 = new Thread(new Runnable() {

            @Override
            public void run() {
                lockTest.method(Thread.currentThread());
            }
        }, "t1");

        Thread t2 = new Thread(new Runnable() {

            @Override
            public void run() {
                lockTest.method(Thread.currentThread());
            }
        }, "t2");

        t1.start();
        t2.start();
    }
}
//执行情况:线程名t1获得了锁
//         线程名t1释放了锁
//         线程名t2获得了锁
//         线程名t2释放了锁

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PRfDalo3-1660630716003)(Java%E9%94%81%E6%9C%BA%E5%88%B6%E8%AF%A6%E8%A7%A3.assets/image-20220816102409686.png)]

4.Lock和synchronized的区别

Lock: 是Java中的接口,可重入锁、悲观锁、独占锁、互斥锁、同步锁。

  1. Lock需要手动获取锁和释放锁。

  2. Lock 是一个接口,而 synchronized 是 Java 中的关键字, synchronized 是内置的语言实现。
    synchronized 在发生异常时,会自动释放线程占有的锁,因此不会导致 死锁现象发生,而 Lock 在发生异常时,如果没有主动通过 unLock() 去释放 锁,则很可能造成死锁现象,因此使用 Lock 时需要在 finally 块中释放锁。

  3. Lock 可以让等待锁的线程响应中断,而 synchronized 却不行,使用 synchronized 时,等待的线程会一直等待下去,不能够响应中断。
    通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。

  4. Lock 可以通过实现读写锁提高多个线程进行读操作的效率。

5.ReentrantLock锁和ReentrantReadwriteLock 读写锁锁

5.1ReentrantLock详解

  • ReentrantLock重入锁,是实现Lock接口的一个类,也是在实际编程中使用频率很高的一个锁,支持重入性,表示能够对共享资源能够重复加锁,即当前线程获取该锁再次获取不会被阻塞。并且也是公平锁。

    公平锁实例:

    package com.hjt.reentrantLock;
    
    /***
     * @author hjt
     * AbstractQueuedSynchonizer 抽象队列同步器 简称AQS
     */
    
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    
    class MyThread extends Thread {
        private Lock lock;
        public MyThread(String name, Lock lock) {
            super(name);
            this.lock = lock;
        }
    
        public void run () {
            lock.lock();
            try {
                System.out.println(Thread.currentThread() + " running");
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            } finally {
                lock.unlock();
            }
        }
    }
    
    public class AbstractQueuedSynchonizerDemo {
        public static void main(String[] args) throws InterruptedException {
            Lock lock = new ReentrantLock(true);
    
            MyThread t1 = new MyThread("t1", lock);
            MyThread t2 = new MyThread("t2", lock);
            MyThread t3 = new MyThread("t3", lock);
            t1.start();
            t2.start();
            t3.start();
        }
    }
    

    运行结果:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nEQjHu72-1660630716004)(Java%E9%94%81%E6%9C%BA%E5%88%B6%E8%AF%A6%E8%A7%A3.assets/image-20220816114810886.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AXuNolm1-1660630716005)(Java%E9%94%81%E6%9C%BA%E5%88%B6%E8%AF%A6%E8%A7%A3.assets/image-20220816114903467.png)]

5.2ReentrantReadwriteLock 读写锁(常用)

JUC的就 是java并发编程工具包

** 读写锁允许同一时刻被多个读线程访问,但是在写线程访问时,所有的读线程和其他的写线程都会被阻塞**

package com.hjt.reentrantReadwriteLock;


import java.util.concurrent.locks.ReentrantReadWriteLock;

class ReadThread extends Thread {
    private ReentrantReadWriteLock rrwLock;

    public ReadThread(String name, ReentrantReadWriteLock rrwLock) {
        super(name);
        this.rrwLock = rrwLock;
    }

    public void run() {
        System.out.println(Thread.currentThread().getName() + " trying to lock");
        try {
            rrwLock.readLock().lock();
            System.out.println(Thread.currentThread().getName() + " lock successfully");
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            rrwLock.readLock().unlock();
            System.out.println(Thread.currentThread().getName() + " unlock successfully");
        }
    }
}

class WriteThread extends Thread {
    private ReentrantReadWriteLock rrwLock;

    public WriteThread(String name, ReentrantReadWriteLock rrwLock) {
        super(name);
        this.rrwLock = rrwLock;
    }

    public void run() {
        System.out.println(Thread.currentThread().getName() + " trying to lock");
        try {
            rrwLock.writeLock().lock();
            System.out.println(Thread.currentThread().getName() + " lock successfully");
        } finally {
            rrwLock.writeLock().unlock();
            System.out.println(Thread.currentThread().getName() + " unlock successfully");
        }
    }
}

public class ReentrantReadWriteLockDemo {
    public static void main(String[] args) {
        ReentrantReadWriteLock rrwLock = new ReentrantReadWriteLock();
        ReadThread rt1 = new ReadThread("rt1", rrwLock);
        ReadThread rt2 = new ReadThread("rt2", rrwLock);
        WriteThread wt1 = new WriteThread("wt1", rrwLock);
        rt1.start();
        rt2.start();
        wt1.start();
    }
}

运行结果,先后顺序可能不一样。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Sdz0ZIbs-1660630716006)(Java%E9%94%81%E6%9C%BA%E5%88%B6%E8%AF%A6%E8%A7%A3.assets/image-20220816135541864.png)]

6.volatile 关键字

被volatile修饰的变量能够保证每个线程能够获取该变量的最新值,从而避免出现数据脏读的现象。

写一段简单的 Java 代码,声明一个 volatile 变量,并赋值。

public class Test {
    private volatile int a;
    public void update() {
        a = 1;
    }
    public static void main(String[] args) {
        Test test = new Test();
        test.update();
    }
}
  
  
    

通过 hsdis 和 jitwatch 工具可以得到编译后的汇编代码:

......
  0x0000000002951563: and    $0xffffffffffffff87,%rdi
  0x0000000002951567: je     0x00000000029515f8
  0x000000000295156d: test   $0x7,%rdi
  0x0000000002951574: jne    0x00000000029515bd
  0x0000000002951576: test   $0x300,%rdi
  0x000000000295157d: jne    0x000000000295159c
  0x000000000295157f: and    $0x37f,%rax
  0x0000000002951586: mov    %rax,%rdi
  0x0000000002951589: or     %r15,%rdi
  0x000000000295158c: lock cmpxchg %rdi,(%rdx)  //在 volatile 修饰的共享变量进行写操作的时候会多出 lock 前缀的指令
  0x0000000002951591: jne    0x0000000002951a15
  0x0000000002951597: jmpq   0x00000000029515f8
  0x000000000295159c: mov    0x8(%rdx),%edi
  0x000000000295159f: shl    $0x3,%rdi
  0x00000000029515a3: mov    0xa8(%rdi),%rdi
  0x00000000029515aa: or     %r15,%rdi
......

lock 前缀的指令在多核处理器下会引发两件事情:

  • 将当前处理器缓存行的数据写回到系统内存。
  • 写回内存的操作会使在其他 CPU 里缓存了该内存地址的数据无效。

所有多核处理器下还会完成:当处理器发现本地缓存失效后,就会从内存中重读该变量数据,即可以获取当前最新值。

volatile 变量通过这样的机制就使得每个线程都能获得该变量的最新值。

volatile 的应用场景

使用 volatile 必须具备的条件

  • 对变量的写操作不依赖于当前值。
  • 该变量没有包含在具有其他变量的不变式中。
  • 只有在状态真正独立于程序内其他内容时才能使用 volatile。
附加问题1: i++为什么不能保证原子性?

对于原子性,需要强调一点,也是大家容易误解的一点:对volatile变量的单次读/写操作可以保证原子性的,如long和double类型变量,但是并不能保证i++这种操作的原子性,因为本质上i++是读、写两次操作。

7. 关键字: final详解

7.1修饰方法

常规的使用就不说了,这里说下:

  • private 方法是隐式的final
  • final方法是可以被重载的
private final

类中所有private方法都隐式地指定为final的,由于无法取用private方法,所以也就不能覆盖它。可以对private方法增添final关键字,但这样做并没有什么好处。看下下面的例子:

public class Base {
    private void test() {
    }
}

public class Son extends Base{
    public void test() {
    }
    public static void main(String[] args) {
        Son son = new Son();
        Base father = son;
        //father.test();
    }
}
  

    

Base和Son都有方法test(),但是这并不是一种覆盖,因为private所修饰的方法是隐式的final,也就是无法被继承,所以更不用说是覆盖了,在Son中的test()方法不过是属于Son的新成员罢了,Son进行向上转型得到father,但是father.test()是不可执行的,因为Base中的test方法是private的,无法被访问到。

7.2final方法是可以被重载的

我们知道父类的final方法是不能够被子类重写的,那么final方法可以被重载吗? 答案是可以的,下面代码是正确的。

public class FinalExampleParent {
    public final void test() {
    }

    public final void test(String str) {
    }
}

代码地址:https://github.com/hongjiatao/spring-boot-anyDemo
欢迎互相交流

  • 10
    点赞
  • 51
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
### 回答1: Java中的锁机制有两种:synchronizedLocksynchronizedJava中最基本的锁机制,它是Java中的内置,可以用于同步方法和同步代码块。synchronized锁机制是基于对象的,每个对象都有一个,当一个线程访问一个对象时,它会尝试获取这个对象的,如果已经被其他线程获取了,那么这个线程就会被阻塞,直到获取到为止。 LockJava中的另一种锁机制,它是Java中的显式,需要手动获取和释放Lock锁机制是基于接口的,它提供了更多的灵活性和功能,比如可以设置的公平性、可重入性、超时等待等。 总的来说,synchronizedJava中最常用的锁机制,它简单易用,但是功能相对较少;而Lock锁机制则更加灵活,但是使用起来相对复杂一些。在实际开发中,应该根据具体的需求选择合适的锁机制。 ### 回答2: Java中的锁机制是多线程编程中非常重要的一个概念,synchronizedLock是两种常用的Java锁机制。它们在实现多线程编程时,提供了同步和互斥的功能。然而,在使用时,它们存在一些不同之处。 Synchronized关键字是Java语言内置的锁机制,它是在Java中最简单和最普遍的一种锁机制。当在代码中添加synchronized关键字时,它会住当前对象,即monitor(即synchronized后面的定对象)。在同步代码块执行前,线程会获得,执行完同步代码块后,就会释放。只有获得了的线程才能执行同步的代码块,其他线程需要等待获取。 相比之下,Lock则是Java中的一种更加灵活的锁机制。它提供了更加丰富的同步控制方法和更加灵活的逻辑控制选项。通过使用Lock,线程可以实现更高级别的同步处理,使用ReentrantLock类实现更加细粒度的定和解操作。 LockSynchronized关键字的主要区别在于,Lock是显式地定义对象,需要手动获取和释放,而Synchronized是隐式的,自动获取和释放。此外,在Synchronized机制下,遇到异常会自动释放,但在Lock机制下,如果遇到异常需要手动释放。 在高并发场景下,Lock的性能更高,更有优势,但是它的使用比Synchronized更加复杂,更容易出现死等问题。因此,在实际开发中,应该根据具体的情况来选择使用哪种锁机制。 总的来说,JavaSynchronizedLock锁机制Java中实现多线程同步的两种方式,它们的原理和功能都是一致的。但是,它们在使用时存在着不同的特点和优缺点,需要根据实际情况进行选择和应用。 ### 回答3: 在Java编程中,是一种有效的控制并发的机制,他们可以确保同一时间内只有一个线程可以访问或修改共享资源。Java中提供了两种主要的锁机制,分别是synchronizedLockSynchronized锁机制SynchronizedJava中最常用也是最基本的锁机制,它是一个关键字,用于标记一个方法或代码块,以保证同一时间只能有一个线程执行它。在synchronized机制中,每个对象都有一个,当某个线程需要访问标记的代码时,它必须先申请并获得该对象的才能执行相关代码,当线程执行完毕,它会主动释放synchronized机制的优点是简单易用,而且它可以自动解,不用担心死的问题,但是如果我们需要更加灵活的控制,或者需要更高的性能,就需要使用Lock机制。 Lock锁机制LockJava中提供的另一种锁机制,它对于synchronized机制来说是一个更加灵活、可扩展和高效的替代方案。与synchronized机制相比,Lock机制可以手动控制的申请和释放,并且它支持多个条件变量和公平Lock机制需要手动加和解,需要使用try-catch-finally代码块来确保总是释放Lock机制比synchronized机制在性能上更高效,因为它的加过程更为细节化,支持更多的类型和粒度,具有更高的灵活性。 总结: 无论是synchronized还是Lock,它们都是Java中的锁机制,但它们的使用场景和实现方式有所不同。synchronized机制适用于对于简单的控制,能够自动释放,不会产生死问题;而Lock机制则更加灵活与可扩展,适用于更为复杂的并发控制场景,可以在性能、加粒度、类型等方面进行更高级别的控制。无论哪一个锁机制,我们都应该在程序中恰当的选择,并慎重使用,以确保程序的性能和安全。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

有点东西且很多

你的鼓励是我最大的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值