大厂之路一由浅入深、并行基础、源码分析一 “J.U.C.L”之读写锁ReentrantReadWriteLock源码级分析(基于AQS、共享/独占锁)

参考博客:

AQS 定义了两种资源共享方式

读写锁ReentrantReadWriteLock

  • 大型网站中很重要的一块内容就是数据的读写,ReentrantLock虽然具有完全互斥排他的效果(即同一时间只有一个线程正在执行lock后面的任务(独占锁的性能)),但是效率非常低。所以在JDK中提供了一种读写锁ReentrantReadWriteLock读写分离锁可以有效地帮助减少锁竞争,提升系统性能。
  • ReentrantReadWriteLock: 顾名思义,是读写锁。它维护了一对相关的锁 — — “读取锁”和“写入锁”,一个用于读取操作,另一个用于写入操作:
    • “读取锁”: 用于只读操作,它是 “共享锁”,能同时被多个线程获取。
    • “写入锁”: 用于写入操作,它是 “独占(排它)锁”,写入锁只能被一个线程锁获取。
    • 注意:所有读写锁的实现必须确保写操作对读操作的内存影响。换句话说,一个获得了读锁的线程必须能看到前一个释放的写锁所更新的内容
  • 读写锁比互斥锁允许对于共享数据更大程度的并发。每次只能有一个写线程,但是同时可以有多个线程并发地读数据
    • 我把这两个锁对应的操作进一步分析:
      • 读和读之间不互斥,因为读操作不会有线程安全问题;
      • 写和写之间互斥,避免一个写操作影响另外一个写操作,引发线程安全问题;
      • 读和写之间互斥,避免读操作的时候写操作修改了内容,引发线程安全问题
      • 总结起来就是,多个线程可以同时进行读取操作,但是同一时刻只允许一个线程进行写入操作。
  • 读写分离锁适用的场景:当读操作的次数 >> 写操作的次数,则读写锁就可以发挥最大的功效,提升系统的性能。

读写锁ReentrantReadWriteLock的特性

ReentrantReadWriteLock有如下特性:

  • 获取顺序
    • 非公平模式(默认):
      • 当以非公平初始化时,读锁和写锁的获取的顺序是不确定的。非公平锁主张竞争获取,可能会延缓一个或多个读或写线程,但是会比公平锁有更高的吞吐量。
    • 公平模式:
      • 当以公平模式初始化时,线程将会以队列的顺序获取锁。当前线程释放锁后,等待时间最长的写锁线程就会被分配写锁;或者有一组读线程组等待时间比写线程长,那么这组读线程组将会被分配读锁。
      • 当有写线程持有写锁或者有等待的写线程时,一个尝试获取公平的读锁(非重入)的线程就会阻塞。这个线程直到等待时间最长的写锁获得锁后并释放掉锁后才能获取到读锁。
  • 可重入性:
    • 允许读锁和写锁可重入。写锁可以获得读锁,读锁不能获得写锁。
  • 锁降级:🧡🧡
    • 遵循获取写锁、获取读锁在释放写锁的次序写锁能够降级成为读锁
  • 中断锁的获取:
    • 在读锁和写锁的获取过程中支持中断
  • 支持条件变量:
    • 写入锁提供了条件变量(Condition)的支持, 这个和独占锁一致, 但是读取锁却不允许获取条件变量, 将得到一个UnsupportedOperationException异常。
  • 监控:
    • 提供确定锁是否被持有等辅助方法

读写锁ReentrantReadWriteLock的结构

在这里插入图片描述

  • 通过读写锁ReentrantReadWriteLock的结构图可以得知:
    • ReentrantReadWriteLock实现了ReadWriteLock接口。ReadWriteLock是一个读写锁的接口,提供了"获取读锁的readLock()方法" 和 “获取写锁的writeLock()方法”;
    • ReentrantReadWriteLock中包含:
      • sync对象,读锁readerLock和写锁writerLock,它们都实现了Lock接口;
      • 读锁ReadLock和写锁WriteLock中也都分别包含了"Sync对象",它们的Sync对象和ReentrantReadWriteLockSync对象 是一样的,就是通过sync,读锁和写锁实现了对同一个对象的访问
      • 和"ReentrantLock"一样,sync是Sync类型;而且,Sync也是一个继承于AQS的抽象类,Sync也包括公平锁FairSync和非公平锁NonfairSync。sync对象是FairSyncNonfairSync中的一个,默认是NonfairSync

读写锁ReentrantReadWriteLock状态的设计

  • ReentrantLock自定义同步器的实现中,同步状态state表示锁被一个线程重复获取的次数,而读写锁的自定义同步器需要在同步状态(一个整形变量)上维护多个读线程和一个写线程的状态,那就需要“按位切割”使用这个状态变量,读写锁将变量切分成两部分,高16位表示读,低16位表示写,划分方式如下:
    在这里插入图片描述
  • 可以看到state的高16位代表读锁的个数;低16位代表写锁的状态;
  • 当前同步状态表示一个线程已经获取了写锁,且重进入了2次,同时也连续获取了两次读锁;
  • 同步状态是通过位运算进行更新的;
    • 假设当前同步状态是S,写状态等于S & EXCLUSIVE_MASK,即S & 0x0000FFFF,读状态等于S >>> 16.当写状态加1时,等于S+1;
    • 当读状态加1时,等于S+SHARED_UNIT,即S+(1 << 16),也就是S + 0x00010000;
  • 即读锁和写锁的状态获取和设置如下:
    • 读锁状态的获取:S >> 16 ;
    • 读锁状态的增加:S + (1 << 16);
    • 写锁状态的获取:S & 0x0000FFFF;
    • 写锁状态的增加:S + 1;

读写锁ReentrantReadWriteLock的方法和属性

读写锁ReentrantReadWriteLock的方法

  • ReadWriteLock 接口的方法:
public interface ReadWriteLock {
	// 返回用于读取操作的锁。
    Lock readLock();
    // 返回用于写入操作的锁。
    Lock writeLock();
}
  • Java并发库中ReetrantReadWriteLock实现了ReadWriteLock接口添加了可重入的特性。

  • ReentrantReadWriteLock有两个构造方法,如下:

  • ReentrantReadWriteLock方法:

// 创建一个新的 ReentrantReadWriteLock,默认是采用非公平锁。
ReentrantReadWriteLock()
// 创建一个新的 ReentrantReadWriteLock,fair是判断是否为公平锁。fair为true,意味着公平锁;否则,意味着非公平锁。
ReentrantReadWriteLock(boolean fair)
// 返回当前拥有写入锁的线程,如果没有这样的线程,则返回 null。
protected Thread getOwner()
// 返回一个 collection,它包含可能正在等待获取读取锁的线程。
protected Collection<Thread> getQueuedReaderThreads()
// 返回一个 collection,它包含可能正在等待获取 读取或写入锁 的线程。
protected Collection<Thread> getQueuedThreads()
// 返回一个 collection,它包含可能正在等待获取 写入锁 的线程。
protected Collection<Thread> getQueuedWriterThreads()
// 返回等待获取读取或写入锁的线程估计数目。
int getQueueLength()
// 查询当前线程在此锁上保持的重入读取锁数量。
int getReadHoldCount()
// 查询在此锁上保持的读取锁数量。
int getReadLockCount()
// 返回一个 collection,它包含可能正在等待与写入锁相关的给定条件的那些线程。
protected Collection<Thread> getWaitingThreads(Condition condition)
// 返回正等待与写入锁相关的给定条件的线程估计数目。
int getWaitQueueLength(Condition condition)
// 查询当前线程在此锁上保持的重入写入锁数量。
int getWriteHoldCount()
// 查询是否给定线程正在等待获取读取或写入锁。
boolean hasQueuedThread(Thread thread)
// 查询是否所有的线程正在等待获取读取或写入锁。
boolean hasQueuedThreads()
// 查询是否有些线程正在等待与写入锁有关的给定条件。
boolean hasWaiters(Condition condition)
// 如果此锁将公平性设置为 ture,则返回 true。
boolean isFair()
// 查询是否某个线程保持了写入锁。
boolean isWriteLocked()
// 查询当前线程是否保持了写入锁。
boolean isWriteLockedByCurrentThread()
// 返回用于读取操作的锁。
ReentrantReadWriteLock.ReadLock readLock()
// 返回用于写入操作的锁。
ReentrantReadWriteLock.WriteLock writeLock()

读写锁ReentrantReadWriteLock的属性

  • 我们来看一看Sync类的主要属性和方法:
		// 读锁同步状态占用的位数🧡
        static final int SHARED_SHIFT   = 16;
        // 每次增加读锁同步状态,就相当于增加SHARED_UNIT🧡
        static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
        // 读锁或写锁的最大请求数量(包含重入)🧡
        static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
        // 低16位的MASK,用来计算写锁的同步状态🧡
        static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
       // 返回共享锁数(读锁)🧡
        static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
        // 返回独占锁数🧡
        static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

读写锁ReentrantReadWriteLock的构造方法

  • ReentrantReadWriteLock有两个构造方法,如下:
    public ReentrantReadWriteLock() {
        this(false);
    }
    /**
     * Creates a new {@code ReentrantReadWriteLock} with
     * the given fairness policy.
     *
     * @param fair {@code true} if this lock should use a fair ordering policy
     */
    public ReentrantReadWriteLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
        readerLock = new ReadLock(this);
        writerLock = new WriteLock(this);
    }
  • 通过上述源码可以看出,默认的构造方法是非公平模式,创建的Sync是NonSync对象,然后初始化读锁、写锁。初始化的时候,调用方法如下:
    public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
    public ReentrantReadWriteLock.ReadLock  readLock()  { return readerLock; }
  • 总结,构造方法决定了Sync是FairSync还是NonfairSync。Sync继承了 AQS,而Sync是一个抽象类,NonfairSync和FairSync继承了Sync,并重写了其中的抽象方法。

读写锁ReentrantReadWriteLock的一些其他方法

  • getOwner(): 返回当前获得写锁的线程,如果没有线程占有写锁,那么返回null。实现如下:
    protected Thread getOwner() {
        return sync.getOwner();
    }
    final Thread getOwner() {   //Sync中的getOwner()
     // 如果独占锁的个数为0,说明没有线程占有写锁,那么返回null;否则返回占有写锁的线程。
     return ((exclusiveCount(getState()) == 0) ?  null : getExclusiveOwnerThread());
 	}
  • getReadLockCount(): 方法用于返回读锁的个数,实现如下:
public int getReadLockCount() {
        return sync.getReadLockCount();
    }
final int getReadLockCount() {    //Sync中的 getReadLockCount()
       return sharedCount(getState());
    }
static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
  • 从上面代码可以看出,要想得到读锁的个数,就是看AQS的state的高16位。这和前面讲过的一样,高16位表示读锁的个数,低16位表示写锁的个数。
  • getReadHoldCount(): 用于返回当前线程所持有的读锁的个数,如果当前线程没有持有读锁,则返回0:
final int getReadHoldCount() {   //Sync的实现:
            //如果没有读锁,自然每个线程都是返回0
            if (getReadLockCount() == 0)
                return 0;
            //得到当前线程
            Thread current = Thread.currentThread();
  //如果当前线程是第一个读线程,返回firstReaderHoldCount参数
            if (firstReader == current)
                return firstReaderHoldCount;
   //如果当前线程不是第一个读线程,得到HoldCounter,返回其中的count
            HoldCounter rh = cachedHoldCounter;
 //如果缓存的HoldCounter不为null并且是当前线程的HoldCounter,直接返回count
            if (rh != null && rh.tid == getThreadId(current))
                return rh.count;
            
  //如果缓存的HoldCounter不是当前线程的HoldCounter,那么从ThreadLocal中得到本线程的HoldCounter,返回计数         
            int count = readHolds.get().count;
 //如果本线程持有的读锁为0,从ThreadLocal中移除
            if (count == 0) readHolds.remove();
            return count;
        }
  • 从上面的代码中,可以看到两个熟悉的变量,firstReaderHoldCounter类型。这两个变量在读锁的获取中接触过,前面没有细说,这里细说一下。HoldCounter类的实现如下:

HoldCounter类的相关介绍

  • HoldCounter保存了线程持有共享锁(读锁)的数量,包括重入的数量;
  • HoldCounter类主要起着计数器的作用,对读锁的获取与释放操作会更新对应的计数值。若线程获取读锁,则该计数器+1,释放读锁,该计数器-1。只有当线程获取读锁后才能对读锁进行释放、重入操作。
       static final class HoldCounter {
      		 // 计数器
            int count;         
            // 线程ID
            final long tid = LockSupport.getThreadId(Thread.currentThread());
        }
  • HoldCounter类很简单,只有一个计数器count变量和线程ID tid变量, 在Java中,若是我们需要将某个对象与线程绑定,就只有ThreadLocal类才能实现了。在ReentrantReadWriteLock类中还有一个ThreadLocal类的子类:
  • readHoldsThreadLocalHoldCounter类,定义如下:
static final class ThreadLocalHoldCounter
    extends ThreadLocal<HoldCounter> {
    public HoldCounter initialValue() {
        return new HoldCounter();
    }
}
  • 通过上面的类HoldCounter就可以与线程进行绑定了。从上面我们可以看到ThreadLocalHoldCounter绑定到当前线程上,同时HoldCounter也持有线程ID,这样在释放锁的时候才能知道ReadWriteLock里面缓存的上一个读取线程(cachedHoldCounter)是否是当前线程。这样做的好处是可以减少ThreadLocal.get()的次数,因为这也是一个耗时操作。需要说明的是这样HoldCounter绑定线程ID而不绑定线程对象的原因是避免HoldCounter和ThreadLocal互相绑定而GC难以释放它们(尽管GC能够智能的发现这种引用而回收它们,但是这需要一定的代价),所以其实这样做只是为了帮助GC快速回收对象而已。
  • 看到这里我们明白了HoldCounter的作用,我们再看看fullTryAcquireShared(Thread)方法获取读锁的代码段:
if (sharedCount(c) == 0) {
    firstReader = current;
    firstReaderHoldCount = 1;
} else if (firstReader == current) {
    firstReaderHoldCount++;
} else {
    if (rh == null)
        rh = cachedHoldCounter;
    if (rh == null || rh.tid != getThreadId(current))
        rh = readHolds.get();
    else if (rh.count == 0)
        readHolds.set(rh);
    rh.count++;
    cachedHoldCounter = rh; // cache for release
}
  • 这段代码涉及了几个变量:firstReaderfirstReaderHoldCountcachedHoldCounter 。我们先理清楚这几个变量:
    • firstReader 看名字就明白了为第一个获取读锁的线程;
    • firstReaderHoldCount为第一个获取读锁的重入数;
    • cachedHoldCounterHoldCounter的缓存;
  • 理清楚上面所有的变量了,HoldCounter也明白了,我们就来给上面那段代码标明注释,如下:
if (sharedCount(c) == 0) {
    firstReader = current;
    firstReaderHoldCount = 1;
} else if (firstReader == current) {
    // 如果获取读锁的线程为第一次获取读锁的线程,则firstReaderHoldCount重入数 + 1
    firstReaderHoldCount++;
} else {
    // 非firstReader计数
    if (rh == null)
        rh = cachedHoldCounter;
    // rh == null或者rh.tid != current.getId(),需要获取rh
    if (rh == null || rh.tid != getThreadId(current))
        rh = readHolds.get();
    // 加入到readHolds中
    else if (rh.count == 0)
        readHolds.set(rh);
    // 计数器 + 1
    rh.count++;
    cachedHoldCounter = rh; // cache for release
}
  • 这里解释下为何要引入firstRead、firstReaderHoldCount。这是为了一个效率问题,firstReader是不会放入到readHolds中的,如果读锁仅有一个的情况下就会避免查找readHolds。
  • getWriteLockCount(): 返回写锁的个数,Sync的实现如下:
 final int getWriteHoldCount() {
            return isHeldExclusively() ? exclusiveCount(getState()) : 0;
        }
  • 可以看到如果没有线程持有写锁,那么返回0;否则返回AQS的state的低16位。
  • 总结:
    • 当分析ReentranctReadWriteLock时,或者说分析内部使用AQS实现的工具类时,需要明白的就是AQS的state代表的是什么:
      • ReentrantLockReadWriteLock中的state同时表示写锁和读锁的个数。
    • 为了实现这种功能,state的高16位表示读锁的个数,低16位表示写锁的个数。
    • AQS有两种模式:共享模式和独占模式,读写锁的实现中,读锁使用共享模式;写锁使用独占模式;另外一点需要记住的即使,当有读锁时,写锁就不能获得;而当有写锁时,除了获得写锁的这个线程可以获得读锁外,其他线程不能获得读锁。

读写锁ReentrantReadWriteLock的使用

读写锁ReentrantReadWriteLock的常用例子

  • 接下来我们通过程序看到底如何简单使用ReentrantReadWriteLock
  • 第一个程序:证明读和读共享:
package com.wwj.lockdemos;

import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReentrantLockReadWriteLockDemo extends ReentrantReadWriteLock {

    public void read(){
        try {
            readLock().lock();
            System.out.println(Thread.currentThread().getName()+"获得了锁,时间为"+System.currentTimeMillis());
            Thread.sleep(1000);
        }catch (InterruptedException e){
            e.printStackTrace();
        } finally{
            readLock().unlock();
        }
    }

    public static void main(String[] args) {
        ReentrantLockReadWriteLockDemo rwl = new ReentrantLockReadWriteLockDemo();
        Runnable readRunnable = new Runnable() {
            @Override
            public void run() {
                rwl.read();
            }
        };
        Thread t0 = new Thread(readRunnable);
        Thread t1 = new Thread(readRunnable);
        Thread t2 = new Thread(readRunnable);
        t0.start();
        t1.start();
        t2.start();
    }
}
/*结果:
Thread-1获得了锁,时间为1623388134341
Thread-2获得了锁,时间为1623388134341
Thread-0获得了锁,时间为1623388134341
*/
  • 尽管方法加了锁,还休眠了1秒,但是三个线程还是几乎同时执行lock()方法后面的代码,看时间就知道了。说明lock.readLock()读锁可以提高程序运行效率,允许多个线程同时执行lock()方法后面的代码
  • 第二个程序:证明写和写互斥:
package com.wwj.lockdemos;

import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReentrantLockReadWriteLockDemo extends ReentrantReadWriteLock {

    public void write(){
        try {
            writeLock().lock();
            System.out.println(Thread.currentThread().getName()+"获得了锁,时间为"+System.currentTimeMillis());
            Thread.sleep(1000);
        }catch (InterruptedException e){
            e.printStackTrace();
        } finally{
            writeLock().unlock();
        }
    }

    public static void main(String[] args) {
        ReentrantLockReadWriteLockDemo rwl = new ReentrantLockReadWriteLockDemo();
        Runnable writeRunnable= new Runnable() {
            @Override
            public void run() {
                rwl.write();
            }
        };
        Thread t0 = new Thread(writeRunnable);
        Thread t1 = new Thread(writeRunnable);
        Thread t2 = new Thread(writeRunnable);
        t0.start();
        t1.start();
        t2.start();
    }
}
/*
结果:
Thread-0获得了锁,时间为1623388262163
Thread-1获得了锁,时间为1623388263192
Thread-2获得了锁,时间为1623388264198
*/
  • 通过代码可看出:它们之间的时间结果正好差不多为1秒一次,正好和Thread.sleep(1000)差不多,说明写和写是互斥的;
  • 第三个程序:证明读和写互斥:
package com.wwj.lockdemos;

import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReentrantLockReadWriteLockDemo extends ReentrantReadWriteLock {

    public void read(){
        try {
            readLock().lock();
            System.out.println(Thread.currentThread().getName()+"获得了锁,时间为"+System.currentTimeMillis());
            Thread.sleep(1000);
        }catch (InterruptedException e){
            e.printStackTrace();
        } finally{
            readLock().unlock();
        }
    }
    public  void write(){
        try {
            writeLock().lock();
            System.out.println(Thread.currentThread().getName()+"获得了锁,时间为"+System.currentTimeMillis());
            Thread.sleep(1000);
        }catch (InterruptedException t){
            t.printStackTrace();
        }finally {
            writeLock().unlock();
        }
    }

    public static void main(String[] args) {
        ReentrantLockReadWriteLockDemo rwl = new ReentrantLockReadWriteLockDemo();
        Runnable readRunnable = new Runnable() {
            @Override
            public void run() {
                rwl.read();
            }
        };
        Runnable writeRunnable = new Runnable() {
            @Override
            public void run() {
                rwl.write();
            }
        };
        Thread t0 = new Thread(readRunnable);
        Thread t1 = new Thread(writeRunnable);
        t0.start();
        t1.start();

    }
}
/*
Thread-1获得了锁,时间为1623389013601
Thread-0获得了锁,时间为1623389014617
*/
  • 通过代码可看出:它们之间的时间结果正好差不多为1秒一次,正好和Thread.sleep(1000)差不多,说明读和写是互斥的;
  • 第四个程序:更深层次的运用:
package com.wwj.lockdemos;

import java.util.Random;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReentrantLockReadWriteLockDemo extends ReentrantReadWriteLock {
    private  static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    private  static Lock readLock = readWriteLock.readLock();
    private  static Lock writeLock = readWriteLock.writeLock();
    private  int value; //读写的数据

    public Object handleRead(Lock lock) throws  InterruptedException{
        try{
            lock.lock();
            System.out.println(System.currentTimeMillis());
            Thread.sleep(1000);
            return value;
        }finally {
            lock.unlock();
        }
    }
    public  void handleWrite(Lock lock , int newVal) throws InterruptedException{
        try{
            lock.lock();
            System.out.println(System.currentTimeMillis());
            Thread.sleep(1000);
            value = newVal;
        }finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        final  ReentrantLockReadWriteLockDemo demo = new ReentrantLockReadWriteLockDemo();
        Runnable readRunnable = new Runnable() {
            @Override
            public void run() {
                try {
                    demo.handleRead(readLock);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
            }
        };
        Runnable writeRunnable = new Runnable() {
            @Override
            public void run() {
                try{
                    demo.handleWrite(writeLock , new Random().nextInt());
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
            }
        };
        for(int i=0 ; i<3 ; i++){
            new Thread(readRunnable).start();
        }
        for(int i=0 ; i<2 ; i++){
            new Thread(writeRunnable).start();
        }
    }
}

/*结果:
读写锁对应结果:   重入锁结果:
1623405857354    1623406031617
1623405857354	 1623406032621
1623405857354	 1623406033628
1623405858357	 1623406034643
1623405859361	 1623406035653
*/
  • 我们分别模拟读操作和写操作,通过时间来看读操作是同时操作,而写操作正好差1秒,显然是互斥的。而重入锁的结果显然都是岔开1秒。

读写锁ReentrantReadWriteLock对一些集合封装使用

  • ReentrantReadWriteLock可以用来提高某些集合的并发性能。当集合比较大,并且读比写频繁时,可以使用该类。下面是TreeMap使用ReentrantReadWriteLock进行封装成并发性能提高的一个例子:
class RWDictionary {
   private final Map<String, Data> m = new TreeMap<String, Data>();
   private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
   private final Lock r = rwl.readLock();
   private final Lock w = rwl.writeLock();

   public Data get(String key) {
     r.lock();
     try { 
     	return m.get(key); 
     	}finally { 
     		r.unlock(); 
     	}
   }
   public String[] allKeys() {
     r.lock();
     try { 
     	return m.keySet().toArray(); 
     	} finally { 
     		r.unlock(); 
     	}
   }
   public Data put(String key, Data value) {
     w.lock();
     try{ 
     	return m.put(key, value); 
     	}finally { 
     		w.unlock(); 
     	}
   }
   public void clear() {
     w.lock();
     try {
      m.clear(); 
      }finally {
       	w.unlock(); 
       }
   }
 }

读写锁ReentrantReadWriteLock的获取锁

  • 我们先看一下获取共享锁的思想(lock):先通过tryAcquireShared()尝试获取共享锁:
    • 尝试成功的话,则直接返回;
    • 尝试失败的话,则通过doAcquireShared()不断的循环并尝试获取锁,若有需要,则阻塞等待。
    • doAcquireShared()在循环中每次尝试获取锁时,都是通过tryAcquireShared()来进行尝试的。
  • 下面看看“获取共享锁”的详细流程:
  • 共享锁的lock()ReadLock中,源码:
   public void lock() {
        sync.acquireShared(1);
    }
  • 我们发现其lock()调用了acquireShared(1)方法,源码:
public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}
  • Sync继承于AQS,acquireShared()定义在AQS中;
  • acquireShared()首先会通过tryAcquireShared()来尝试获取锁:
    • 尝试成功的话,则不再做任何动作(因为已经成功获取到锁了)。
    • 尝试失败的话,则通过doAcquireShared()来获取锁。doAcquireShared()会获取到锁了才返回。
  • tryAcquireShared()定义在ReentrantReadWriteLockSync中,源码:
protected final int tryAcquireShared(int unused) {
	//获取当前线程🧡
    Thread current = Thread.currentThread();
    // 获取同步状态(锁)🧡
    int c = getState();		
      // 如果存在写锁,且持有写锁的线程不是当前对象,返回-1,表示获取读锁失败🧡
    if (exclusiveCount(c) != 0 &&
        getExclusiveOwnerThread() != current)
        return -1;
    // 获取“读锁”的计数🧡
    int r = sharedCount(c);
    //如果读不需要阻塞,并且读锁的计数小于其最大值65535(16位),并且可以成功更新同步状态值,成功🧡
    if (!readerShouldBlock() &&
        r < MAX_COUNT &&
        compareAndSetState(c, c + SHARED_UNIT)) {
        // 如果是第一次获取读锁🧡
        if (r == 0) { 
        	  // 读锁第一次被线程获取🧡
            firstReader = current;
            firstReaderHoldCount = 1;
          //如果当前(读)线程重入了,记录firstReaderHoldCount🧡
        } else if (firstReader == current) { 
         // 读锁被第一次获取读锁的线程重复获取
           firstReaderHoldCount++;
            //当前读线程和第一个读线程不同,记录每一个线程读的次数🧡
        } else {
            // HoldCounter是用来统计该线程获取“读取锁”的次数🧡
            HoldCounter rh = cachedHoldCounter;
            if (rh == null || rh.tid != current.getId())
                cachedHoldCounter = rh = readHolds.get();
            else if (rh.count == 0)
                readHolds.set(rh);
            // 将该线程获取“读取锁”的次数+1🧡
            rh.count++;
        }
        return 1;
    }
     // // 获取读锁失败,调用fullTryAcquireShared(Thread)方法,放到循环里重试🧡
    return fullTryAcquireShared(current);
}
  • tryAcquireShared()的作用是尝试获取“共享锁”;
    • 如果当前有写线程并且本线程不是写线程,那么失败,返回-1;
    • 否则,说明当前没有写线程或者本线程就是写线程(可重入),接下来判断是否应该读线程阻塞并且读锁的个数是否小于最小值,并且CAS成功使读锁+1,成功,返回1。其余的操作主要是用于计数的
    • 如果前一步失败了,失败的原因有三种可能:
      • 读线程应该被阻塞;
      • 读锁达到了上线;
      • CAS更新同步状态state失败;
    • 失败会调用fullTryAcquireShared方法
  • fullTryAcquireShared()ReentrantReadWriteLock中定义,源码:
final int fullTryAcquireShared(Thread current) {
    HoldCounter rh = null;
    for (;;) {
        // 获取“锁”的状态🧡
        int c = getState();
         // 这里对应锁降级,若当前线程已持有写锁,则允许当前线程继续获取读锁,否则直接返回🧡
        if (exclusiveCount(c) != 0) {
            if (getExclusiveOwnerThread() != current)
                return -1;
        // 如果读线程需要阻塞等待
          // 当前线程是第一次获取读锁的线程
        } else if (readerShouldBlock()) {
            // 如果想要获取锁的线程(current)是第1个获取锁(firstReader)的线程
            if (firstReader == current) {
            } else {   
            // 当前线程不是第一次获取读锁的线程
                if (rh == null) {
                    rh = cachedHoldCounter;
                    if (rh == null || rh.tid != current.getId()) {
                        rh = readHolds.get();
                        if (rh.count == 0)
                            readHolds.remove();
                    }
                }
                // 如果当前线程获取锁的计数=0,则返回-1。
                if (rh.count == 0)
                    return -1;
            }
        }
        // 如果“不需要阻塞等待”,则获取“读锁”的计数;
        // 如果共享统计数超过MAX_COUNT,则抛出异常。
        if (sharedCount(c) == MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        // 将线程获取“读取锁”的次数+1。
        if (compareAndSetState(c, c + SHARED_UNIT)) {
            // 如果是第1次获取“读取锁”,则更新firstReader和firstReaderHoldCount。
            // 下面的处理与tryAcquireShared(int)类似
            if (sharedCount(c) == 0) {
                firstReader = current;
                firstReaderHoldCount = 1;
            // 如果想要获取锁的线程(current)是第1个获取锁(firstReader)的线程(重入锁),
            // 则将firstReaderHoldCount+1。
            } else if (firstReader == current) {
                firstReaderHoldCount++;
            } else {
                if (rh == null)
                    rh = cachedHoldCounter;
                if (rh == null || rh.tid != current.getId())
                    rh = readHolds.get();
                else if (rh.count == 0)
                    readHolds.set(rh);
                // 更新线程的获取“读锁”的计数
                rh.count++;
                cachedHoldCounter = rh; // cache for release
            }
            return 1;
        }
    }
}
  • 通过上述源码可以看出,fullTryAcquireShared()tryAcquireShared类似,会根据“是否需要阻塞等待”,“读取锁的共享计数是否超过限制”等等进行处理。如果不需要阻塞等待,并且锁的共享计数没有超过限制,则通过CAS尝试获取锁,并返回1。
  • 在上面可以看到多次调用了readerShouldBlock方法:
    • 对于公平锁,只要队列中有别的线程在等待,那么将会返回true即阻塞,也就意味着读线程需要阻塞;
    • 对于非公平锁,如果当前有线程获取了写锁(读锁不用),则返回true即阻塞。一旦不阻塞,那么读线程将会有机会获得读锁。
  • doAcquireShared()定义在AQS函数中,源码:
private void doAcquireShared(int arg) {
    // addWaiter(Node.SHARED)的作用是,创建“当前线程”对应的节点,并将该线程添加到CLH队列中。
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            // 获取“node”的前一节点
            final Node p = node.predecessor();
            // 如果“当前线程”是同步队列的表头,则尝试获取共享锁。
            if (p == head) {
                int r = tryAcquireShared(arg);
                if (r >= 0) {
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    if (interrupted)
                        selfInterrupt();
                    failed = false;
                    return;
                }
            }
 // 如果“当前线程”不是同步队列的表头,则通过shouldParkAfterFailedAcquire()判断是否需要等待,
 // 需要的话,则通过parkAndCheckInterrupt()进行阻塞等待。若阻塞等待过程中,线程被中断过,则设置interrupted为true。
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}
  • doAcquireShared()的作用是获取共享锁:
    • 它会首先创建线程对应的同步队列的节点,然后将该节点添加到同步队列中。同步队列是管理获取锁的等待线程的队列。
    • 如果“当前线程”是同步队列的表头,则尝试获取共享锁;否则,则需要通过shouldParkAfterFailedAcquire()判断是否阻塞等待,需要的话,则通过parkAndCheckInterrupt()进行阻塞等待。
    • doAcquireShared()会通过for循环,不断的进行上面的操作;目的就是获取共享锁。需要注意的是:doAcquireShared()在每一次尝试获取锁时,是通过tryAcquireShared()来执行的!
  • 独占锁的lock()writeLock中,源码:
  • 其实和readLock的流程差不读:
        public void lock() { 
            sync.acquire(1);
        }
🧡🧡🧡🧡🧡  🧡🧡🧡🧡🧡  🧡🧡🧡🧡🧡  🧡🧡🧡🧡🧡  🧡🧡🧡🧡🧡  🧡🧡🧡🧡🧡  🧡🧡🧡🧡🧡  🧡🧡🧡🧡
       public final void acquire(int arg) {   //AQS中的
   		 if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
       		 selfInterrupt();
    	}
   //从上面可以看到,写锁使用的是AQS的独占模式。首先尝试获取锁,如果获取失败,那么将会把该线程加入到等待队列中。🧡
🧡🧡🧡🧡🧡  🧡🧡🧡🧡🧡  🧡🧡🧡🧡🧡  🧡🧡🧡🧡🧡  🧡🧡🧡🧡🧡  🧡🧡🧡🧡🧡  🧡🧡🧡🧡🧡  🧡🧡🧡🧡    	
	      protected final boolean tryAcquire(int acquires) {  //Sync中的
	      /*
	       * Walkthrough:
	       * 1. If read count nonzero or write count nonzero
	       *    and owner is a different thread, fail.
	       * 2. If count would saturate, fail. (This can only
	       *    happen if count is already nonzero.)
	       * 3. Otherwise, this thread is eligible for lock if
	       *    it is either a reentrant acquire or
	       *    queue policy allows it. If so, update state
	       *    and set owner.
	       */
	        //得到调用lock方法的当前线程
	      Thread current = Thread.currentThread();
	      int c = getState(); //获取同步状态
	      int w = exclusiveCount(c);  //获得写锁的个数
	      // 同步状态不为0,表示至少有一个线程获取了写锁或者读锁
	      if (c != 0) {
	          // (Note: if c != 0 and w == 0 then shared count != 0)
	           // 如果写锁为0或者当前线程不是独占线程(不符合重入),返回false
 /*
	 该方法和ReentrantLock的tryAcquire(int)方法大致一样,只不过在判断重入时增加了一个读锁是否存在的判断。
	 因为要确保写锁的操作对读锁是可见的,如果在读锁存在的情况下允许获取写锁,那么那些已经获取读锁的其他线程 可能就
	 无法感知当前写线程的操作。
	 因此只有等读锁完全释放后,写锁才能够被当前线程所获取,一旦写锁获取了,所有其他读、写线程均会被阻塞。

 */
	          if (w == 0 || current != getExclusiveOwnerThread())  
	              return false;
	              //如果写锁的个数超过了最大值,抛出异常
	          if (w + exclusiveCount(acquires) > MAX_COUNT)
	              throw new Error("Maximum lock count exceeded");
	          // Reentrant acquire
	            // 写锁重入,返回true
	          setState(c + acquires);
	          return true;
	      }
	       //如果当前没有写锁或者读锁,如果写线程应该阻塞或者CAS失败,返回false
	      if (writerShouldBlock() || !compareAndSetState(c, c + acquires))
	          return false;
	           //否则将当前线程置为获得写锁的线程,返回true
	      setExclusiveOwnerThread(current);
	      return true;
	  }
🧡🧡🧡🧡🧡  🧡🧡🧡🧡🧡  🧡🧡🧡🧡🧡  🧡🧡🧡🧡🧡  🧡🧡🧡🧡🧡  🧡🧡🧡🧡🧡  🧡🧡🧡🧡🧡  🧡🧡🧡🧡    
	  final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
🧡🧡🧡🧡🧡  🧡🧡🧡🧡🧡  🧡🧡🧡🧡🧡  🧡🧡🧡🧡🧡  🧡🧡🧡🧡🧡  🧡🧡🧡🧡🧡  🧡🧡🧡🧡🧡  🧡🧡🧡🧡
       static void selfInterrupt() {
        Thread.currentThread().interrupt();
    }
  • 通过源码可以看出,流程大致分为三步:
    • 如果当前有写锁或者读锁:如果只有读锁,返回false,因为这时如果可以写,那么读线程得到的数据就有可能错误;如果有写锁,但是线程不同,即不符合写锁重入规则,返回false;
    • 如果写锁的数量将会超过最大值65535,抛出异常;否则,写锁重入;
    • 如果没有读锁或写锁的话,如果需要阻塞或者CAS失败,返回false;否则将当前线程置为获得写锁的线程
  • 从上面可以看到调用了writerShouldBlock方法,后面会有详细介绍。
  • 总结:
    • 如果当前没有写锁或读锁时,第一个获取锁的线程都会成功,无论该锁是写锁还是读锁;
    • 如果当前已经有了读锁,那么这时获取写锁将失败,获取读锁有可能成功也有可能失败;
    • 如果当前已经有了写锁,那么这时获取读锁或写锁,如果线程相同(可重入),那么成功;否则失败

读写锁ReentrantReadWriteLock的释放锁

  • 释放读锁的思想,是先通过tryReleaseShared()尝试释放共享锁。尝试成功的话,则通过doReleaseShared()唤醒“其他等待获取读锁的线程”,并返回true;否则的话,返回flase。
    • 获取锁要做的是更改AQS的状态值以及将需要等待的线程放入到队列中;
    • 释放锁要做的就是更改AQS的状态值以及唤醒队列中的等待线程来继续获取锁;
public  void unlock() {
    sync.releaseShared(1);
}
  • 该函数实际上调用releaseShared(1)释放读锁:
  • releaseShared()在AQS中实现,源码:
public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}
  • releaseShared()的目的是让当前线程释放它所持有的读锁。
    它首先会通过tryReleaseShared()去尝试释放读锁。尝试成功,则直接返回;尝试失败,则通过doReleaseShared()去释放读锁。
  • tryReleaseShared()定义在ReentrantReadWriteLock中,源码:
protected final boolean tryReleaseShared(int unused) {
    // 得到调用unlock的线程
    Thread current = Thread.currentThread();
    // 如果想要释放锁的线程(current)是第1个获取锁(firstReader)的线程,
    // 并且“第1个获取锁的线程获取锁的次数”=1,则设置firstReader为null;
    // 否则,将“第1个获取锁的线程的获取次数”-1。
    if (firstReader == current) {
        // assert firstReaderHoldCount > 0;
        if (firstReaderHoldCount == 1)
            firstReader = null;
        else
            firstReaderHoldCount--;
    // 获取rh对象,并更新“当前线程获取锁的信息”。
    } else {
 
        HoldCounter rh = cachedHoldCounter;
        if (rh == null || rh.tid != current.getId())
            rh = readHolds.get();
        int count = rh.count;
        if (count <= 1) {
            readHolds.remove();
            if (count <= 0)
                throw unmatchedUnlockException();
        }
        --rh.count;
    }
    for (;;) {
        // 获取锁的状态
        int c = getState();
        // 将锁的获取次数-1。
        int nextc = c - SHARED_UNIT;
        // 通过CAS更新锁的状态。
        if (compareAndSetState(c, nextc))
            return nextc == 0;
    }
}
  • tryReleaseShared()的作用是尝试释放读锁
  • 从上面可以看到,释放锁的第一步是更新firstReader或HoldCounter的计数,接下来进入死循环,尝试更新AQS的状态,一旦更新成功,则返回;否则,则重试。
  • 释放读锁对读线程没有影响,但是可能会使等待的写线程解除挂起开始运行。所以,一旦没有锁了,就返回true,否则false;返回true后,那么则需要释放等待队列中的线程,这时读线程和写线程都有可能再获得锁。
  • doReleaseShared()定义在AQS中,源码:
private void doReleaseShared() {
    for (;;) {
        // 获取同步队列的头节点
        Node h = head;
        // 如果头节点不为null,并且头节点不等于tail节点。
        if (h != null && h != tail) {
            // 获取头节点对应的线程的状态
            int ws = h.waitStatus;
            // 如果头节点对应的线程是SIGNAL状态,则意味着“头节点的下一个节点所对应的线程”需要被unpark唤醒。
            if (ws == Node.SIGNAL) {
                // 设置“头节点对应的线程状态”为空状态。失败的话,则继续循环。
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;
                // 唤醒“头节点的下一个节点所对应的线程”。
                unparkSuccessor(h);
            }
            // 如果头节点对应的线程是空状态,则设置“文件点对应的线程所拥有的共享锁”为其它线程获取锁的空状态。
            else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        // 如果头节点发生变化,则继续循环。否则,退出循环。
        if (h == head)                   // loop if head changed
            break;
    }
}
  • doReleaseShared()会释放“读锁”。它会从前往后的遍历同步队列,依次“唤醒”然后“执行”队列中每个节点对应的线程;
  • 最终的目的是让这些线程释放它们所持有的锁。
  • 我们接下来再看一下 写锁的释放:
 public void unlock() {
            sync.release(1);
        }
 🧡🧡🧡🧡🧡  🧡🧡🧡🧡🧡 🧡🧡🧡🧡🧡   🧡🧡🧡🧡🧡   🧡🧡🧡🧡🧡   🧡🧡🧡🧡🧡   🧡🧡🧡🧡🧡   🧡🧡
 public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}
//调用tryRelease尝试释放锁,一旦释放成功了,那么如果等待队列中有线程再等待,那么调用unparkSuccessor将下一个线程解除挂起。
//Sync需要实现tryRelease方法,如下:
🧡🧡🧡🧡🧡  🧡🧡🧡🧡🧡 🧡🧡🧡🧡🧡   🧡🧡🧡🧡🧡   🧡🧡🧡🧡🧡   🧡🧡🧡🧡🧡   🧡🧡🧡🧡🧡   🧡🧡
        protected final boolean tryRelease(int releases) {
        //如果本线程没有获取写锁,抛出异常 
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            int nextc = getState() - releases;
          // 判断写锁是否完全释放,若是,则将写锁持有线程设置为nul
            boolean free = exclusiveCount(nextc) == 0;
            //如果没有写锁了,那么将AQS的线程置为null
            if (free)
                setExclusiveOwnerThread(null);
                //更新状态
            setState(nextc);
            return free;
        }

  • 总结:写锁的释放分为两步:
    • 如果当前没有线程持有写锁,但是还要释放写锁,抛出异常;
    • 如果持有写锁,则判断写锁是否完全释放,然后更新AQS的状态;
  • 从上面可以看到,返回true当且只当没有写锁的情况下,还有写锁则返回false;
  • 释放锁的总结:
    • 如果当前是写锁被占有了,只有当写锁的同步状态降为0时才认为释放成功;否则失败。因为只要有写锁,那么除了占有写锁的那个线程,其他线程即不可以获得读锁,也不能获得写锁;
    • 如果当前是读锁被占有了,那么只有在写锁的个数为0时才认为释放成功。因为一旦有写锁,别的任何线程都不应该再获得读锁了,除了获得写锁的那个线程

读写锁ReentrantReadWriteLock的公平共享锁和非公平共享锁

  • 和互斥锁ReentrantLock一样,ReadLock、WriteLock 也分为公平锁和非公平锁;
  • 公平锁和非公平锁的区别:体现在判断是否需要阻塞的函数readerShouldBlock()writerShouldBlock()是不同的;
  • 公平锁的readerShouldBlock()、writerShouldBlock()的源码如下:
    static final class FairSync extends Sync {
        private static final long serialVersionUID = -2274990926593161451L;
        final boolean writerShouldBlock() {
            return hasQueuedPredecessors();
        }
        final boolean readerShouldBlock() {
            return hasQueuedPredecessors();
        }
    }
  • writerShouldBlockreaderShouldBlock方法都表示当有别的线程也在尝试获取锁时,是否应该阻塞;
  • 对于公平模式: 🧡🧡🧡
    • hasQueuedPredecessors()方法表示前面是否有等待线程。一旦前面有等待线程,那么为了遵循公平,当前线程也就应该被挂起。
  • 相应非公平锁的readerShouldBlock()\writerShouldBlock()的源码:
        final boolean writerShouldBlock() {
            return false; // writers can always barge
        }
        final boolean readerShouldBlock() {
            return apparentlyFirstQueuedIsExclusive();
        }
    }
  • 对于非公平模式: 🧡🧡🧡
    • writerShouldBlock()直接返回false,说明不需要阻塞;
    • readShouldBlock()调用了apparentFirstQueuedIsExcluisve()方法,该方法在当前线程是写锁占用的线程时,返回true;否则返回false。也就说明,如果当前有一个写线程正在写,那么该读线程应该阻塞。

读写锁ReentrantReadWriteLock的锁降级

  • 锁降级是指写锁降级成为读锁,但是需要遵循先获取写锁、获取读锁再释放写锁的次序,注意如果当前线程先获取写锁,然后释放写锁,再获取读锁这个过程不能称之为锁降级,锁降级一定要遵循那个次序。我们看一个锁降级的示例。因为数据不常变化,所以多个线程可以并发地进行数据处理,当数据变更后,如果当前线程感知到数据变化,则进行数据的准备工作,同时其他处理线程则被阻塞,直到当前线程完成数据的准备工作,如下述代码所示:
public void processData() {
    readLock.lock();
    if (!update) {
        // 必须先释放读锁
        readLock.unlock();
        // 锁降级从获取到写锁开始
        writeLock.lock();
        
        try {
            if (!update) {
                // 准备数据的流程(略)
                update = true;
            }
            readLock.lock();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            writeLock.unlock();
        }
        // 锁降级完成,写锁降级为读锁
    }
    
    try {
        // 使用数据的流程(略)
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        readLock.unlock();
    }
}
  • 在上述代码中,当数据发生变化后,update变量(布尔volatile类型)被设置为false,此时所有访问processData()方法的线程都能够感知到变化,但是只有一个线程能够获取到写锁,其它线程都会阻塞在读锁和写锁的lock()方法上。当前线程获取写锁完成数据准备之后,再获取读锁,随后释放写锁,完成锁降级。
  • 锁降级中读锁的获取是否是必要的呢?答案是必要的。主要原因是为了保证数据的可见性,如果当前线程不获取读锁而是直接释放写锁,假设此刻另一个线程(记作线程T)获取了写锁并修改了数据,那么当前线程无法感知线程T的数据更新。如果当前线程获取了读锁**,既遵循锁降级的步骤**,则线程T将会被阻塞,直到当前线程使用数据并释放读锁之后,线程T才能获取写锁进行数据更新。
  • ReentrantReadWriteLock不支持锁升级(把持读锁、获取写锁,最后释放读锁的过程)。目的也是为了保证数据可见性,如果读锁已被多个线程获取,其中任意线程成功获取了写锁并更新了数据,则其更新对其他获取到读锁的线程是不可见的。
  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值