优先级队列

前言:

优先级队列,数据结构是堆,存储元素呢一般用数组存储。前面是逻辑上的数据结构,后面是物理上的数据结构。堆的数据结构是一个完全二叉树,如果二叉树每一层的节点都是满的称为满二叉树,除开最后一层其它层都是满称为完全二叉树。

堆中又分为小顶堆和大顶堆,小顶堆就是顶部元素是二叉树节点中最小的,大顶堆就是顶部元素是所有节点中最大的。

我们直到满二叉树是个等比数列,虽然堆的数据结构是完全二叉树,但是也是适用等比数列的性质,为了快速定位到某个节点的父节点,index/2就是父节点的索引,在用数组存储数据的时候会把索引为0的位置空着。

PriorityBlockingQueue

优先级队列,不再是FIFO或LIFO了,它内部有个比较器Comparator参数,可以自己定义比较规则来确定什么元素先出队。如果不传的话就是自然顺序。

它是基于数组实现,线程安全的,取元素是由顺序的,但是存储是无序的,也就是在数组里的顺序是不确定的,

举个例子看下优先级的效果:

PriorityBlockingQueue<Integer> integers = new PriorityBlockingQueue<>();
        integers.add(4);
        integers.add(1);
        integers.add(43);
        integers.add(14);
        integers.add(54);
        integers.add(74);
        integers.add(24);
        Iterator<Integer> iterator = integers.iterator();
        System.out.println("迭代的顺序");
        while (iterator.hasNext()){
            System.out.println(iterator.next());
        }
        Iterator<Integer> iterator2 = integers.iterator();
        System.out.println("出队的顺序");
        while (iterator2.hasNext()){
            iterator2.next();
            System.out.println(integers.poll());
        }


成员变量:底层是数组,平衡二叉树堆的实现

// 默认容量为11
private static final int DEFAULT_INITIAL_CAPACITY = 11;
// 最大数组大小
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
// 存储元素的数组
private transient Object[] queue;
// 元素个数
private transient int size;
// 排序比较器
private transient Comparator<? super E> comparator;
// 重入锁
private final ReentrantLock lock;
// 非空条件
private final Condition notEmpty;
// 扩容的时候使用的控制变量,CAS更新这个值,谁更新成功了谁扩容,其它线程让出CPU,自旋锁
private transient volatile int allocationSpinLock;
// 数组实现的最小堆,writeObject和readObject用到。 为了兼容之前的版本,只有在序列化和反序列化才非空
private PriorityQueue<E> q;

构造器:

public PriorityBlockingQueue() {
    this(DEFAULT_INITIAL_CAPACITY, null);// 默认容量为11
}
// 传入初始容量
public PriorityBlockingQueue(int initialCapacity) {
    this(initialCapacity, null);
}
// 传入初始容量和比较器,初始化各变量
public PriorityBlockingQueue(int initialCapacity, Comparator<? super E> comparator) {
    if (initialCapacity < 1)
        throw new IllegalArgumentException();
    this.lock = new ReentrantLock();
    this.notEmpty = lock.newCondition();
    this.comparator = comparator;
    this.queue = new Object[initialCapacity];
}
public PriorityBlockingQueue(Collection<? extends E> c) {
    this.lock = new ReentrantLock();
    this.notEmpty = lock.newCondition();
    boolean heapify = true; 
    boolean screen = true;  
    if (c instanceof SortedSet<?>) {//如果是SortedSet类型,无须进行堆有序化
        SortedSet<? extends E> ss = (SortedSet<? extends E>) c;
        this.comparator = (Comparator<? super E>) ss.comparator();
        heapify = false;//不需要重建堆
    }
    else if (c instanceof PriorityBlockingQueue<?>) {//如果是PriorityBlockingQueue类型,无须进行堆有序化
        PriorityBlockingQueue<? extends E> pq = (PriorityBlockingQueue<? extends E>) c;
        this.comparator = (Comparator<? super E>) pq.comparator();
        screen = false;
        if (pq.getClass() == PriorityBlockingQueue.class) //严格匹配,非子类
            heapify = false;//不需要重建堆
    }
    Object[] a = c.toArray();
    int n = a.length;
    if (c.getClass() != java.util.ArrayList.class)
        a = Arrays.copyOf(a, n, Object[].class);
    if (screen && (n == 1 || this.comparator != null)) {
        for (int i = 0; i < n; ++i)
            if (a[i] == null)//不接受null元素
                throw new NullPointerException();
    }
    this.queue = a;
    this.size = n;
    if (heapify)
        heapify();
}

入队:永远返回true,无界靠扩容

public boolean offer(E e) {
    if (e == null)//不支持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 {
        Comparator<? super E> cmp = comparator;
        
        if (cmp == null)//默认比较器为null,自下而上的堆化
            siftUpComparable(n, e, array);
        else  
            siftUpUsingComparator(n, e, array, cmp);
        //队列元素增加1,并且激活notEmpty的条件队列里面的一个阻塞线程
        size = n + 1;
        notEmpty.signal();//激活调用take()方法被阻塞的线程
    } finally {
        lock.unlock();
    }
    return true;
}

默认的插入规则中,新加入的元素可能会破坏小顶堆的性质,因此需要进行调整。

调整的过程为:从尾部下标的位置开始,将加入的元素逐层与当前点的父节点的内容进行比较并交换,直到满足父节点内容都小于子节点的内容为止。

默认的删除调整中,首先获取顶部下标和最尾部的元素内容,从顶部的位置开始,将尾部元素的内容逐层向下与当前点的左右子节点中较小的那个交换,直到判断元素内容小于或等于左右子节点中的任何一个为止

扩容:

private void tryGrow(Object[] array, int oldCap) {
    lock.unlock(); // 释放锁,防止阻塞的线程过多
    Object[] newArray = null;
    //CAS更新allocationSpinLock变量为1的线程获得扩容资格
    if (allocationSpinLock == 0 &&   
        UNSAFE.compareAndSwapInt(this, allocationSpinLockOffset,0, 1)) {
        try {//oldGap<64则扩容新增oldcap+2,否者扩容50%,并且最大为MAX_ARRAY_SIZE
            int newCap = oldCap + ((oldCap < 64) ?
                                   (oldCap + 2) : 
                                   (oldCap >> 1));
            if (newCap - MAX_ARRAY_SIZE > 0) {    //扩容后超过最大容量处理
                int minCap = oldCap + 1;
                if (minCap < 0 || minCap > MAX_ARRAY_SIZE)//整数溢出
                    throw new OutOfMemoryError();
                newCap = MAX_ARRAY_SIZE;
            }
            if (newCap > oldCap && queue == array)//queue是公共变量
                newArray = new Object[newCap];
        } finally {// 解锁,因为只有一个线程到此,因而不需要CAS操作;
            allocationSpinLock = 0;
        }
    }
    //扩容失败的线程newArray == null,调用Thread.yield()让出cpu, 让扩容成功线程优先调用lock.lock重新获取锁,
    //但是这得不到一定的保证,有可能调用Thread.yield()的线程先获取了锁(进入自旋)
    if (newArray == null) 
        Thread.yield();//让出cpu
    lock.lock();//有可能扩容成功的线程先走到这里,也有可能扩容失败的线程先走到这里。
    //准备赋值给共有变量queue,要加锁,
    //扩容成功的线程newArray != null ,扩容失败的线程newArray == null(再次进入while循环去扩容) 
    if (newArray != null && queue == array) {
        queue = newArray;// 队列赋值为新数组
        System.arraycopy(array, 0, newArray, 0, oldCap);// 并拷贝旧数组元素到新数组中
    }
}

tryGrow 目的是扩容,这里要思考下为啥在扩容前要先释放锁,然后使用 cas 控制只有一个线程可以扩容成功呢?

其实这里不先释放锁也是可以的,也就是在整个扩容期间一直持有锁,但是扩容是需要花时间的,如果扩容的时候还占用锁,那么其他线程在这个时候是不能进行出队和入队操作的,

这大大降低了并发性。所以为了提高性能,使用CAS控制只有一个线程可以进行扩容,并且在扩容前释放了锁,让其他线程可以进行入队和出队操作。

spinlock锁使用CAS控制只有一个线程可以进行扩容,CAS失败的线程会调用Thread.yield() 让出 cpu,目的是为了让扩容线程扩容后优先调用 lock.lock 重新获取锁,

但是这得不到一定的保证。有可能yield的线程在扩容线程扩容完成前已经退出,并执行了代码lock.lock()获取到了锁。如果当前数组扩容还没完毕,当前线程会再次调用tryGrow方法,

然后释放锁,这又给扩容线程获取锁提供了机会,如果这时候扩容线程还没扩容完毕,则当前线程释放锁后又调用yield方法让出CPU增加扩容的线程得到CPU。可知当扩容线程进行扩容期间,

其他线程是原地自旋通过while检查当前扩容是否完毕,等扩容完毕后才退出while的循环。

当扩容线程扩容完毕后会重置自旋锁变量allocationSpinLock 为 0,这里并没有使用UNSAFE方法的CAS进行设置是因为同时只可能有一个线程获取了该锁,并且 allocationSpinLock 被修饰为了 volatile。

当扩容线程扩容完毕后会执行代码lock.lock()获取锁,获取锁后复制当前 queue 里面的元素到新数组。

出队:

public E take() throws InterruptedException {
    //获取锁,可被中断
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    E result;
    try {
        //如果队列为空,则阻塞,把当前线程放入notEmpty的条件队列
        while ( (result = dequeue()) == null)
            notEmpty.await();//阻塞当前线程
    } finally {
        lock.unlock();//释放锁
    }
    return result;
}
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;
    }
}
private static <T> void siftDownComparable(int k, T x, Object[] array,
                                           int n) {
    if (n > 0) {
        Comparable<? super T> key = (Comparable<? super T>)x;
        int half = n >>> 1;           
        // 只需要遍历到叶子节点就够了
        while (k < half) {
            // 左子节点
            int child = (k << 1) + 1; 
            // 左子节点的值
            Object c = array[child];
            // 右子节点
            int right = child + 1;
            // 取左右子节点中最小的值
            if (right < n &&
                ((Comparable<? super T>) c).compareTo((T) array[right]) > 0)
                c = array[child = right];
            // key如果比左右子节点都小,则堆化结束
            if (key.compareTo((T) c) <= 0)
                break;
            // 否则,交换key与左右子节点中最小的节点的位置
            array[k] = c;
            k = child;
        }
        // 找到了放元素的位置,放置元素
        array[k] = key;
    }
}

PriorityBlockingQueue在入队的时候如果没有空间了是会自动扩容的,也就不存在队列满了的状态,也就是不需要等待通知队列不满了可以放元素了,所以也就不需要notFull条件了

DelayQueue

DelayQueue实现了BlockingQueue,所以它是一个阻塞队列。

另外,DelayQueue还组合了一个叫做Delayed的接口,DelayQueue中存储的所有元素必须实现Delayed接口。基于阻塞队列来实现的。

public class DelayQueue<E extends Delayed> extends AbstractQueue<E>
    implements BlockingQueue<E> {
    
    
public interface Delayed extends Comparable<Delayed> {
    long getDelay(TimeUnit unit);
}

成员变量:

// 并发的锁
private final transient ReentrantLock lock = new ReentrantLock();
// 优先级队列,存储元素
private final PriorityQueue<E> q = new PriorityQueue<E>();
// 用于标记当前是否有线程在排队(仅用于取元素时)
private Thread leader = null;
// 条件,用于表示现在是否有可取的元素
private final Condition available = lock.newCondition();
使用优先级队列实现,无界

构造器:

public DelayQueue() {}
public DelayQueue(Collection<? extends E> c) {
    this.addAll(c);
}

入队:

public boolean add(E e) {
    return offer(e);
}
public void put(E e) {
    offer(e);
}
//timeout unit都会被忽略(接口方法)
public boolean offer(E e, long timeout, TimeUnit unit) {
    return offer(e);
}
public boolean offer(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        q.offer(e);//入队
        if (q.peek() == e) {//如果添加的元素是堆顶元素,就把leader置为空,并唤醒等待在条件available上的线程;
            leader = null;
            available.signal();
        }
        return true;
    } finally {
        lock.unlock();
    }
}

出队:

public E poll() {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        E first = q.peek();// 取出堆顶元素
        //检查第一个元素,如果为空或者还没到期,就返回null;
        if (first == null || first.getDelay(NANOSECONDS) > 0)
            return null;
        else
            return q.poll();//弹出第一个元素
    } finally {
        lock.unlock();
    }
}
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)// 如果小于0说明已到期,直接调用poll()方法弹出堆顶元素
                    return q.poll();
                
                // 如果delay大于0 ,则下面要阻塞了
                // 将first置为空方便gc
                first = null; 
                // 如果前面有其它线程在等待,直接进入等待
                if (leader != null)
                    available.await();
                else {
                    // 如果leader为null,把当前线程赋值给它
                    Thread thisThread = Thread.currentThread();
                    leader = thisThread;
                    try {
                        // 等待delay时间后自动醒过来
                        // 醒过来后把leader置空并重新进入循环判断堆顶元素是否到期
                        // 这里即使醒过来后也不一定能获取到元素
                        // 因为有可能其它线程先一步获取了锁并弹出了堆顶元素
                        // 条件锁的唤醒分成两步,先从Condition的队列里出队
                        // 再入队到AQS的队列中,当其它线程调用LockSupport.unpark(t)的时候才会真正唤醒
                        available.awaitNanos(delay);
                    } finally {
                        // 如果leader还是当前线程就把它置为空,让其它线程有机会获取元素
                        if (leader == thisThread)
                            leader = null;
                    }
                }
            }
        }
    } finally {
        // 成功出队后,如果leader为空且堆顶还有元素,就唤醒下一个等待的线程
        if (leader == null && q.peek() != null)
            // signal()只是把等待的线程放到AQS的队列里面,并不是真正的唤醒
            available.signal();
        // 解锁,这才是真正的唤醒
        lock.unlock();
    }
}

(1)加锁;

(2)判断堆顶元素是否为空,为空的话直接阻塞等待;

(3)判断堆顶元素是否到期,到期了直接调用优先级队列的poll()弹出元素;

(4)没到期,再判断前面是否有其它线程在等待,有则直接等待;

(5)前面没有其它线程在等待,则把自己当作第一个线程等待delay时间后唤醒,再尝试获取元素;

(6)获取到元素之后再唤醒下一个等待的线程;

(7)解锁;

java中的线程池实现定时任务是直接用的DelayQueue吗?

不是,ScheduledThreadPoolExecutor中使用的是它自己定义的内部类DelayedWorkQueue,其实里面的实现逻辑基本都是一样的,只不过DelayedWorkQueue里面没有使用现成的PriorityQueue,而是使用数组又实现了一遍优先级队列,本质上没有什么区别。

PriorityQueue

里面没有锁,非线程安全的,入队不会阻塞。

集合中的每个元素都有一个权重值,每次出队都弹出优先级最大或最小的元素。

  • PriorityQueue是一个小顶堆;
  • PriorityQueue是非线程安全的;
  • PriorityQueue不是有序的,只有堆顶存储着最小的元素;
  • 入队就是堆的插入元素的实现;
  • 出队就是堆的删除元素的实现

成员变量:

// 默认容量
private static final int DEFAULT_INITIAL_CAPACITY = 11;
// 存储元素的数组
transient Object[] queue; 
// 元素个数
private int size = 0;
// 比较器   保证优先级,为空则是自然顺序
private final Comparator<? super E> comparator;
// 操作次数
transient int modCount = 0; 
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;//容量限制

构造器:
 

//默认容量11
public PriorityQueue() {
    this(DEFAULT_INITIAL_CAPACITY, null);
}
//指定容量    
public PriorityQueue(int initialCapacity) {
    this(initialCapacity, null);
}
//默认容量,指定比较器
public PriorityQueue(Comparator<? super E> comparator) {
    this(DEFAULT_INITIAL_CAPACITY, comparator);
}
//指定容量,指定比较器,判断传入的容量是否合法,初始化存储元素的数组及比较器
public PriorityQueue(int initialCapacity, Comparator<? super E> comparator) {
    if (initialCapacity < 1)
        throw new IllegalArgumentException();
    this.queue = new Object[initialCapacity];
    this.comparator = comparator;
}
//根据传入的集合构造队列
public PriorityQueue(Collection<? extends E> c) {
    if (c instanceof SortedSet<?>) {//SortedSet类型的集合
        SortedSet<? extends E> ss = (SortedSet<? extends E>) c;
        this.comparator = (Comparator<? super E>) ss.comparator();//取c的比较器
        initElementsFromCollection(ss);//元素入队
    }
    else if (c instanceof PriorityQueue<?>) {//同上
        PriorityQueue<? extends E> pq = (PriorityQueue<? extends E>) c;
        this.comparator = (Comparator<? super E>) pq.comparator();
        initFromPriorityQueue(pq);
    }
    else {
        this.comparator = null;//自然顺序
        initFromCollection(c);
    }
}
@SuppressWarnings("unchecked")
public PriorityQueue(PriorityQueue<? extends E> c) {
    this.comparator = (Comparator<? super E>) c.comparator();
    initFromPriorityQueue(c);
}
@SuppressWarnings("unchecked")
public PriorityQueue(SortedSet<? extends E> c) {
    this.comparator = (Comparator<? super E>) c.comparator();
    initElementsFromCollection(c);
}
private void initFromPriorityQueue(PriorityQueue<? extends E> c) {
    if (c.getClass() == PriorityQueue.class) {//说明是相同的类型,直接赋值
        this.queue = c.toArray();
        this.size = c.size();
    } else {
        initFromCollection(c);
    }
}
private void initElementsFromCollection(Collection<? extends E> c) {
    Object[] a = c.toArray();
    if (c.getClass() != ArrayList.class)//底层是数组,进行数组拷贝
        a = Arrays.copyOf(a, a.length, Object[].class);
    int len = a.length;
    if (len == 1 || this.comparator != null)
        for (int i = 0; i < len; i++)
            if (a[i] == null)
                throw new NullPointerException();//不支持null元素
    this.queue = a;
    this.size = a.length;
}
private void initFromCollection(Collection<? extends E> c) {
    initElementsFromCollection(c);
    heapify();//堆化
}

入队:

public boolean add(E e) {
    return offer(e);
}
public boolean offer(E e) {
    if (e == null)
        throw new NullPointerException();// 不支持null元素
    modCount++;//记录操作数
    int i = size;
    if (i >= queue.length)// 元素个数大于数组长度,需要扩容
        grow(i + 1);
    size = i + 1;// 元素个数加1
    // 如果还没有元素,直接插入到数组第一个位置
    if (i == 0)
        queue[0] = e;
    else
        // 否则,插入元素到数组的size位置,自下而上堆化
        siftUp(i, e);
    return true;
}
private void siftUp(int k, E x) {
    // 根据是否有比较器,使用不同的方法
    if (comparator != null)
        siftUpUsingComparator(k, x);
    else
        siftUpComparable(k, x);
}
@SuppressWarnings("unchecked")
private void siftUpComparable(int k, E x) {
    Comparable<? super E> key = (Comparable<? super E>) x;
    while (k > 0) {
        // 找到父节点的位置
        // 因为元素是从0开始的,所以减1之后再除以2
        int parent = (k - 1) >>> 1;
        // 父节点的值
        Object e = queue[parent];
        // 比较插入的元素与父节点的值
        // 如果比父节点大,则跳出循环
        // 否则交换位置
        if (key.compareTo((E) e) >= 0)
            break;
        // 与父节点交换位置
        queue[k] = e;
        // 现在插入的元素位置移到了父节点的位置
        // 继续与父节点再比较
        k = parent;
    }
    // 最后找到应该插入的位置,放入元素
    queue[k] = key;
}

出队:

public E remove() {
    E x = poll();// 调用poll弹出队首元素
    if (x != null)
        return x;// 有元素就返回弹出的元素
    else
        throw new NoSuchElementException();// 否则抛出异常
}
@SuppressWarnings("unchecked")
public E poll() {
    if (size == 0)// 如果size为0,说明没有元素
        return null;
    int s = --size;// 弹出元素,元素个数减1
    modCount++; 
    E result = (E) queue[0];// 队列首元素
    E x = (E) queue[s];// 队尾元素
    queue[s] = null; // 将队列末元素删除
    if (s != 0)// 如果弹出元素后还有元素
        // 将队列末元素移到队列首,再做自上而下的堆化
        siftDown(0, x);
    return result;// 返回弹出的元素
}
private void siftDown(int k, E x) {
    // 根据是否有比较器,选择不同的方法
    if (comparator != null)
        siftDownUsingComparator(k, x);
    else
        siftDownComparable(k, x);
}
@SuppressWarnings("unchecked")
private void siftDownComparable(int k, E x) {
    Comparable<? super E> key = (Comparable<? super E>)x;
    // 只需要比较一半就行了,因为叶子节点占了一半的元素
    int half = size >>> 1;        // loop while a non-leaf
    while (k < half) {
        // 寻找子节点的位置,这里加1是因为元素从0号位置开始
        int child = (k << 1) + 1; // assume left child is least
        // 左子节点的值
        Object c = queue[child];
        // 右子节点的位置
        int right = child + 1;
        if (right < size &&
            ((Comparable<? super E>) c).compareTo((E) queue[right]) > 0)
            // 左右节点取其小者
            c = queue[child = right];
        // 如果比子节点都小,则结束
        if (key.compareTo((E) c) <= 0)
            break;
        // 如果比最小的子节点大,则交换位置
        queue[k] = c;
        // 指针移到最小子节点的位置继续往下比较
        k = child;
    }
    // 找到正确的位置,放入元素
    queue[k] = key;
}

(1)将队列首元素弹出;

(2)将队列末元素移到队列首;

(3)自上而下堆化,一直往下与最小的子节点比较;

(4)如果比最小的子节点大,就交换位置,再继续与最小的子节点比较;

(5)如果比最小的子节点小,就不用交换位置了,堆化结束;

(6)这就是堆中的删除堆顶元素;

扩容:

private void grow(int minCapacity) {
    // 旧容量
    int oldCapacity = queue.length;
    
    // 旧容量小于64时,容量翻倍,旧容量大于等于64,容量只增加旧容量的一半
    int newCapacity = oldCapacity + ((oldCapacity < 64) ?
                                     (oldCapacity + 2) :
                                     (oldCapacity >> 1));
    // 检查是否溢出
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
        
    // 创建出一个新容量大小的新数组并把旧数组元素拷贝过去
    queue = Arrays.copyOf(queue, newCapacity);
}
private static int hugeCapacity(int minCapacity) {
    if (minCapacity < 0) // overflow
        throw new OutOfMemoryError();
    return (minCapacity > MAX_ARRAY_SIZE) ?
        Integer.MAX_VALUE :
        MAX_ARRAY_SIZE;
}

 

 

 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

姑苏冷

您的打赏是对原创文章最大的鼓励

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

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

打赏作者

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

抵扣说明:

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

余额充值