AbstractQueuedSynchronizer

AbstractQueuedSynchronizer ---- jdk1.8

锁的大基础LockSupport

LockSupport的park() unpark()对比于Thread的 suspend和resume有如下优势:
Thead.suspend和Thread.resume有两种死锁场景,其一是不释放锁,其二是suspend和resume的顺序反了,oracel的原文是:If the thread that would resume the target thread attempts to lock this monitor prior to calling resume, deadlock results. Such deadlocks typically manifest themselves as “frozen” processes.而park和unpark没有顺序问题引起的死锁,但是park和unpark同样有互持不会释放锁的问题。如果在同步代码块中运行还是可能导致死锁的。

LockSupport 的park/unpark方法

其实park/unpark的设计原理核心是“许可”。park是等待一个许可。unpark是为某线程提供一个许可。如果某线程A调用park,那么除非另外一个线程调用unpark(A)给A一个许可,否则线程A将阻塞在park操作上。
有一点比较难理解的,是unpark操作可以再park操作之前。也就是说,先提供许可。当某线程调用park时,已经有许可了,它就消费这个许可,然后可以继续运行。
这个类当中的一些重要方法

	private static final long parkBlockerOffset;
	// 阻塞方法
	public static void park(Object blocker) {
        Thread t = Thread.currentThread();
        setBlocker(t, blocker); // 把当前线程阻塞到blockers上
        UNSAFE.park(false, 0L);
        setBlocker(t, null);
    }
	// 设定阻塞对象
	private static void setBlocker(Thread t, Object arg) {
        // Even though volatile, hotspot doesn't need a write barrier here.
        UNSAFE.putObject(t, parkBlockerOffset, arg);
    }
    // 获得阻塞对象
    public static Object getBlocker(Thread t) {
        if (t == null)
            throw new NullPointerException();
        return UNSAFE.getObjectVolatile(t, parkBlockerOffset);
    }

了解下AQS使用的数据结构,等待队列(双向链表)

AQS等待队列

AQS拥有一个同步队列和多个等待队列

AQS数据结构一个同步队列和多个等待队列

AQS之同步器–线程安全神器

这个体系下的锁都是通过各自的内部类Sync extends AbstractQueuedSynchronizera来实现同步的。其提供了一些基本的lock、release方法来解决同步问题。

先了解个名词CLH

CLH锁即Craig, Landin, and Hagersten (CLH)
locks。CLH锁是一个自旋锁。能确保无饥饿性。提供先来先服务的公平性。CLH锁也是一种基于链表的可扩展、高性能、公平的自旋锁,申请线程仅仅在本地变量上自旋,它不断轮询前驱的状态,假设发现前驱释放了锁就结束自旋。

	// AQS 中有一个深度释放重入锁的方法
    final int fullyRelease(Node node) {
        boolean failed = true;
        try {
            int savedState = getState();
            if (release(savedState)) {
                failed = false;
                return savedState;
            } else {
                throw new IllegalMonitorStateException();
            }
        } finally {
            if (failed)
                node.waitStatus = Node.CANCELLED;
        }
    }
    // 线程设置中断状态
    static void selfInterrupt() {
        Thread.currentThread().interrupt();
    }

AQS之ConditionObject–阻塞神器

ConditionObject是很重要的一个成员,看看他的类图。首先它通过Node维护了一个双向队列见上图2。然后也维护了firstWaiter和lastWaiter,也提供了最基本的方法await和signal/signalAll。
Condition

	// await方法,线程阻塞方法
	public final void await() throws InterruptedException {
       if (Thread.interrupted())// 当然了线程都中断了,就用不着阻塞了
           throw new InterruptedException();
       Node node = addConditionWaiter(); // 加入阻塞队列
       int savedState = fullyRelease(node); // 深度释放锁(重入的也统统释放)
       int interruptMode = 0;
       while (!isOnSyncQueue(node)) {
           LockSupport.park(this); // 最终的阻塞方法,看到没用的LockSupport.park,阻塞的条件是this,也就是当前的condition对象,调用await的线程都会被阻塞在该condition上
           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 Node addConditionWaiter() {
       Node t = lastWaiter;
       // If lastWaiter is cancelled, clean out.队列中最后一个都不阻塞了,理应都移出队列
       if (t != null && t.waitStatus != Node.CONDITION) {
           unlinkCancelledWaiters();
           t = lastWaiter;
       }
       Node node = new Node(Thread.currentThread(), Node.CONDITION); // 把当前线程构建成一个阻塞在condition上状态的一个阻塞节点
       if (t == null)
           firstWaiter = node; // 队列为空,加到队首
       else
           t.nextWaiter = node; // 否则加到队尾
       lastWaiter = node;
       return node;
   }
   // 至于定时阻塞都是通过自旋来等待时间结束的

   // 唤醒线程
   public final void signal() {
       if (!isHeldExclusively())
           throw new IllegalMonitorStateException();
       Node first = firstWaiter;
       if (first != null)
           doSignal(first);
   }
   private void doSignal(Node first) {
       do {
           if ( (firstWaiter = first.nextWaiter) == null)
               lastWaiter = null;
           first.nextWaiter = null;
       } while (!transferForSignal(first) &&
                (first = firstWaiter) != null);
   }
   final boolean transferForSignal(Node node) {
      /*
       * If cannot change waitStatus, the node has been cancelled.
       */
      if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
          return false;

      /*
       * Splice onto queue and try to set waitStatus of predecessor to
       * indicate that thread is (probably) waiting. If cancelled or
       * attempt to set waitStatus fails, wake up to resync (in which
       * case the waitStatus can be transiently and harmlessly wrong).
       */
      Node p = enq(node); // 加入竞争锁的队列
      int ws = p.waitStatus;
      if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
          LockSupport.unpark(node.thread);
      return true;
  }

ReentrantLock

一个重要的同步器 private final Sync sync; // 同步器是基于AQS的
通过构造方法来决定使用公平/非公平,默认是非公平
	public ReentrantLock() {
        sync = new NonfairSync();
    }
abstract static class Sync extends AbstractQueuedSynchronizer
定义了两个AQS,一个是公平锁,一个是非公平锁
static final class NonfairSync extends Sync // 非公平锁
static final class FairSync extends Sync // 公平锁

公平锁

	// 上锁 公平锁(static final class FairSync extends Sync)
	final void lock() {
           acquire(1); // 上来就以公平的方式去拿锁
    }
    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();// 如果acquireQueued的结果是当前线程已被中断 则中断当前线程
    }
    
    protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            if (!hasQueuedPredecessors() && // 此方法逻辑:true:CLH队列中已经有线程在当前线程前排队;false:当前线程是head节点,或者队列empty。这正是公平与非公平的区别之处,公平是上来看看CLH中是否有比当前行程还早的排队线程,非公平是上来就尝试取锁
                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;
    }
    /**
     * @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
     * @return the new node
     */
    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)) {
                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())) // 原子初始化,如果失败表名已经被其他线程初始化了,通过自旋再来一次加入队尾
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) { // 通过自旋无限次加入队尾,因为有竞争关系,所以不是每次都能成功
                    t.next = node;
                    return t;
                }
            }
        }
    }
    /**
     * acquireQueued()的作用就是“当前线程”会根据公平性原则进行阻塞等待,直到获取锁为止;并且返回当前线程在等待过程中有没有并中断过。
     */
    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)) { // 一般线程阻塞后由两种方式解除阻塞:1 别的线程调用interrupt();2 别的线程调用unpark()。如果是调用unpark() 则是其前驱节点触发的,所以会判断p==head,这也是公平性的保证。
                    setHead(node); // 拿到锁后把当前线程设置为head,
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt()) // 如果当前线程在阻塞过程中被interrupted了,则返回中断标识
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

非公平锁

	// 上锁 非公平锁(NonfairSync extends Sync)
	final void lock() {
       if (compareAndSetState(0, 1)) // 此处非公平第一次机会,上来就先抢一次锁
           setExclusiveOwnerThread(Thread.currentThread());
       else
           acquire(1);// 直接没抢着再通过非公平方式去抢一次
   }
   public final void acquire(int arg) {
	   if (!tryAcquire(arg) &&
	       acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
	       selfInterrupt();  
   }
   protected final boolean tryAcquire(int acquires) {
       return nonfairTryAcquire(acquires);
   }
   // 非公平获取锁逻辑,上来就直接尝试获取锁,没获得就返回false
   final boolean nonfairTryAcquire(int acquires) {
       final Thread current = Thread.currentThread();
       int c = getState();
       if (c == 0) {
           if (compareAndSetState(0, acquires)) { // 此处非公平第二次机会:来了不排队就去拿锁
               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;
   }

看看代码比对区别一下:
公平锁与非公平锁的获得锁方式对比

综上:
公平锁:当线程来获取锁的时候优先看是否有在之之前排队的,有的话就排队,然后自旋等待获取锁(除非被中断)
非公平锁:当线程来获取锁的时候不管前面有没有其他线程排队,直接尝试拿锁,没拿到的话后续就跟公平锁一样了加入队列。


ReentrantLock中的tryLock是走的非公平锁逻辑。

ReentrantLock.unlock() 此方法完全继承自Sync的父类AQS

    // 放锁
    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 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);// 释放后把排它锁的持有者置为 null
       }
       setState(c); // 这里可以看到完全释放后的线程状态时 0 (none)
       return free;
   }
   private void unparkSuccessor(Node node) {
	   /*
	    * 把head的线程的waitStatus设置为0(none)
	    */
	   int ws = node.waitStatus;
	   if (ws < 0)
	       compareAndSetWaitStatus(node, ws, 0);
	
	   /*
	    * Thread to unpark is held in successor, which is normally
	    * just the next node.  But if cancelled or apparently null,
	    * traverse backwards from tail to find the actual
	    * non-cancelled successor.
	    * 如果当前节点的next不为空且没有被cancel就唤醒它,如果不满足这个条件就从尾部开始一直倒推找到最靠近head的waitStatus为 SIGNAL = -1 | CONDITION = -2 | PROPAGATE = -3;的线程来唤醒它
	    */
	   Node s = node.next;
	   if (s == null || s.waitStatus > 0) {
	       s = null;
	       for (Node t = tail; t != null && t != node; t = t.prev)
	           if (t.waitStatus <= 0)
	               s = t;
	   }
	   if (s != null)
	       LockSupport.unpark(s.thread);
   }

等待队列

	// ReentrantLock#newCondition
    public Condition newCondition() {
        return sync.newCondition();
    }
	// Sync#newCondition
	final ConditionObject newCondition() {
        return new ConditionObject(); // 到这里就是使用的AQS的等待队列
    }

ReentrantReadWriteLock

读写锁大成员
可以看到读写锁中有两个Lock来分别控制读和写
读写锁也是基于AQS实现(Sync),读写锁也支持公平与非公平。

看看构造函数

	public ReentrantReadWriteLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
        readerLock = new ReadLock(this); // new 一个读锁
        writerLock = new WriteLock(this); // new 一个写锁
    }
    // 获取读写锁的方法
	public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
    public ReentrantReadWriteLock.ReadLock  readLock()  { return readerLock; }

ReadLock的lock方法

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

WriteLock的lock方法

public void lock() {
   sync.acquire(1); // 排它锁
}

BlockingQueue

这里的阻塞的一个直白解释:在需要的空间资源/对象资源不足时线程阻塞到此条件上,知道别的线程补给了相应的资源并唤醒阻塞线程。

BlockingQueue API
java 默认实现了如下阻塞队列(AbstractQueue作为一个基础)
在这里插入图片描述

ArrayBlockingQueue 和 LinkedBlockingQueue

这两个都是继承自AbstractQueue和实现BlockingQueue
异同如下:

  1. 实现数据结构不同 ABQ是基于数组存储元素的 final Object[] items且实例化时务必指定capacity,不可扩展;LBQ是基于双向链表存储元素的,实例化时capacity不是必须的。
  2. ABQ数据结构是数组,因此是通过putIndex和takeIndex来标记进出元素的位置的;LBQ是通过队列的first和last来标记进出元素位置的。
  3. 两个都是基于非公平锁ReentrantLock来控制线程安全的,都是通过lock对应的双condition来阻塞/唤醒增删线程的。

PriorityBlockintQueue

要搞明白优先阻塞队列,先来看一看优先队列PriorityQueue
PriorityQueue的数据结构是一个最小堆/最大堆 ,这个是线程不安全的

/**
 *Priority queue represented as a balanced binary heap: the two
 *children of queue[n] are queue[2*n+1] and queue[2*(n+1)]
 */
 transient Object[] queue;
 

然后看看它的构造方法

    public PriorityQueue(int initialCapacity,
                         Comparator<? super E> comparator) {
        // Note: This restriction of at least one is not actually needed,
        // but continues for 1.5 compatibility
        if (initialCapacity < 1)
            throw new IllegalArgumentException();
        this.queue = new Object[initialCapacity];
        this.comparator = comparator; // 比较器,优先级的判定者,如果不传默认是自然顺序
    }

优先核心逻辑,最小堆的维护

最小堆,是一种经过排序的完全二叉树,其中任一非终端节点的数据值均不大于其左子节点和右子节点的值。

    private void heapify() {
        for (int i = (size >>> 1) - 1; i >= 0; i--)//逆序是因为要确保所有的孩子与父节点都进行了比较,这里算出来的话正好是从最左边的第一个非叶子节点开始
            siftDown(i, (E) queue[i]);// 每个节点处理完的时间复杂度与子树的高度线性相关(O(logn))
    }
    private void siftDown(int k, E x) {
        if (comparator != null)
            siftDownUsingComparator(k, x);
        else
            siftDownComparable(k, x);
    }
    // 维护最小堆:把父节点与左右子节点中的最小者交换,直到父节点比左右子节点都小为止
    private void siftDownUsingComparator(int k, E x) {
        int half = size >>> 1;
        while (k < half) {
            int child = (k << 1) + 1; // 左子节点脚标
            Object c = queue[child]; // 临时变量指向左子节点,最终目的是要这个临时变量指向左右子节点中的最小者
            int right = child + 1; // 右子节点脚标
            if (right < size && // 右子节点脚标没越界
                comparator.compare((E) c, (E) queue[right]) > 0) // 如果左子节点大于右子节点
                c = queue[child = right];// 临时变量指向右子节点(最小的一个)
            if (comparator.compare(x, (E) c) <= 0) // 父节点小于等于原左孩子的值,满足最小堆的规则,不需要处理
                break;
            queue[k] = c; // 否则把左右自己点中的最小者赋值给父节点
            k = child;
        }
        queue[k] = x; // 左右子节点中最小者的位置放置父节点的值,其实就是跟父节点换位置
    }
    
	// 元素出列
	public E poll() {
        if (size == 0)
            return null;
        int s = --size;
        modCount++;
        E result = (E) queue[0]; // 从对顶出列(最小的)
        E x = (E) queue[s];
        queue[s] = null;
        if (s != 0)
            siftDown(0, x); // 然后把最后一个元素放到对顶后并进行最小堆维护。
        return result;
    }
    
	// 扩容很简单,小于64的时候近乎翻倍,大于64增加50% 
    private void grow(int minCapacity) {
        int oldCapacity = queue.length;
        // Double size if small; else grow by 50%
        int newCapacity = oldCapacity + ((oldCapacity < 64) ?
                                         (oldCapacity + 2) :
                                         (oldCapacity >> 1));
        // overflow-conscious code
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        queue = Arrays.copyOf(queue, newCapacity);
    }

PriorityBlockingQueue的优先级维护同PriorityQueue一致,PriorityBlockingQueue 只可能阻塞调用take方法的线程。
最大的区别有:

  • PriorityBlockingQueue 通过ReentrantLock及其Condition添加了阻塞关系。
  • PriorityBlockingQueue 的扩容是通过乐观锁CAS类进行的
	// 由于是一个无界队列,所以添加元素的操作永远不会阻塞,这样put和offer就一样了(看上去跟BlockingQueue接口中声明的put方法不一致?因为无界,所以无法!)
    public void put(E e) {
        offer(e); // never need to block
    }
    public boolean offer(E e) {
        if (e == null)
            throw new NullPointerException();
        final ReentrantLock lock = this.lock;
        lock.lock();
        int n, cap;
        Object[] array;
        while ((n = size) >= (cap = (array = queue).length))
            tryGrow(array, cap);// 扩容:注意try这个用词
        try {
            Comparator<? super E> cmp = comparator;
            if (cmp == null)
                siftUpComparable(n, e, array);
            else
                siftUpUsingComparator(n, e, array, cmp);
            size = n + 1;
            notEmpty.signal();
        } finally {
            lock.unlock();
        }
        return true;
    }
    
    /**
     * Tries to grow array to accommodate at least one more element
     * (but normally expand by about 50%), giving up (allowing retry)
     * on contention (which we expect to be rare). Call only while
     * holding lock.
     *
     * @param array the heap array
     * @param oldCap the length of the array
     */
    private void tryGrow(Object[] array, int oldCap) {
        lock.unlock(); // must release and then re-acquire main lock 这个地方为了更好的并发性释放了全局锁,下一步就通过乐观锁来扩容
        Object[] newArray = null;
        if (allocationSpinLock == 0 &&
            UNSAFE.compareAndSwapInt(this, allocationSpinLockOffset,
                                     0, 1)) { // cas乐观锁
            try {
                int newCap = oldCap + ((oldCap < 64) ?
                                       (oldCap + 2) : // grow faster if small
                                       (oldCap >> 1));
                if (newCap - MAX_ARRAY_SIZE > 0) {    // possible overflow
                    int minCap = oldCap + 1;// 扩容时至少得扩1个元素,如果连一个都没法扩了,就报OOM
                    if (minCap < 0 || minCap > MAX_ARRAY_SIZE)
                        throw new OutOfMemoryError();
                    newCap = MAX_ARRAY_SIZE;
                }
                if (newCap > oldCap && queue == array)
                    newArray = new Object[newCap];
            } finally {
                allocationSpinLock = 0;
            }
        }
        if (newArray == null) // back off if another thread is allocating
            Thread.yield();// CAS扩容失败,暂时放弃CPU执行权
        lock.lock();// 再把前面释放的全局锁"抢"回来
        if (newArray != null && queue == array) { // 扩容成功的话迁移元素
            queue = newArray;
            System.arraycopy(array, 0, newArray, 0, oldCap);
        }
    }

	// 出列
    public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        E result;
        try {
            while ( (result = dequeue()) == null)
                notEmpty.await(); // 阻塞
        } finally {
            lock.unlock();
        }
        return result;
    }

	// 堆顶出列,出去后还需要维护最小堆,在lock内执行的,所以线程安全
	private E dequeue() {
        int n = size - 1;
        if (n < 0)
            return null;
        else {
            Object[] array = queue;
            E result = (E) array[0];
            E x = (E) array[n];
            array[n] = null;
            Comparator<? super E> cmp = comparator;
            if (cmp == null)
                siftDownComparable(0, x, array, n);
            else
                siftDownUsingComparator(0, x, array, n, cmp);
            size = n;
            return result;
        }
    }

DelayQueue 延时队列

DelayQueue 实现的是 BlockingQueue接口,是阻塞的。 DelayQueue 使用了ReentrantLock处理同步问题,也是线程安全的。
DelayQueue 中的元素E extends Delayed。所以必须具备一个获取剩余延时时长的方法:long getDelay(TimeUnit unit);

使用场景:
DelayQueue使用场景

  • 关闭空闲连接。
  • 缓存过期自动清理。
  • 任务超时处理。

下面从两方面简要分析一下:1 数据结构,2 take方法

  • 1 数据结构: DelayQueue
    是通过一个全局变量PriorityQueue来充当它的数据结构的,所以它里面的元素具有优先性。按照getDelay的值作为排序依据。
  • 2 take方法
    public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            for (;;) {
                E first = q.peek(); // 堆顶元素,这个元素理应是最早到时的
                if (first == null)
                    available.await();// 堆顶无资源,阻塞,等待其他线程添加资源
                else {
                    long delay = first.getDelay(NANOSECONDS); // 堆顶有资源的话,获取该资源的剩余延迟时长,看是否是到时
                    if (delay <= 0)
                        return q.poll(); // 到时就弹出堆顶
                    first = null; // don't retain ref while waiting
					// 以下为堆顶元素还未到期的处理逻辑,leader为等待的线程。
                    if (leader != null)  // 等待线程不为空,说明之前有线程早就在等待资源到期
                        available.await(); // 有人在前面了,自己乖乖等着吧
                    else {
                        Thread thisThread = Thread.currentThread();
                        leader = thisThread; // 暂没有等待资源的线程,则让当前线程占据高地
                        try {
                            available.awaitNanos(delay); // 占据高地后 自己也自旋等待吧(不过在自旋的过程中它会自我唤醒)
                        } finally {
                            if (leader == thisThread) // 自旋完到时间后,要把高地让出来
                                leader = null;
                        }
                    }
                }
            }
        } finally {
            if (leader == null && q.peek() != null)
                available.signal();
            lock.unlock();
        }
    }

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值