Java LinkedBlockingQueue类总结 从数据结构层面理解LinkedBlockingQueue

LinkedBlockingQueue类源码重点

LinkedBlockingQueue类的实现

LinkedBlockingQueue类的数据结构

Node节点

LinkedBlockingQueue是由Node节点构成的,Node是一个链表节点,所以LinkedBlockingQueue阻塞队列是由链表实现的

static class Node<E> {
    E item;
    Node<E> next;
    Node(E x) { item = x; }
}

LinkedBlockingQueue类

  1. capacity是LinkedBlockingQueue阻塞队列的容量,容量不等于有效元素个数
  2. count是AtomicInteger原子类,表示LinkedBlockingQueue中有效元素个数,也就是链表节点Node的个数
  3. head是阻塞队列头,也就是链表头,last是阻塞队列尾,也就是链表尾
  4. takeLock是可重入锁,作用是作为出队列锁,在获取队头元素时候上的锁,putLock也是可重入锁,作用是作为入队列锁,在往阻塞队列队尾插入元素时加上的锁,takeLock会锁住所有同时想操作队头的线程,只允许一个线程操作队头,putLock会锁住所有同时想操作队尾的线程,只允许一个线程操作队尾,ReentrantLock 源码及其实现可以看我这篇文章 ReentrantLock
  5. Condition是AQS(AbstractQueueSynchronizer)中的条件队列,在这里有两个,notEmpty,用于存放当LinkedBlockingQueue阻塞队列中没有元素时存放想获取元素的线程,而notFull用于存放当阻塞队列已满时想往其中添加元素的线程,条件队列中每个节点都是一个阻塞着的线程,当Lock替换了synchronized方法和语句时,Condition替换了对象监视器方法的使用,Condition 源码可以看我这篇文章 Condition
public class LinkedBlockingQueue<E> extends AbstractQueue<E>
        implements BlockingQueue<E>, java.io.Serializable {
	private final int capacity;
    private final AtomicInteger count = new AtomicInteger();
    transient Node<E> head;
    private transient Node<E> last;
    private final ReentrantLock takeLock = new ReentrantLock();
    private final Condition notEmpty = takeLock.newCondition();
    private final ReentrantLock putLock = new ReentrantLock();
    private final Condition notFull = putLock.newCondition();
}

入队列操作

offer操作

  1. 先判断想添加的元素是否为空,为空抛出NullPointerException异常
  2. 获取当前容器内有效元素个数count,判断容易是否已满,已满直接返回false添加失败
  3. 如果容器未满,先创建一个链表节点Node里面存放元素e,使用putLock入队列锁,执行putLock.lock方法对try catch块内的代码进行上锁,解锁方法putLock.unlock写在finally中
  4. try 中是执行添加元素的具体操作,再次判断当前元素个数是否小于容量,如果等于则不进行添加元素的操作,这是双检锁,防止第一次调用count.get时,有其他线程正在添加一个元素但还未成功,但第一次调用完count.get后成功了,所以在上锁代码里再检测一次
  5. 如果容量条件满足,调用enqueue往链表尾中插入链表节点,并调用count.getAndIncrement让count的值原子加1,并返回加1之前的有效元素个数c
  6. 判断c + 1是否仍然小于容量capacity,如果仍然小于,说明容器未满,则调用notFull.signal方法唤醒在notFull条件队列中线程,通知这些被唤醒的线程进行添加元素操作
  7. 最后如果c等于0,则唤醒notEmpty条件队列中线程,让这些线程获取阻塞队列中的值,为什么是c等于0呢,因为当notEmpty条件队列中有线程的话,阻塞队列的有效元素个数就是0,当调用count.getAndIncrement()返回得到的就是0,而如果调用了count.getAndIncrement说明已经有一个元素加入阻塞队列中,所以可以唤醒notEmpty条件队列中线程消费该新插入的元素
public boolean offer(E e) {
    if (e == null) throw new NullPointerException();
    final AtomicInteger count = this.count;
    if (count.get() == capacity)
        return false;
    int c = -1;
    // 创建链表节点
    Node<E> node = new Node<E>(e);
    final ReentrantLock putLock = this.putLock;
    putLock.lock();
    try {
        if (count.get() < capacity) {
            enqueue(node);
            c = count.getAndIncrement();
            if (c + 1 < capacity)
                notFull.signal();
        }
    } finally {
        putLock.unlock();
    }
    if (c == 0)
        signalNotEmpty();
    return c >= 0;
}
enqueue操作

往阻塞队列中插入元素的操作非常简单,只需要先把链表尾节点指向要插入的node节点,再将链表尾节点设为node节点即可

private void enqueue(Node<E> node) {
    // last.next = node
    // last = last.next
    last = last.next = node;
}
signalNotEmpty操作

先获取读锁,再唤醒notEmpty条件队列的线程,让线程读取阻塞队列中的元素(元素出队列),notFull.signal()和notEmpty.signal()底层都是AQS里的Condition条件队列操作

private void signalNotEmpty() {
    final ReentrantLock takeLock = this.takeLock;
    takeLock.lock();
    try {
        notEmpty.signal();
    } finally {
        takeLock.unlock();
    }
}

offer(E e, long timeout, TimeUnit unit)操作

  1. 可以看到大致和offer一样,只是offer(E e, long timeout, TimeUnit unit),引入了等待时间,如果容量已满则先等待调用notFull.awaitNanos(nanos),将该线程放入条件队列中等待nanos毫秒,如果超时直接返回false,如果容量未满则和普通offer一模一样
  2. 而且把上面的putLock.lock换成了可中断的锁putLock.lockInterruptibly
public boolean offer(E e, long timeout, TimeUnit unit)
    throws InterruptedException {
    if (e == null) throw new NullPointerException();
    long nanos = unit.toNanos(timeout);
    int c = -1;
    final ReentrantLock putLock = this.putLock;
    final AtomicInteger count = this.count;
    putLock.lockInterruptibly();
    try {
        while (count.get() == capacity) {
            if (nanos <= 0L)
                return false;
            nanos = notFull.awaitNanos(nanos);
        }
        enqueue(new Node<E>(e));
        c = count.getAndIncrement();
        if (c + 1 < capacity)
            notFull.signal();
    } finally {
        putLock.unlock();
    }
    if (c == 0)
        signalNotEmpty();
    return true;
}

put操作

  1. 可以看到和offer类似,只是offer在容器已满时直接会返回false,而put当容器已满时会调用notFull.await(),将当前想往阻塞队列添加元素的线程,放入条件队列中阻塞等待
  2. 条件队列中每个节点都是一个阻塞的线程
public void put(E e) throws InterruptedException {
    if (e == null) throw new NullPointerException();
    int c = -1;
    Node<E> node = new Node<E>(e);
    final ReentrantLock putLock = this.putLock;
    final AtomicInteger count = this.count;
    putLock.lockInterruptibly();
    try {
        while (count.get() == capacity) {
            notFull.await();
        }
        enqueue(node);
        c = count.getAndIncrement();
        if (c + 1 < capacity)
            notFull.signal();
    } finally {
        putLock.unlock();
    }
    if (c == 0)
        signalNotEmpty();
}

出队列操作

poll操作

  1. 先判断阻塞队列中有效元素个数是否等于0,如果等于直接返回null,大于0则进行出队列操作
  2. 使用takeLock出队列锁锁住所有同时想操作阻塞队列队头的元素
  3. 如果count.get大于0,说明阻塞队列中有元素,则调用dequeue将元素出队列
  4. 如果count.getAndDecrement大于1,说明仍然还有元素没有取出来,使用notEmpty.signal唤醒在notEmpty条件队列的线程去消费元素
  5. 如果c等于capacity说明此时阻塞队列中已经有一个元素出队列了,不再是满的状态了,因为c是count.getAndDecrement获取的,getAndDecrement是先获取再减1,说明此时元素个数已经是c-1,说明阻塞队列未满,可以使用signalNotFull唤醒notFull队列中的线程去往阻塞队列中添加元素
  6. 和offer一样,仍然利用了双检锁,调用了两次count.get()
public E poll() {
    final AtomicInteger count = this.count;
    if (count.get() == 0)
        return null;
    E x = null;
    int c = -1;
    final ReentrantLock takeLock = this.takeLock;
    takeLock.lock();
    try {
        if (count.get() > 0) {
            x = dequeue();
            c = count.getAndDecrement();
            if (c > 1)
                notEmpty.signal();
        }
    } finally {
        takeLock.unlock();
    }
    if (c == capacity)
        signalNotFull();
    return x;
}
dequeue操作
  1. 获取阻塞队列队头的元素,和队头节点的下一个节点,由于阻塞队列队头不存储元素
  2. 将队头的next指向其自己,帮助队头被GC回收,将队头的下一个节点设为队头
  3. 获取原队头节点的下一个节点中的元素,该元素才是要返回的元素,将新队头的元素置为空,因为队头不存储元素,返回刚刚获取到的元素
private E dequeue() {
	// 获取阻塞队列队头的元素,和队头节点的下一个节点
    Node<E> h = head;
    Node<E> first = h.next;
    // 将队头的next指向其自己
    h.next = h;
    // 将队头的下一个节点设为队头
    head = first;
    // 获取原队头节点的下一个节点中的元素
    E x = first.item;
    //将新队头的元素置为空
    first.item = null;
    return x;
}
signalNotFull操作

先调用入队列锁执行putLock.lock上锁,然后调用notFull.signal唤醒notFull条件队列的线程去往阻塞队列中添加元素

private void signalNotFull() {
    final ReentrantLock putLock = this.putLock;
    putLock.lock();
    try {
        notFull.signal();
    } finally {
        putLock.unlock();
    }
}

poll(long timeout, TimeUnit unit)操作

和poll类似,和offer(long timeout, TimeUnit unit)操作的超时原理类型,获取阻塞队列中元素时如果发现阻塞队列中没有元素,那么调用notEmpty.awaitNanos阻塞等待nanos纳秒,如果直到超时了阻塞队列仍然没有元素就直接返回null

public E poll(long timeout, TimeUnit unit) throws InterruptedException {
    E x = null;
    int c = -1;
    long nanos = unit.toNanos(timeout);
    final AtomicInteger count = this.count;
    final ReentrantLock takeLock = this.takeLock;
    takeLock.lockInterruptibly();
    try {
        while (count.get() == 0) {
            if (nanos <= 0L)
                return null;
            nanos = notEmpty.awaitNanos(nanos);
        }
        x = dequeue();
        c = count.getAndDecrement();
        if (c > 1)
            notEmpty.signal();
    } finally {
        takeLock.unlock();
    }
    if (c == capacity)
        signalNotFull();
    return x;
}

take操作

和put操作类似,如果阻塞队列中没有元素,则直接调用notEmpty.await()将当前线程作为节点放入notEmpty阻塞等待,等待入队列操作的signalNotEmpty()方法的唤醒

public E take() throws InterruptedException {
   E x;
    int c = -1;
    final AtomicInteger count = this.count;
    final ReentrantLock takeLock = this.takeLock;
    takeLock.lockInterruptibly();
    try {
        while (count.get() == 0) {
            notEmpty.await();
        }
        x = dequeue();
        c = count.getAndDecrement();
        if (c > 1)
            notEmpty.signal();
    } finally {
        takeLock.unlock();
    }
    if (c == capacity)
        signalNotFull();
    return x;
}

remove操作

  1. 如果要删除的元素为空返回false,否则调用fullyLock同时锁上入队列锁和出队列锁,也就是同时锁上阻塞队列的队头和队尾,禁止其他线程对队头队尾进行操作
  2. 从阻塞队列也就是链表的第二个节点开始查找对应元素,找到则调用unlink删除阻塞队列中含有给定对象的节点并返回true,如果从第二个节点找到最后都没找到则返回false
  3. 为什么要有trail和p两个节点呢,因为LinkedBlockingQueue阻塞队列是用单向链表实现的,如果要删除节点必须获取要删除的节点前一节点和要删除的节点才行
  4. finally块中调用fullyUnlock解除入队列锁和出队列锁
public boolean remove(Object o) {
    if (o == null) return false;
    fullyLock();
    try {
        for (Node<E> trail = head, p = trail.next;
             p != null;
             trail = p, p = p.next) {
            if (o.equals(p.item)) {
                unlink(p, trail);
                return true;
            }
        }
        return false;
    } finally {
        fullyUnlock();
    }
}
fullyLock操作

调用 putLock.lock()和 takeLock.lock()把入队列锁和出队列锁一起锁上

void fullyLock() {
    putLock.lock();
    takeLock.lock();
}
unlink操作
  1. 先将要删除的节点内部元素置空
  2. 将要删除的节点的前一节点指向要删除的节点的后一节点
  3. 如果要删除的节点是链表尾节点,则将要删除的节点的前一节点设为尾节点
  4. 如果count.getAndDecrement()等于capacity,说明删除该节点之前阻塞队列已满,而删除后空出一个位置,调用 notFull.signal() 唤醒notFull条件队列上阻塞的线程,去往阻塞队列中添加元素
void unlink(Node<E> p, Node<E> trail) {
    p.item = null;
    trail.next = p.next;
    if (last == p)
        last = trail;
    if (count.getAndDecrement() == capacity)
        notFull.signal();
}
fullyLock操作

调用 putLock.unlock()和 takeLock.unlock()把入队列锁和出队列锁一起解锁

void fullyUnlock() {
    takeLock.unlock();
    putLock.unlock();
}

查询操作

peek操作

  1. 如果count.get为0,即当前阻塞队列中无元素,则返回null
  2. 否则先使用takeLock加上出队列锁,防止多个线程同时操作阻塞队列队头,然后使用双检锁,再次检索阻塞队列中是否还有元素
  3. 返回阻塞队列队头的下一个节点上面的元素,因为队头节点不存储元素
  4. 在finally块中解除出队列锁
public E peek() {
    if (count.get() == 0)
        return null;
    final ReentrantLock takeLock = this.takeLock;
    takeLock.lock();
    try {
        return (count.get() > 0) ? head.next.item : null;
    } finally {
        takeLock.unlock();
    }
}

contains操作

  1. 如果要查找的元素为空返回false,否则调用fullyLock同时锁上入队列锁和出队列锁,也就是同时锁上阻塞队列的队头和队尾,禁止其他线程对队头队尾进行操作
  2. 从阻塞队列也就是链表的第二个节点开始查找对应元素,找到返回true,如果从第二个节点找到最后都没找到则返回false
  3. finally块中调用fullyUnlock解除入队列锁和出队列锁
public boolean contains(Object o) {
    if (o == null) return false;
    fullyLock();
    try {
        for (Node<E> p = head.next; p != null; p = p.next)
            if (o.equals(p.item))
                return true;
        return false;
    } finally {
        fullyUnlock();
    }
}

构造函数

LinkedBlockingQueue()

直接调用LinkedBlockingQueue(int capacity)构造方法,分配Integer.MAX_VALUE的容量,由于是链表实现的阻塞队列,所以无论初始容量多大,都不用做额外操作

public LinkedBlockingQueue() {
    this(Integer.MAX_VALUE);
}

LinkedBlockingQueue(int capacity)

如果容量值不合法抛出异常,创建一个Node节点,其内部元素为null,将队头和队尾都设为该Node节点

public LinkedBlockingQueue(int capacity) {
   if (capacity <= 0) throw new IllegalArgumentException();
   this.capacity = capacity;
   last = head = new Node<E>(null);
}

LinkedBlockingQueue(Collection<? extends E> c)

  1. 先上入队列锁,然后调用enqueue将Collection中所有元素全部添加进队列中
  2. 每插入一个元素有效元素计数n就加一,最后调用count.set(n)原子性将count设为n
  3. 不允许集合c中元素为空,如果在插入过程中超出阻塞队列容量,则抛出IllegalStateException异常
  4. 上锁后如果仍然有其他线程尝试调用putLock.lock,会进入AQS队列中阻塞并排队
public LinkedBlockingQueue(Collection<? extends E> c) {
    this(Integer.MAX_VALUE);
    final ReentrantLock putLock = this.putLock;
    putLock.lock(); 
    try {
        int n = 0;
        for (E e : c) {
            if (e == null)
                throw new NullPointerException();
            if (n == capacity)
                throw new IllegalStateException("Queue full");
            enqueue(new Node<E>(e));
            ++n;
        }
        count.set(n);
    } finally {
        putLock.unlock();
    }
}

Iterator实现

Iterator是一个迭代器,是一个接口,用来迭代元素,里面定义的方法可以看我这篇文章Iterator

Itr类

  1. current是迭代器当前的Node节点
  2. lastRet用于在next获取元素时,存储current的值,然后current会变成下一个节点
  3. currentElement迭代器当前的Node节点中的元素
private class Itr implements Iterator<E> {
	private Node<E> current;
    private Node<E> lastRet;
    private E currentElement;
}

Itr操作

迭代器当前的Node节点current一开始就是阻塞队列的链表的第二个节点

Itr构造函数
Itr() {
    fullyLock();
    try {
        current = head.next;
        if (current != null)
            currentElement = current.item;
    } finally {
        fullyUnlock();
    }
}
hasNext方法

如果当前节点不为空,则迭代器还能进行迭代

public boolean hasNext() {
   return current != null;
}
next方法
  1. 获取当前元素 currentElement存为x,将lastRet设为当前的节点current
  2. 将current设为下一个节点,该下一个节点内部元素item不为空
  3. 将currentElement设为current内部元素或null
  4. 返回一开始的元素x
public E next() {
    fullyLock();
    try {
        if (current == null)
            throw new NoSuchElementException();
        E x = currentElement;
        lastRet = current;
        current = nextNode(current);
        currentElement = (current == null) ? null : current.item;
        return x;
    } finally {
        fullyUnlock();
    }
}
nextNode方法
  1. 获取p的下一个节点s,如果s和p相等,则说明该阻塞队列只有一个队头,返回队头
  2. 如果s为null或s的元素不为null,返回s,也就是如果s不为空但其内部元素为空,则会跳过该节点,去找下一个节点,直到找到s为null或s的元素不为null的节点并返回
private Node<E> nextNode(Node<E> p) {
    for (;;) {
        Node<E> s = p.next;
        if (s == p)
            return head.next;
        if (s == null || s.item != null)
            return s;
        p = s;
    }
}
remove方法
  1. 获取上一个元素lastRet存为node,将lastRet设为null
  2. 从阻塞队列头部开始往后找,直到找到node节点,但也保持其前一个结点trail,调用unlink从链表中删除该结点
  3. 可以看出remove删除的是调用next()获取到的结点
public void remove() {
    if (lastRet == null)
        throw new IllegalStateException();
    fullyLock();
    try {
        Node<E> node = lastRet;
        lastRet = null;
        for (Node<E> trail = head, p = trail.next;
             p != null;
             trail = p, p = p.next) {
            if (p == node) {
                unlink(p, trail);
                break;
            }
        }
    } finally {
        fullyUnlock();
    }
}

Spliterator实现

Spliterator是一个迭代器,是一个接口,用来并行迭代元素,里面定义的方法可以看我这篇文章Spliterator

LBQSpliterator类

  1. MAX_BATCH 是最大批处理元素个数
  2. queue 是当前的阻塞队列对象
  3. current 是当前节点,初始化前为空
  4. batch 是一个Spliterator拆分后,另一个Spliterator中的元素个数
  5. exhausted是如果没有更多节点,则为true
  6. est是estimate估算的简写即Spliterator中的元素个数估算
static final class LBQSpliterator<E> implements Spliterator<E> {
    static final int MAX_BATCH = 1 << 25;
    final LinkedBlockingQueue<E> queue;
    Node<E> current; 
    int batch;         
    boolean exhausted; 
    long est;   
}

LBQSpliterator操作

LBQSpliterator构造函数

初始化阻塞队列queue,初始化Spliterator中的元素个数估算est

LBQSpliterator(LinkedBlockingQueue<E> queue) {
    this.queue = queue;
    this.est = queue.size();
}
estimateSize方法

获取Spliterator中的元素个数估算est

public long estimateSize() { 
	return est; 
}
tryAdvance方法
  1. 获取阻塞队列,如果exhausted为true,说明阻塞队列中没有元素了,则直接返回false
  2. 如果exhausted为false,说明阻塞队列中还有元素,如果current当前节点为空,则设为阻塞队列第二个节点
  3. whlie循环中是获取current节点里面的元素,如果元素item为空,则跳到下一个节点,不为空则跳出循环
  4. 拿到元素e后,调用fullyUnlock释放入队列锁和出队列锁,如果current已经为null,则将 exhausted设为空,表示阻塞队列中元素已经耗尽
  5. 如果元素e不为空,则调用Consumer里面的accept方法处理e,Consumer是一个函数式接口,里面只有一个accept方法,接收一个参数没有返回值,Consumer可以看我这篇文章 Consumer
public boolean tryAdvance(Consumer<? super E> action) {
    if (action == null) throw new NullPointerException();
    final LinkedBlockingQueue<E> q = this.queue;
    if (!exhausted) {
        E e = null;
        q.fullyLock();
        try {
            if (current == null)
                current = q.head.next;
            while (current != null) {
                e = current.item;
                current = current.next;
                if (e != null)
                    break;
            }
        } finally {
            q.fullyUnlock();
        }
        if (current == null)
            exhausted = true;
        if (e != null) {
            action.accept(e);
            return true;
        }
    }
    return false;
}
forEachRemaining方法

forEachRemaining相当于循环调用tryAdvance,直到把阻塞队列中所有元素都用Consumer的action处理完

public void forEachRemaining(Consumer<? super E> action) {
    if (action == null) throw new NullPointerException();
    final LinkedBlockingQueue<E> q = this.queue;
    if (!exhausted) {
        exhausted = true;
        Node<E> p = current;
        do {
            E e = null;
            q.fullyLock();
            try {
                if (p == null)
                    p = q.head.next;
                while (p != null) {
                    e = p.item;
                    p = p.next;
                    if (e != null)
                        break;
                }
            } finally {
                q.fullyUnlock();
            }
            if (e != null)
                action.accept(e);
        } while (p != null);
    }
}
characteristics方法

characteristics方法是获取Spliterator中的元素特性,如ORDERED表示是按插入顺序排序的,类似List,NONNULL是不允许非空元素,CONCURRENT是不用外部加同步也能线程安全访问、

public int characteristics() {
   return Spliterator.ORDERED | Spliterator.NONNULL |
        Spliterator.CONCURRENT;
}
trySplit方法
  1. 这个方法的作用是将一个Spliterator分为两个Spliterator,分离后两个Spliterator的元素都只是原来的一部分,加起来才是原来的总和
  2. batch是批处理分离的量,先判断是否大于MAX_BATCH最大允许批处理元素个数,计算出要进行的批处理次数n
  3. 如果exhausted不为true,即阻塞队列中有元素,且current不为空,阻塞队列头节点的下一个节点也不为空,头节点的下下个也不为空则进行分离操作,即阻塞队列至少要有3个节点才可以进行分离操作
  4. 根据n创建一个数组,遍历阻塞队列,将其中元素不为空的节点放入数组,直至放入了n个元素,才释放两个锁
  5. 把刚刚do循环中一直往后移动的节点引用p赋给current,即把当前的Spliterator的current往后移动,如果p为null,则est估算量直接设为0, exhausted设为true,表示阻塞队列已经耗尽
  6. 最后用Spliterators.spliterator方法创建一个Spliterator,将数组a,以及其中有效元素的索引传入进去,以及Spliterator的特征如Spliterator.ORDERED这些也传入进去创建一个分离出去的Spliterator
public Spliterator<E> trySplit() {
    Node<E> h;
    final LinkedBlockingQueue<E> q = this.queue;
    int b = batch;
    int n = (b <= 0) ? 1 : (b >= MAX_BATCH) ? MAX_BATCH : b + 1;
    if (!exhausted &&
        ((h = current) != null || (h = q.head.next) != null) &&
        h.next != null) {
        Object[] a = new Object[n];
        int i = 0;
        Node<E> p = current;
        q.fullyLock();
        try {
            if (p != null || (p = q.head.next) != null) {
                do {
                    if ((a[i] = p.item) != null)
                        ++i;
                } while ((p = p.next) != null && i < n);
            }
        } finally {
            q.fullyUnlock();
        }
        if ((current = p) == null) {
            est = 0L;
            exhausted = true;
        }
        else if ((est -= i) < 0L)
            est = 0L;
        if (i > 0) {
            batch = i;
            return Spliterators.spliterator
                (a, 0, i, Spliterator.ORDERED | Spliterator.NONNULL |
                 Spliterator.CONCURRENT);
        }
    }
    return null;
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

lolxxs

你的鼓励将是我创作的最大动力

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

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

打赏作者

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

抵扣说明:

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

余额充值