ReentrantLock源码解读(2)——ReentrantLock源码与AQS

ReentrantLock源码系列
ReentrantLock源码解读(1)——CAS
ReentrantLock源码解读(2)——ReentrantLock源码与AQS
ReentrantLock源码解读(3)——Condition

AQS(AbstractQueuedSynchronizer),它是java中可重入锁和其他同步组件的基础框架。
先看一段ReentrantLock中的部分源码码

 /**
     * Creates an instance of {@code ReentrantLock}.
     * This is equivalent to using {@code ReentrantLock(false)}.
     */
    public ReentrantLock() {
        sync = new NonfairSync();
    }

    /**
     * Creates an instance of {@code ReentrantLock} with the
     * given fairness policy.
     *
     * @param fair {@code true} if this lock should use a fair ordering policy
     */
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

ReentrantLock的构造函数。核心成员sync,根据传入的boolean决定是公平还是非公平。

 static final class NonfairSync extends Sync {
...
 static final class FairSync extends Sync {
 ...
 abstract static class Sync extends AbstractQueuedSynchronizer {

不管是公平锁还是非公平锁,都是继承Sync,Sync继承AbstractQueuedSynchronizer,而关于加解锁的核心操作,都在AbstractQueuedSynchronizer中,所以需要先阅读AQS源码。

  • 初看AQS
    先看看AQS成员变量
 /**
     * Head of the wait queue, lazily initialized.  Except for
     * initialization, it is modified only via method setHead.  Note:
     * If head exists, its waitStatus is guaranteed not to be
     * CANCELLED.
     */
    private transient volatile Node head;

    /**
     * Tail of the wait queue, lazily initialized.  Modified only via
     * method enq to add new wait node.
     */
    private transient volatile Node tail;

    /**
     * The synchronization state.
     */
    private volatile int state;

	/**
     * The number of nanoseconds for which it is faster to spin
     * rather than to use timed park. A rough estimate suffices
     * to improve responsiveness with very short timeouts.
     */
    static final long spinForTimeoutThreshold = 1000L;

	 /**
     * Setup to support compareAndSet. We need to natively implement
     * this here: For the sake of permitting future enhancements, we
     * cannot explicitly subclass AtomicInteger, which would be
     * efficient and useful otherwise. So, as the lesser of evils, we
     * natively implement using hotspot intrinsics API. And while we
     * are at it, we do the same for other CASable fields (which could
     * otherwise be done with atomic field updaters).
     */
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long stateOffset;
    private static final long headOffset;
    private static final long tailOffset;
    private static final long waitStatusOffset;
    private static final long nextOffset;

    static {
        try {
            stateOffset = unsafe.objectFieldOffset
                (AbstractQueuedSynchronizer.class.getDeclaredField("state"));
            headOffset = unsafe.objectFieldOffset
                (AbstractQueuedSynchronizer.class.getDeclaredField("head"));
            tailOffset = unsafe.objectFieldOffset
                (AbstractQueuedSynchronizer.class.getDeclaredField("tail"));
            waitStatusOffset = unsafe.objectFieldOffset
                (Node.class.getDeclaredField("waitStatus"));
            nextOffset = unsafe.objectFieldOffset
                (Node.class.getDeclaredField("next"));

        } catch (Exception ex) { throw new Error(ex); }
    }

head:等待队列的头部,tail:等待队列的尾部,state同步状态,spinForTimeoutThreshold一个1ms的超时时间,还不知道具体做什么用。后面几个是CAS操作所需要的,上一篇讲过。
看到这里有个疑惑,这个等待队列只有头和尾,那它到底是个什么结构呢?看Node代码。

 /** Marker to indicate a node is waiting in shared mode */
        static final Node SHARED = new Node();
        /** Marker to indicate a node is waiting in exclusive mode */
        static final Node EXCLUSIVE = null;

两个标记,共享模式等待标记,和独占模式等待标记。分别对应这共享锁和排他锁

 /** waitStatus value to indicate thread has cancelled */
        static final int CANCELLED =  1;
        /** waitStatus value to indicate successor's thread needs unparking */
        static final int SIGNAL    = -1;
        /** waitStatus value to indicate thread is waiting on condition */
        static final int CONDITION = -2;
        /**
         * waitStatus value to indicate the next acquireShared should
         * unconditionally propagate
         */
        static final int PROPAGATE = -3;

		volatile int waitStatus;

枚举了4个等待状态,CANCELLED:线程取消等待;SIGNAL:需要唤醒下一个线程;CONDITION:有condition等待;PROPAGATE:传播,暂时不知道具体是什么。

volatile Node prev;

volatile Node next;

/**
* The thread that enqueued this node.  Initialized on
* construction and nulled out after use.
 */
volatile Thread thread;

prev指向上一个node,next指向下一个node,thread就是node所持有线程,也就是队列中排队的线程,也就是没有抢到锁而等待的线程。

/**
         * Link to next node waiting on condition, or the special
         * value SHARED.  Because condition queues are accessed only
         * when holding in exclusive mode, we just need a simple
         * linked queue to hold nodes while they are waiting on
         * conditions. They are then transferred to the queue to
         * re-acquire. And because conditions can only be exclusive,
         * we save a field by using special value to indicate shared
         * mode.
         */
        Node nextWaiter;

        /**
         * Returns true if node is waiting in shared mode.
         */
        final boolean isShared() {
            return nextWaiter == SHARED;
        }

这是一个共享锁判断,nextWaiter还与condition使用有关。

/**
         * Returns previous node, or throws NullPointerException if null.
         * Use when predecessor cannot be null.  The null check could
         * be elided, but is present to help the VM.
         *
         * @return the predecessor of this node
         */
        final Node predecessor() throws NullPointerException {
            Node p = prev;
            if (p == null)
                throw new NullPointerException();
            else
                return p;
        }

简单地获取上一个节点的的引用。
看完node源码可以大致知道,这个队列就是等待线程队列,它是一个双向队列,队列中持有的核心值就是thread,每个节点还记录thread等待状态以及共享锁、独占锁标志。
这时候想看看通过一个等待队列和一个状态位如何实现锁的,但AQS类方法太多,不知道如何下手。读源码,方法太多不知道如何下手的时候,两个办法:从public方法下手;从调用的地方下手。AQS类public方法也很多,这个时候采取后者。

  • ReentrantLock源码看AQS
    首先,ReentrantLock源码中的NonfairSync和FairSync策略模式,先看从非公平锁看起。
    lock()方法
        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

compareAndSetState是AQS类的方法。

protected final boolean compareAndSetState(int expect, int update) {
        // See below for intrinsics setup to support this
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }

这里就是一个CAS操作,期望值是0,要更新为1。偏移量为stateOffset,所以它是操作state的,state默认是0,加锁时期望更新为1,所以0,1分别对应已加锁,和未加锁的状态。
回到lock()方法,这个CAS操作成功,说明加锁成功。然后看,加锁成功的后的操作setExclusiveOwnerThread()

public abstract class AbstractOwnableSynchronizer
    implements java.io.Serializable {
	...
	/**
     * The current owner of exclusive mode synchronization.
     */
    private transient Thread exclusiveOwnerThread;

    /**
     * Sets the thread that currently owns exclusive access.
     * A {@code null} argument indicates that no thread owns access.
     * This method does not otherwise impose any synchronization or
     * {@code volatile} field accesses.
     * @param thread the owner thread
     */
    protected final void setExclusiveOwnerThread(Thread thread) {
        exclusiveOwnerThread = thread;
    }
    ...
}

这个方法来源于AbstractOwnableSynchronizer,而AQS也继承了这个类,这个类只有一个成员变量exclusiveOwnerThread,用来记录获取到独占锁的线程,所以setExclusiveOwnerThread(Thread.currentThread());是把当前线程set为获取独占锁的线程。
接下来看加锁失败后的操作

	/**
     * Acquires in exclusive mode, ignoring interrupts.  Implemented
     * by invoking at least once {@link #tryAcquire},
     * returning on success.  Otherwise the thread is queued, possibly
     * repeatedly blocking and unblocking, invoking {@link
     * #tryAcquire} until success.  This method can be used
     * to implement method {@link Lock#lock}.
     *
     * @param arg the acquire argument.  This value is conveyed to
     *        {@link #tryAcquire} but is otherwise uninterpreted and
     *        can represent anything you like.
     */
    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

这也是AQS里面的一个方法,翻译一下说明,以独占模式获取,忽略中断。传过来的参数是1,这个获取应该也是获取锁。继续看方法体里面调用的几个方法

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

初看一眼,可能会有点疑惑,它抛出一个不支持方法异常,说明这个方法要被它的子类重写,但它并没有设置成虚方法,说明它要求使用这个方法的子类必须重写这个方法,它这里只起一个申明作用。回到NonfairSync类看这个方法。

    static final class NonfairSync extends Sync {
   	...
        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }
// 分割线---
    abstract static class Sync extends AbstractQueuedSynchronizer {
            /**
         * Performs non-fair tryLock.  tryAcquire is implemented in
         * subclasses, but both need nonfair try for trylock method.
         */
        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;
        }
}

万万没想到,它的最终实现在Sync这个类里面。这个方法也是尝试获得锁,先看锁有没有被其他线程占用,没有就CAS尝试加锁。然后再看当前线程是否锁的持有线程,是的话,这个state再加个1,这一步就是锁的重入,多次加锁。所以这个state不仅有0、1,还有其他值,表示被加锁的次数,看到这里,可以知道,如果我们使用ReentrantLock多次加锁,必须释放锁同样次数。
再看acquireQueued(addWaiter(Node.EXCLUSIVE), arg),这里注意一点就是如果tryAcquire(arg)尝试获得锁成功,就不会往下走。

	/**
     * Creates and enqueues node for current thread and given mode.
     *
     * @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;
    }

    /**
     * Inserts node into queue, initializing if necessary. See picture above.
     * @param node the node to insert
     * @return node's predecessor
     */
    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;
                }
            }
        }
    }

	 /**
     * CAS head field. Used only by enq.
     */
    private final boolean compareAndSetHead(Node update) {
        return unsafe.compareAndSwapObject(this, headOffset, null, update);
    }

    /**
     * CAS tail field. Used only by enq.
     */
    private final boolean compareAndSetTail(Node expect, Node update) {
        return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
    }

这是addWaiter涉及到的所有操作,addWaiter中的操作其实只是一个快速操作,或者说预操作,因为开始pred为null,直接进enq(),其次compareAndSetTail可能失败。但是它也有意义就是最好情况下,直接将node加入到队列:pred指向tail,tail指向node,node.prev指向pred,pred.next指向node。
在enq()中操作其实一样,只不过多了初始化的操作。注意初始化操作是new Node()作为head。贴一张百度找来的队列的结构图。
这个方法最后return的是addWaiter方法里面创建的包含当前线程引用的node。
aqs队列
再看acquireQueued方法

/**
     * Acquires in exclusive uninterruptible mode for thread already in
     * queue. Used by condition wait methods as well as acquire.
     *
     * @param node the node
     * @param arg the acquire argument
     * @return {@code true} if interrupted while waiting
     */
    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);
        }
    }

看到这个for死循环,结合CAS操作,这里大概就是循环不断尝试获得锁,也就是锁自旋。在这个自旋里面有两个判断,第一个判断,node的上一个节点为head头节点,而且tryAcquire()尝试加锁成功。这个时候返回的interrupted(中断状态)为false。
第二个判断,接着看代码。

	/**
     * Checks and updates status for a node that failed to acquire.
     * Returns true if thread should block. This is the main signal
     * control in all acquire loops.  Requires that pred == node.prev.
     *
     * @param pred node's predecessor holding status
     * @param node the node
     * @return {@code true} if thread should block
     */
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            /*
             * This node has already set status asking a release
             * to signal it, so it can safely park.
             */
            return true;
        if (ws > 0) {
            /*
             * Predecessor was cancelled. Skip over predecessors and
             * indicate retry.
             */
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            /*
             * waitStatus must be 0 or PROPAGATE.  Indicate that we
             * need a signal, but don't park yet.  Caller will need to
             * retry to make sure it cannot acquire before parking.
             */
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

	/**
     * CAS waitStatus field of a node.
     */
    private static final boolean compareAndSetWaitStatus(Node node,
                                                         int expect,
                                                         int update) {
        return unsafe.compareAndSwapInt(node, waitStatusOffset,
                                        expect, update);
    }

shouldParkAfterFailedAcquire这个方法,主要操作pred的waitstatus,pred是前驱节点而不是当前节点。将它的值设置为SIGNAL,只有已经为SIGNAL了才返回true,其他情况返回false。并且如果pred.waitstatus > 0(CANCELLED),则跳过该前驱节点,并且一直循环往前跳,直到出现没有CANCELLED的节点。
再看parkAndCheckInterrupt()

	/**
     * Convenience method to park and then check if interrupted
     *
     * @return {@code true} if interrupted
     */
    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }

通过 LockSupport.park(this),传入this,隔离不同的ReentrantLock。LockSupport原理,内部也是通过unsafe类实现,这里不多说。Thread.interrupted()不仅检验当前线程是否有中断标志,并且会清除中断。
到此,加锁失败的线程已经阻塞并在等待队列了。什么时候会被唤醒,调用LockSupport.unpark(),还有一种方法,调用Thread类的interrupt()方法,产生一个中断。 LockSupport.park()是会响应这个中断的。
在acquireQueued()方法中,有个try finally代码块。调用cancelAcquire()来取消获取锁这个操作,看它如何实现。

/**
     * Cancels an ongoing attempt to acquire.
     *
     * @param node the node
     */
    private void cancelAcquire(Node node) {
        // Ignore if node doesn't exist
        if (node == null)
            return;

        node.thread = null;

        // Skip cancelled predecessors
        Node pred = node.prev;
        while (pred.waitStatus > 0)
            node.prev = pred = pred.prev;

        // predNext is the apparent node to unsplice. CASes below will
        // fail if not, in which case, we lost race vs another cancel
        // or signal, so no further action is necessary.
        Node predNext = pred.next;

        // Can use unconditional write instead of CAS here.
        // After this atomic step, other Nodes can skip past us.
        // Before, we are free of interference from other threads.
        node.waitStatus = Node.CANCELLED;

        // If we are the tail, remove ourselves.
        if (node == tail && compareAndSetTail(node, pred)) {
            compareAndSetNext(pred, predNext, null);
        } else {
            // If successor needs signal, try to set pred's next-link
            // so it will get one. Otherwise wake it up to propagate.
            int ws;
            if (pred != head &&
                ((ws = pred.waitStatus) == Node.SIGNAL ||
                 (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
                pred.thread != null) {
                Node next = node.next;
                if (next != null && next.waitStatus <= 0)
                    compareAndSetNext(pred, predNext, next);
            } else {
                unparkSuccessor(node);
            }

            node.next = node; // help GC
        }
    }

这个方法里先skip node前面CANCELLED状态的节点。这个方法中pred为node的前驱节点(实际上是非CANCELLED状态的前驱),predNext为前驱节点的后后继节点,next为node的后继节点,不能看出,后面的操作就是将这个节点移除。然后还调用unparkSuccessor()方法,唤醒后继节点。unparkSuccessor()方法,释放锁的时候再详细看。
在acquire方法中,selfInterrupt()还没看。

    /**
     * Convenience method to interrupt current thread.
     */
    static void selfInterrupt() {
        Thread.currentThread().interrupt();
    }

就是给当前线程加一个中断标志。
lock()总结
总结一下非公平锁加锁涉及到的几个方法。
ReentrantLock类:
tryAcquire->nonfairTryAcquire:尝试获得锁,有锁重入支持。

AbstractOwnableSynchronizer类:
setExclusiveOwnerThread:设置锁的持有线程。

AQS类:
acquire:获得锁
addWaiter:初始化node并入队列
acquireQueued:死循环尝试获得锁
shouldParkAfterFailedAcquire:检测获取锁失败后是否需要阻塞,实际没有阻塞操作
parkAndCheckInterrupt:阻塞当前线程,并返回线程是否中断
cancelAcquire:取消获取锁这个操作。
流程图
lock流程图
unLock()方法
ReentrantLock类的unLock()方法,调用的是AQS类的release()方法。

	/**
     * Releases in exclusive mode.  Implemented by unblocking one or
     * more threads if {@link #tryRelease} returns true.
     * This method can be used to implement method {@link Lock#unlock}.
     *
     * @param arg the release argument.  This value is conveyed to
     *        {@link #tryRelease} but is otherwise uninterpreted and
     *        can represent anything you like.
     * @return the value returned from {@link #tryRelease}
     */
    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尝试释放。释放锁成功后,if判断head,这个判断主要是看等待队列是不是还有等待线程,没有当然就不用唤醒其他线程了。最后调用unparkSuccessor()唤醒其他等待的线程

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

// 分割线 ReentrantLock.java
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;
        }

tryRelease跟tryAcquire一样,最终的实现在子类中。这里跟重入加锁的过程刚好相反,一个是state加1,一个是state-1。最后state为0就把owner线程清空同时返回true。

 /**
     * Wakes up node's successor, if one exists.
     *
     * @param node the node
     */
    private void unparkSuccessor(Node node) {
        /*
         * If status is negative (i.e., possibly needing signal) try
         * to clear in anticipation of signalling.  It is OK if this
         * fails or if status is changed by waiting thread.
         */
        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.
         */
        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);
    }

unparkSuccessor()这个方法释放后继节点,通过CAS set当前node的状态0,找到node的next节点s。如果s为空或者取消,则从尾部向前遍历,一直遍历到头部,并不是找到就退出,是一直遍历完,找到最后一个符合条件的。这里就有个疑惑,为什么不从head遍历?
找到这个node后,就unpark唤醒它持有的线程。然后就回到加锁过程中的自旋操作。
node节点怎么移除的?在自旋操作中有一步,设置当前节点为头节点,之前的头节点就被垃圾回收了。

看完非公平锁的加解锁,可能会有一个疑问,非公平体现在哪?释放锁时候,是按照队列来唤醒的,所以还是公平的。
其实,这个非公平体现在park阻塞之前,有几次获得锁的机会。一旦进入队列并park,那么它就是公平的了。总的来说,新的线程可以抢占锁,老的线程排队等待锁,这就是非公平锁。
unLock()总结
unlock()流程比较简单,释放锁,唤醒下一节点。
tryRelease:释放锁
unparkSuccessor:唤醒后继节点

FairSync公平锁
公平锁和非公平锁,在ReentrantLock类中用的是策略模式,它们的区别,也就是看这两个类中方法的区别。对比来看。

	static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;

        /**
         * Performs lock.  Try immediate barge, backing up to normal
         * acquire on failure.
         */
        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }
// 分割线---
	static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;

        final void lock() {
            acquire(1);
        }

        /**
         * Fair version of tryAcquire.  Don't grant access unless
         * recursive call or no waiters or is first.
         */
        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;
        }
    }

就两个方法lock()与tryAcquire()。
lock:相比于非公平锁,公平锁直接调用了AQS的acquire,而非公平锁,在这里给了新来的线程一次获得锁的机会。
tryAcquire:这个方法,是在AQS类的acquire()方法中调用的。在非公平锁中,直接尝试可重入地获取锁。
在公平锁中,这个尝试获取锁有给前提条件 !hasQueuedPredecessors()

public final boolean hasQueuedPredecessors() {
        // The correctness of this depends on head being initialized
        // before tail and on head.next being accurate if the current
        // thread is first in queue.
        Node t = tail; // Read fields in reverse initialization order
        Node h = head;
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }

这个方法判断队列中是否有排队的node,且这个node持有的thread不是当前线程。
所以在公平锁里面,新来的线程相比于队列中的老线程,没有一次可以获得锁的机会,保证了绝对的先来后到。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值