【总结】锁

01 / synchronized

1.1锁的粒度

  • 对于同步代码块,锁是synchonized括号里配置的对象;
  • 对于成员同步方法,锁是当前实例对象(this) ;
  • 对于静态同步方法,锁是当前类的Class对象。

1.2实现机制

synchronized用的锁是存在Java对象头里的。如果对象是数组类型,则虚拟机用3个字宽存储对象头,如果对象是非数组类型,则用2字宽存储对象头。其中,Mark Word里存储了锁的信息,包括锁的标志位与状态。

对象头存的是对象的描述信息,对象体:对象的具体属性

在这里插入图片描述

无锁状态01,偏向锁也是01,但是前面还有一个1

无锁01(偏向标志0)—》偏向锁01(偏向标志为1)----》轻量级锁00----》重量级锁10

锁升级步骤:

当没有线程访问的时候为无锁,当有一个线程访问的时候升级为偏向锁,当有两个线程并发访问的时候升级为轻量级锁(锁存到线程的栈帧里,有一个区域指向锁的位置),如果竞争进一步加剧,其中的线程以自旋抢几次升级为重量级锁

注意:轻量级锁没有获得锁的线程并不会阻塞,而是自旋以cas方式去抢锁。抢了几次抢不到会升级为重量级锁,进入阻塞

偏向锁访问临界区会打个标记,下次访问的时候看下标记变没,没变就继续访问,变了就升级为轻量级锁,自旋去抢锁,抢了几次抢不到会升级为重量级锁,进入阻塞

1.3锁的升级

Java 1.6为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁"和“轻量级锁”。在Java1.6中, 锁-共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。

1.4偏向锁

当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID.以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试-一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。
偏向锁在Java 6和Java 7里是默认启用的,但是它在应用程序启动几秒钟之后才激活,如有必要可以使用JVM参数来关闭延迟。如果你确定应用程序里所有的锁通常情况下处于竞争状态,可以通过JVM参数关闭偏向锁。

image-20210915155321095

1.5轻量级锁

加锁时,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的MarkWord复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。
解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。

image-20210915160527001

(无锁指的是偏向锁取消后的状态)

轻量级锁指向栈的指针,重量级锁指向monitor

各个锁的关键点:

偏向锁只有一个线程访问,存自己线程ID做个标记。轻量级锁没有抢到锁的线程自旋,重量级锁没有抢到的要阻塞,抢到的线程释放锁的时候还有唤醒阻塞的线程

CAS原子替换(比较并修改),先比较一下原来的值变没变,如果变了,修改失败,触发重量级锁的释放过程,并唤醒其他阻塞的线程

02 / AQS

队列同步器AbstractQueuedSynchronizer(抽象类),是用来构建锁或者其他同步组件的基础框架,它使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。同步器的设计是基于模板方法模式的,也就是说,使用者需要继承同步器并重写指定的方法,随后将同步器组合在自定义同步组件的实现中,并调用同步器提供的模板方法,而这些模板方法将会调用使用者重写的方法。

因为锁不知只有synchronized还有Lock(底层AQS)

模板方法模式:已知做这个事情的流程(1。。2.。。3.。),1,3都实现了,但是2要根据具体的业务去实现,所以将2设为抽象类,让具体子类去实现,即就是一个模板

核心代码:

image-20210915163548889

public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
    }

AQS:可以实现 以独占的方式加锁解锁,共享锁(同时加锁):读写分离

加锁是一套流程先试试能不能抢到锁,抢不到要排队。在不同的场景下(重入锁,不/公平锁),排队的机制是一样的,但是抢锁方案是不一样的

抢锁方案单独提出来要重写,不重写会抛异常,为什么不定义为抽象的?

因为有可能当前做的锁就是互斥锁(读锁),用不到其他的方法,就没必要重写。即根据实用性,用什么锁重写什么方法

同步队列:此时还有那些线程在等待,顺序是怎么样的Node是双向的链表,

同步状态:不同的场景不同的数值代表的意义不一样,

AQS是抽象的,他只是一个框架

2.2同步队列

当前线程获取同步状态失败时,同步器会将当前线程以及等待状态等信息构造成为一个节点并将其加入同步队列,同时会阻塞当前线程。当同步状态释放时,会把首节点中的线程唤醒,使其再次尝试获取同步状态。

同步队列是一个双向链表,同步器拥有链表的头和尾节点

image-20210915165412364

image-20210915165835062

当来新的线程时,插入队列的尾节点(cas)

头节点时拿到锁的线程,后面是等待的头结点是获取锁了没有竞争不需要CAS去改,而尾部是有竞争的,需要CAS去修改

第一次创建头节点也是要CAS,有了链表后,在改变头节点,不需要CAS

image-20210915170246426

2.3阅读源码:

1.独占式获取同步状态: acquire(). addWaiter(). enq(). acquireQueued)
2.独占式释放同步状态: release()
3.共享式获取同步状态: acquireShared(), doAcquireShared(). setHeadAndPropagate()
4.共享式释放同步状态: releaseShared(). doReleaseShared()

独占式获取同步状态: acquire():加锁
public final void acquire(int arg) {
    //尝试抢锁,抢不成功排队
        if (!tryAcquire(arg) //尝试抢锁 &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))//排队,先把node加到队列里,在处理队列中的状态
            selfInterrupt(); //acquireQueued不支持中断,支持中断信号,打一个中断信号,
    }

//抢锁是否成功
 protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
    }

//把当前线程节点加到同步队列里面去
private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {//以CAS方式添加尾节点
                pred.next = node;
                return node;
            }
        }
        enq(node);//添加失败时,自旋
        return node;
    }

//自旋
 private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))//第一次创建头节点也是要CAS,有了链表后,在改变头节点,不需要CAS
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

//队列中的节点什么时候阻塞或唤醒
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);
        }
    }
独占式释放同步状态: release()
public final boolean release(int arg) {
        if (tryRelease(arg)) {//尝试释放锁
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);//唤醒后继节点,头节点的后继节点被唤醒
            return true;
        }
        return false;
    }


protected boolean tryRelease(int arg) {
        throw new UnsupportedOperationException();
    }

//唤醒后继节点的核心
if (s != null)
            LockSupport.unpark(s.thread);
//它调用了unsafe的unpark
public static void unpark(Thread thread) {
        if (thread != null)
            UNSAFE.unpark(thread);
    }

共享式获取同步状态: acquireShared()

共享锁是不互斥的,后面依旧是共享锁,它也可以加锁成功,要传播,如果是一串共享锁,都会加锁成功,但是如果出现互斥锁,排他锁,传播停止。

public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)//尝试失败:有独占锁,当前有共享锁存在会成功
            doAcquireShared(arg);//排队
    }

protected int tryAcquireShared(int arg) {
        throw new UnsupportedOperationException();
    }

//排队
 private void doAcquireShared(int arg) {
        final Node node = addWaiter(Node.SHARED);//将当前节点加到队列里面
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {//自旋
                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;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())//阻塞
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

//setHeadAndPropagate
Node s = node.next;
            if (s == null || s.isShared())
                doReleaseShared();//递归下去,知道下一个节点null或者不是共享状态是独占状态
共享式释放同步状态: releaseShared()
public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {//解锁成功
            doReleaseShared();//
            return true;
        }
        return false;
    }
//唤醒后继节点
private void doReleaseShared() {
        for (;;) {
            Node h = head;
            if (h != null && h != tail) {
                int ws = h.waitStatus; //等待和通知
                if (ws == Node.SIGNAL) {
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;            // loop to recheck cases
                    unparkSuccessor(h);//唤醒后继节点
                }
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            if (h == head)                   // loop if head changed
                break;
        }
    }


AQS不是具体的锁,他只是锁底层的东西

03 / Lock

3.1 Lock接口

image-20210915180251358

加锁synchronized 和Lock的区别?

synchronized自动解锁,Lock需要手动释放

synchronized有什么缺点?才要有Lock

1、Lock加锁的时候可以被中断,避免死锁。而sy。。没有

2、尝试加锁tryLock,避免阻塞

3、支持超时

4、Condition通过锁创建的,它是一个方法,一个Lock可以有多个Condition。创建多个等待通知组件,即可以有多个等待队列

synchronized是利用锁来实现等待通知(通过wait,notify来通知),底层是同步队列,等待队列,只有一个等待队列。

3.2 ReentrantLock实现

可重入锁(对于一个线程可以重复加锁),Sync继承了AQS,并且是一个抽象类,有两个实现NonfairSync,FairSync不公平和公平

image-20210915181444437

3.3阅读源码

锁的构造: ReentrantLock

//默认是不公平的锁,可以插队,效率高
public ReentrantLock() {
        sync = new NonfairSync();
    }
//业务场景需要公平的时传入参数true
public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

//加锁,解锁都是调用sync
public void lock() {
        sync.lock();//调用同步器的lock
    }

//加锁有公平和不公平,需要根据业务场景,所以abstract交给子类去实现
abstract void lock();

 // Sync实现独占的互斥锁又是一个不公平锁,还是一个可重入锁
//继承了AQS要重写里面的try方法
 abstract static class Sync extends AbstractQueuedSynchronizer
     
  //重写了tryRelease,没有实现tryacquired,因为有公平锁和不公平锁,没法具体实现   
 protected final boolean tryRelease(int releases) {
            int c = getState() - releases;//重入次数-释放锁的次数
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);//当前线程清空
            }
            setState(c);
            return free;
        }

Sync有(NonfairSync, FairSync),它继承AQS

image-20210915183632193

NonfairSync:不公平锁

不公平体现两点:第一加锁的时候先抢锁,抢不到去排队,

第二:排队的时候再先抢一下

可重入性:如果锁不为0,还是自己,那就改变重入次数

 static final class NonfairSync extends Sync{
     
     final void lock() {
            if (compareAndSetState(0, 1))//抢锁:不公平的体现1
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);//排队
        }
     
     protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
     
     //不公平的加锁
     final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {//此时没有锁(不代表没有人在排队)
                if (compareAndSetState(0, acquires)) {//以CAS改状态加锁,不公平的体现2
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {//独占线程是自己
                int nextc = c + acquires;//修改重入次数
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
    
 }

FairSync公平锁:

公平体现在两点:第一先排队,第二在排队的时候如果锁为0,先判断一下队列中是否还有前驱节点,没有才加锁

重入体现在:排队的时候如果锁不为0,先看锁是不是自己,如果是,增加重入次数

 static final class FairSync extends Sync {
     //一上来就排队
      final void lock() {
            acquire(1);
        }
     
     //尝试加锁
      protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {//没有锁,先判断同步队列重前面有没有前驱节点,有就等待,无就加锁
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
     
 }

04 /ReadWriteLock

4.1 ReadWriteLock接口

image-20210915210611320

写锁是独占的(acqurie),读锁(acquireShared),写锁和任何锁互斥(写写,写读)

4.2 ReentrantReadWriteLock实现

image-20210915210934435

public ReentrantReadWriteLock() {
        this(false);
    }

public ReentrantReadWriteLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();//指定公平不公平
        readerLock = new ReadLock(this);//读锁和写锁的公平性要是一致的!!
        writerLock = new WriteLock(this);
    }

protected ReadLock(ReentrantReadWriteLock lock) {
            sync = lock.sync;//先创建同步器,在创建读锁写锁的时候,把同步器带给这两个锁
        }

public void lock() {
            sync.acquireShared(1);//读锁时共享的
        }

public void unlock() {
            sync.releaseShared(1);
        }
//整个读锁内部是使用sync和shared的方法实现的,---共享

-----------写锁----------------
 protected WriteLock(ReentrantReadWriteLock lock) {
            sync = lock.sync;
        }

public void lock() {
            sync.acquire(1);
        }
 public void unlock() {
            sync.release(1);
        }
//写锁互斥的,独占,
//ReentrantLock是独占的,而ReentrantReadWriterLock有独占的和共享的

//Sync继承AQS

//读锁的数量
        static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
 //写锁的数量
        static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; } 
//读写锁共享一个状态,那怎么区分两种锁?
//将状态的32bit前16位(高位)存读锁,后16位(低位)存写锁
//注意:读写锁不能并存!!
    

Sync:(继承AQS,所以他要实现父类模板里面的方法,try…方法)

abstract boolean readerShouldBlock();

abstract boolean writerShouldBlock(); //要不要堵塞,需要子类去实现


//抢锁,独占锁
protected final boolean tryAcquire(int acquires) {
           
            Thread current = Thread.currentThread();
            int c = getState();
            int w = exclusiveCount(c);
            if (c != 0) { //状态不等于0,有人已经上锁了
                // (Note: if c != 0 and w == 0 then shared count != 0)
                if (w == 0 || current != getExclusiveOwnerThread())//看独占线程是不是自己
                    return false;
                if (w + exclusiveCount(acquires) > MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                // Reentrant acquire
                setState(c + acquires);//是自己,重入,加次数
                return true;
            }
            if (writerShouldBlock() ||
                !compareAndSetState(c, c + acquires))//要不要阻塞或者尝试抢锁
                return false;
            setExclusiveOwnerThread(current);//将当前线程设置为自己
            return true;
        }

//独占的释放锁
protected final boolean tryRelease(int releases) {
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            int nextc = getState() - releases;//解锁,减数量
            boolean free = exclusiveCount(nextc) == 0;//独占锁的数量是不是为0
            if (free)
                setExclusiveOwnerThread(null);//清空线程
            setState(nextc);
            return free;
        }


----------共享抢锁和释放
    protected final int tryAcquireShared(int unused) {
           
            Thread current = Thread.currentThread();
            int c = getState();
    //如果独占线程数不为0,有写锁
            if (exclusiveCount(c) != 0 &&
                getExclusiveOwnerThread() != current)
                return -1;
            int r = sharedCount(c);//当前读锁的数量
    //要不要阻塞并且CAS抢锁
            if (!readerShouldBlock() &&
                r < MAX_COUNT &&
                compareAndSetState(c, c + SHARED_UNIT)) {
                if (r == 0) {
                    firstReader = current;
                    firstReaderHoldCount = 1;
                } else if (firstReader == current) {
                    firstReaderHoldCount++;
                } else {
                    HoldCounter rh = cachedHoldCounter;
                    if (rh == null || rh.tid != getThreadId(current))
                        cachedHoldCounter = rh = readHolds.get();
                    else if (rh.count == 0)
                        readHolds.set(rh);
                    rh.count++;
                }
                return 1;
            }
            return fullTryAcquireShared(current);
        }

//共享解锁
protected final boolean tryReleaseShared(int unused) {
           。。。。
            for (;;) {//自旋
                int c = getState();
                int nextc = c - SHARED_UNIT;
                if (compareAndSetState(c, nextc))//CAS修改状态
                   
                    return nextc == 0;
            }
        }

公平和不公平体现在子类重写的方法

公平:不管读锁还是写锁,有前驱节点就阻塞

不公平锁:写锁不阻塞(直接抢),读锁要看看前面有没有写锁,有就阻塞,没有就强

05 / Condition

Condition定义了等待/通知两种类型的方法,当前线程调用这些方法时,需要提前获取到Condition对象关联的锁(依赖Lock)Condition对 象是由Lock对象创建出来的,换句话说Condition是 依赖Lock对象的。当调用Condition的await()方法后,当前线程会释放锁并在此等待。而其他线程调用Condition对象的signal()方法通知当前线程后,当前线程才从await()方法返回,并且在返回前已经获取了锁。

5.1线程通信

image-20210917151733713

5.2通信机制

调用等待方法,当前线程进入等待队列。调用通知方法,将等待队列中的线程转移到同步队列(将等待队列移到同步队列)synchronized只能有一个Monitor,所以它只有一个等待队列。Lock可 以创建出多个Condition,所以它拥有多个等待队列。

5.3 ArrayBlockingQueue源码

  • 目标:生产者通知消费者,消费者通知生产者。
  • 避免:生产者通知生产者,消费者通知消费者

生产者和消费都需要等待,但是synchronized只有一个等待队列,唤醒的时候可能会可能出现唤醒错误。

public ArrayBlockingQueue(int capacity) {
        this(capacity, false);
    }
public ArrayBlockingQueue(int capacity, boolean fair) {
        if (capacity <= 0)
            throw new IllegalArgumentException();
        this.items = new Object[capacity];
        lock = new ReentrantLock(fair);
        notEmpty = lock.newCondition();//用消费者
        notFull =  lock.newCondition();//生产者用
    }

//生产者放东西
 public void put(E e) throws InterruptedException {
        checkNotNull(e);
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == items.length)
                notFull.await();
            enqueue(e);
        } finally {
            lock.unlock();
        }
    }

两个等待队列可实现生产者精确通知消费者,消费者精确通知生产者,避免了生产者误通知生产者的情况

/** Condition for waiting takes */
    private final Condition notEmpty;//消费者拿东西

    /** Condition for waiting puts */
    private final Condition notFull;//生产者

//通过队列来实现,condition是接口,实现类是一个内部类定义在AQS的内部
 public class ConditionObject implements Condition, java.io.Serializable {
      static final class Node {//AQS里面的Node,同步队列和等待队列结构一样,但是等待队列有一个等待状态,
          
          
 //怎么去实现的await阻塞方法?
          //先加入队列,阻塞自己,知道别人唤醒了我,我到了同步队列去,再加锁
      public final void await() throws InterruptedException {
            if (Thread.interrupted())
                throw new InterruptedException();
            Node node = addConditionWaiter();//阻塞:把当前的线程加到等待队列里面去
            long savedState = fullyRelease(node);//解锁
            int interruptMode = 0;
            while (!isOnSyncQueue(node)) {//如果当前节点不在同步队列里,就阻塞
                LockSupport.park(this);
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            if (node.nextWaiter != null) // clean up if cancelled
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
        } 
 //唤醒机制
      //唤醒等待队列里面的第一个
          //头节点向后移(唤醒)
           private void doSignal(Node first) {
            do {
                if ( (firstWaiter = first.nextWaiter) == null)
                    lastWaiter = null;
                first.nextWaiter = null;
            } while (!transferForSignal(first) &&
                     (first = firstWaiter) != null);
        }
          

image-20210917154129855

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值