JUC原理

JUC概述

JUC就是java.util.concurrent⼯具包的简称。这是⼀个处理线程并发的⼯具包,JDK 1.5开始出现的。
在这里插入图片描述

AQS

AQS(AbstractQueuedSynchronizer) “抽象队列同步器”。它定义了⼀套多线程访问共享资源的同步器框架。
在这里插入图片描述

它维护了⼀个volatile int state(代表共享资源)和⼀个FIFO线程等待队列(多线程争⽤资源被阻塞时会进⼊此队列)。

AQS定义两种资源共享⽅式:Exclusive(独占,只有⼀个线程能执⾏,如ReentrantLock)和Share(共享,多个线程可同时执⾏,如Semaphore/CountDownLatch)。

不同的⾃定义同步器争⽤共享资源的⽅式也不同。⾃定义同步器在实现时只需要实现共享资源state的获取与释放⽅式即可,⾄于具体线程等待队列的维护(如获取资源失败⼊队/唤醒出队等),AQS已经在顶层实现好了。⾃定义同步器实现时主要实现以下⼏种⽅法:

  • isHeldExclusively():该线程是否正在独占资源。只有⽤到condition才需要去实现它。
  • tryAcquire(int):独占⽅式。尝试获取资源,成功则返回true,失败则返回false。
  • tryRelease(int):独占⽅式。尝试释放资源,成功则返回true,失败则返回false。
  • tryAcquireShared(int):共享⽅式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可⽤资源;正数表示成功,且有剩余资源。
  • tryReleaseShared(int):共享⽅式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。

以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()时,会调⽤tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为⽌,其它线程才有机会获取该锁。当然,释放锁之前,A线程⾃⼰是可以重复获取此锁的(state会累加),这就是可重⼊的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的。

Unsafe

通过直接操作内存的⽅式来保证并发处理的安全性,使⽤的是硬件的安全机制sun.misc.Unsafe,这个类包含了⼤量的对C代码的操作,包括很多直接内存分配以及原⼦操作的调⽤,⽽它之所以标记为⾮安全的,是告诉你这个⾥⾯⼤量的⽅法调⽤都会存在安全隐患,需要⼩⼼使⽤,否则会导致严重的后果,例如在通过unsafe分配内存的时候,如果⾃⼰指定某些区域可能会导致⼀些类似C++⼀样的指针越界到其他进程的问题,不推荐直接使⽤unsafe来操作原⼦变量,⽽是通过java封装好的⼀些类来操作原⼦变量。

Unsafe类提供了以下这些功能:

  1. 内存管理。包括分配内存、释放内存等。
  2. ⾮常规的对象实例化 allocateInstance 。
  3. 操作类、对象、变量。
    这部分包括了staticFieldOffset(静态域偏移)、defineClass(定义类)、defineAnonymousClass(定义匿名类)、ensureClassInitialized(确保类初始化)、objectFieldOffset(对象域偏移)等⽅法。
  4. 数组操作。
    这部分包括了arrayBaseOffset(获取数组第⼀个元素的偏移地址)、arrayIndexScale(获取数组中元素的增量地址)等⽅法。
  5. 多线程同步。包括锁机制、CAS操作等。
    这部分包括了monitorEnter、tryMonitorEnter、monitorExit、compareAndSwapInt、compareAndSwap等⽅法。
  6. 挂起与恢复。
    这部分包括了park、unpark等⽅法。 LockSupport调⽤了Unsafe.park()⽅法。
  7. 内存屏障。
    这部分包括了loadFence、storeFence、fullFence等⽅法。

atomic

在java6以后我们不但接触到了Lock相关的锁,也接触到了很多更加乐观的原⼦修改操作,也就是在修改时我们只需要保证它的那个瞬间是安全的即可,经过相应的包装后可以再处理对象的并发修改,以及并发中的ABA问题。

  • 基本类型:AtomicInteger、AtomicLong、AtomicBoolean;
  • 引⽤类型:AtomicReference、AtomicReference的ABA实例(AtomicStampedRerence、AtomicMarkableReference)
  • 数组类型:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray属性原⼦修改器(Updater):AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater。
import java.util.concurrent.atomic.AtomicInteger;

public class AtomicIntegerTest {

    /**
     * 常见的方法列表
     * @see AtomicInteger#get()             直接返回值
     * @see AtomicInteger#getAndAdd(int)    增加指定的数据,返回变化前的数据
     * @see AtomicInteger#getAndDecrement() 减少1,返回减少前的数据
     * @see AtomicInteger#getAndIncrement() 增加1,返回增加前的数据
     * @see AtomicInteger#getAndSet(int)    设置指定的数据,返回设置前的数据
     *
     * @see AtomicInteger#addAndGet(int)    增加指定的数据后返回增加后的数据
     * @see AtomicInteger#decrementAndGet() 减少1,返回减少后的值
     * @see AtomicInteger#incrementAndGet() 增加1,返回增加后的值
     * @see AtomicInteger#lazySet(int)      仅仅当get时才会set
     *
     * @see AtomicInteger#compareAndSet(int, int) 尝试新增后对比,若增加成功则返回true否则返回false
     */
    public final static AtomicInteger TEST_INTEGER = new AtomicInteger(0);

    public static void main(String []args) throws InterruptedException {
        final Thread []threads = new Thread[10];
        for(int i = 0 ; i < 10 ; i++) {
            final int num = i;
            threads[i] = new Thread(() -> {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                int now = TEST_INTEGER.incrementAndGet();
                System.out.println("我是线程:" + num + ",我得到值了,增加后的值为:" + now);
            });
            threads[i].start();
        }
        for(Thread t : threads) {
            t.join();
        }
        System.out.println("最终运行结果:" + TEST_INTEGER.get());
    }
}

基本类型

AtomicIntegerFieldUpdater

Updater也就是修改器,它算是Atomic的系列的⼀个扩展,Atomic系列是为你定义好的⼀些对象,你可以使⽤,但是如果是别⼈已经在使⽤的对象会原先的代码需要修改为Atomic系列,此时若全部修改类型到对应的对象相信很麻烦,因为牵涉的代码会很多,此时java提供⼀个外部的Updater可以对对象的属性本身的修改提供类似Atomic的操作,也就是它对这些普通的属性的操作是并发下安全的,分别由:AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceUpdater,这样操作后,系统会更加灵活,也就是可能那些类的属性只是在某些情况下需要控制并发,很多时候不需要,但是他们的使⽤通常有以下⼏个限制:

  • 限制1:操作的⽬标不能是static类型。
  • 限制2:操作的⽬标不能是final类型的,因为final根本没法修改。
  • 限制3:必须是volatile类型的数据,也就是数据本身是读⼀致的。
  • 限制4:属性必须对当前的Updater所在的区域是可⻅的,也就是private如果不是当前类肯定是不可⻅的,protected如果不存在⽗⼦关系也是不可⻅的,default如果不是在同⼀个package下也是不可⻅的。

实现⽅式:通过反射找到属性,对属性进⾏操作,但是并不是设置accessable,所以必须是可⻅的属性才能操作。

import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;

public class AtomicIntegerFieldUpdaterTest {

    static class A {
        volatile int intValue = 100;
    }

    /**
     * 可以直接访问对应的变量,进行修改和处理
     * 条件:要在可访问的区域内,如果是private或挎包访问default类型以及非父亲类的protected均无法访问到
     * 其次访问对象不能是static类型的变量(因为在计算属性的偏移量的时候无法计算),也不能是final类型的变量(因为根本无法修改),必须是普通的成员变量
     *
     * 方法(说明上和AtomicInteger几乎一致,唯一的区别是第一个参数需要传入对象的引用)
     * @see AtomicIntegerFieldUpdater#addAndGet(Object, int)
     * @see AtomicIntegerFieldUpdater#compareAndSet(Object, int, int)
     * @see AtomicIntegerFieldUpdater#decrementAndGet(Object)
     * @see AtomicIntegerFieldUpdater#incrementAndGet(Object)
     *
     * @see AtomicIntegerFieldUpdater#getAndAdd(Object, int)
     * @see AtomicIntegerFieldUpdater#getAndDecrement(Object)
     * @see AtomicIntegerFieldUpdater#getAndIncrement(Object)
     * @see AtomicIntegerFieldUpdater#getAndSet(Object, int)
     */
    public final static AtomicIntegerFieldUpdater<A> ATOMIC_INTEGER_UPDATER = AtomicIntegerFieldUpdater.newUpdater(A.class, "intValue");

    public static void main(String []args) {
        final A a = new A();
        for(int i = 0 ; i < 100 ; i++) {
            final int num = i;
            new Thread(() -> {
                if(ATOMIC_INTEGER_UPDATER.compareAndSet(a, 100, 120)) {
                    System.out.println("我是线程:" + num + " 我对对应的值做了修改!");
                    System.out.println(a.intValue);
                }
            }).start();
        }
    }
}
AtomicLong 和 LongAdder

AtomicLong的原理:AtomicLong是通过依靠底层的CAS来保障原⼦性的更新数据,在要添加或者减少的时候,会使⽤死循环不断地cas到特定的值,从⽽达到更新数据的⽬的。

LongAdder的原理:LongAdder是在AtomicLong的基础上将单点更新压⼒分散到各个节点,在低并发的时候通过对base的直接更新可以很好的保障和AtomicLong的性能基本保持⼀致,⽽在⾼并发的时候通过分散提⾼了性能。缺点就是LongAdder在统计的时候如果有并发更新,可能导致统计的数据有误差。

LongAdder继承了Striped64类

public class LongAdder extends Striped64 implements Serializable LongAdder继承了Striped64类,来实现累加功能的,它是实现⾼并发累加的⼯具类;
Striped64的设计核⼼思路就是通过内部的分散计算来避免竞争。Striped64内部包含⼀个base和⼀个Cell[] cells数组,⼜叫hash表。
没有竞争的情况下,要累加的数通过cas累加到base上;如果有竞争的话,会将要累加的数累加到Cells数组中的某个cell元素⾥⾯。

AtomicLong 可否被 LongAdder 替代?
LongAdder只提供了add、increment等简单的⽅法,适合的是统计求和计数的场景,场景⽐较单⼀,⽽AtomicLong还具有compareAndSet等⾼级⽅法,可以应对除了加减之外的更复杂的需要CAS的场景。

tools

CountDownLatch

countDownLatch这个类使⼀个线程等待其他线程各⾃执⾏完毕后再执⾏。

CyclicBarrier

在很多环节需要卡住,要多个线程同时在这⾥都达到后,再向下⾛。

CountDownLatch和CyclicBarrier区别:

  1. countDownLatch是⼀个计数器,线程完成⼀个记录⼀个,计数器递减,只能只⽤⼀次;
  2. CyclicBarrier的计数器更像⼀个阀⻔,需要所有线程都到达,然后继续执⾏,计数器递增,提供reset功能,可以多次使⽤。
Semaphore

信号量,根据⼀些阀值做访问控制

Exchanger

线程之间交互数据,且在并发时候使⽤,两两交换,交换中不会因为线程多⽽混乱,发送出去没接收到会⼀直等,由交互器完成交互过程。

Executors

线程池
java.util.concurrent.Executor,负责线程的使⽤和调度的根接⼝。
|–ExecutorService ⼦接⼝,线程池的主要接⼝。
|–ThreadPoolExecutor 线程池的实现类。
|–ScheduleExecutorService ⼦接⼝,负责线程的调度。
|–ScheduleThreadPoolExecutor,继承了ThreadPoolExecutor,实现了ScheduleExecutorService

  1. Executors.newFixedThreadPool(int)
    创建⼀个指定⼤⼩的线程池,如果超过⼤⼩,放⼊blocken队列中,默认是LinkedBlockingQueue,默认的ThreadFactory为:Executors.defaultThreadFactory(),是⼀个Executors的⼀个内部类。

    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }
    
  2. Executors.newFixedThreadPool(int,ThreadFactory)
    创建⼀个指定⼤⼩的线程池,如果超过⼤⼩,放⼊blocken队列中,默认是LinkedBlockingQueue,⾃⼰指定ThreadFactory,⾃⼰写的ThreadFactory,必须implements ThreadFactory,实现⽅法:newThread(Runnable)。

    public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>(),
                                      threadFactory);
    }
    
  3. Executors.newSingleThreadExecutor()
    创建线程池⻓度为1的,也就是只有⼀个⻓度的线程池,多余的必须等待,它和调⽤Executors.newFixedThreadPool(1)得到的结果⼀样。

    public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }
    
  4. Executors.newSingleThreadExecutor(ThreadFactory) 和 Executors.newCachedThreadPool()
    创建可以进⾏缓存的线程池,默认缓存60s,数据会放在⼀个SynchronousQueue上,⽽不会进⼊blocken队列中,也就是只要有线程进来就直接进⼊调度队列,并且线程空闲后,会保留60秒⽤于等待新的任务才会被释放,在⾼并发下,这个不推荐使⽤,因为在并发较⾼的情况下容易出问题,除⾮⽤来模拟⼀些并发的测试,⽤它有⼀些特殊⽤途。

    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }
    
  5. Executors.newSingleThreadScheduledExecutor()
    添加⼀个Schedule的调度器的线程池,默认只有⼀个调度。

    public static ScheduledExecutorService newSingleThreadScheduledExecutor() {
        return new DelegatedScheduledExecutorService
            (new ScheduledThreadPoolExecutor(1));
    }
    
  6. Executors.newScheduledThreadPool()

    public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
    }
    

executor

Runnable

Runnable 是⼀个接⼝,简单就⼀个⽅法,实现run⽅法,在run⽅法⾥⾯编写你要执⾏的代码就⾏了,但是没有任务返回接⼝,并且⽆法抛出异常。

Callable

Callable也是⼀个接⼝,很简单就⼀个call⽅法,在call⽅法⾥⾯编写你要执⾏的代码就⾏了,返回的就是执⾏的结果了。和Runnable 差别就是它有返回的结果,并且可以抛出异常!⼀般配合ThreadPoolExecutor使⽤的。

Future

Future也是⼀个接⼝,它可以对具体的Runnable或者Callable任务进⾏取消、判断任务是否已取消、查询任务是否完成、获取任务结果。如果是Runnable的话返回的结果是null。
在这里插入图片描述

FutureTask

因为Future只是⼀个接⼝,所以是⽆法直接⽤来创建对象使⽤的,因此就有了下⾯的FutureTask。FutureTask不是接⼝了,是个class。它实现了RunnableFuture接⼝。

public class FutureTask<V> implements RunnableFuture<V>

⽽RunnableFuture接⼝⼜继承了Runnable和Future。

public interface RunnableFuture<V> extends Runnable, Future<V>

线程池 ThreadPoolExecutor,

public abstract class AbstractExecutorService implements ExecutorService {
	/**
     * @throws RejectedExecutionException {@inheritDoc}
     * @throws NullPointerException       {@inheritDoc}
     */
    public Future<?> submit(Runnable task) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<Void> ftask = newTaskFor(task, null);
        execute(ftask);
        return ftask;
    }

    /**
     * @throws RejectedExecutionException {@inheritDoc}
     * @throws NullPointerException       {@inheritDoc}
     */
    public <T> Future<T> submit(Runnable task, T result) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<T> ftask = newTaskFor(task, result);
        execute(ftask);
        return ftask;
    }

    /**
     * @throws RejectedExecutionException {@inheritDoc}
     * @throws NullPointerException       {@inheritDoc}
     */
    public <T> Future<T> submit(Callable<T> task) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<T> ftask = newTaskFor(task);
        execute(ftask);
        return ftask;
    }
}

三个⽅法其实都是把task转成FutureTask,如果task是Callable,就直接赋值。如果是Runnable 就转为Callable再赋值。

CompletableFuture

Java8新增的CompletableFuture则借鉴了Netty等对Future的改造,简化了异步编程的复杂性,并且提供了函数式编程的能⼒。

Fork/Join

ForkJoinPool

ForkJoinPool实现了ExecutorService的线程池。但ForkJoinPool不同于其他类型的ExecutorService,主要是因为它使⽤了窃取⼯作机制

  • 窃取⼯作机制

窃取⼯作机制是指池中的所有线程都试图查找和执⾏提交给池和/或由其他活动任务创建的任务(如果不存在⼯作,则最终阻塞等待⼯作)。但ForkJoinPool并不是为了代替其他两个线程池,⼤家所适⽤的场景各不相同。ForkJoinPool主要是为了执⾏ForkJoinTask⽽存在,⽽ForkJoinTask是⼀种可以将任务进⾏递归分解执⾏从⽽提⾼执⾏并⾏度的任务,那么ForkJoinPool线程池当然主要就是为了完成这些可递归分解任务的调度执⾏,加上⼀些对线程池⽣命周期的控制,以及提供⼀些对池的状态检查⽅法(例如getStealCount),⽤于帮助开发、调优和监视fork/join应⽤程序。

  • 内部数据结构

ForkJoinPool采⽤了哈希数组 + 双端队列的⽅式存放任务,但这⾥的任务分为两类,⼀类是通过execute、submit 提交的外部任务,另⼀类是ForkJoinWorkerThread⼯作线程通过fork/join分解出来的⼯作任务,ForkJoinPool并没有把这两种任务混在⼀个任务队列中。

对于外部任务,会利⽤Thread内部的随机probe值映射到哈希数组的偶数槽位中的提交队列中,这种提交队列是⼀种数组实现的双端队列称之为Submission Queue,专⻔存放外部提交的任务。

对于ForkJoinWorkerThread⼯作线程,每⼀个⼯作线程都分配了⼀个⼯作队列,这也是⼀个双端队列,称之为Work Queue,这种队列都会被映射到哈希数组的奇数槽位,每⼀个⼯作线程fork/join分解的任务都会被添加到⾃⼰拥有的那个⼯作队列中。

在ForkJoinPool中的属性 WorkQueue[] workQueues 就是我们所说的哈希数组,其元素就是内部类WorkQueue实现的基于数组的双端队列。该哈希数组的⻓度为2的幂,并且⽀持扩容。

collections

CopyOnWriteXXXX

ConcurrentHashMap

ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap的结构,即内部拥有⼀个Entry数组,数组中的每个元素⼜是⼀个链表,同时⼜是⼀个ReentrantLock(Segment继承了ReentrantLock)

LinkedBlockingQueue、ArrayBlockingQueue

LinkedBlockingQueue是BlockingQueue的⼀种使⽤Link List的实现,它对头和尾(取和添加操作)采⽤两把不同的锁,相对于ArrayBlockingQueue提⾼了吞吐量。它也是⼀种阻塞型的容器,适合于实现“消费者⽣产者”模式。LinkedBlockingQueue可以不设置队列容量,默认为Integer.MAX_VALUE.其容易造成内存溢出,⼀般要设置其值。

ArrayBlockingQueue是对BlockingQueue的⼀个数组实现,它使⽤⼀把全局的锁并⾏对queue的读写操作,同时使⽤两个Condition阻塞容量为空时的取操作和容量满时的写操作。正因为LinkedBlockingQueue使⽤两个独⽴的锁控制数据同步,所以可以使存取两种操作并⾏执⾏,从⽽提⾼并发效率。⽽ArrayBlockingQueue使⽤⼀把锁,造成在存取两种操作争抢⼀把锁,⽽使得性能相对低下。

对应的⾮并发容器:BlockingQueue特点:拓展了Queue,增加了可阻塞的插⼊和获取等操作原理:通过ReentrantLock实现线程安全,通过Condition实现阻塞和唤醒使⽤阻塞队列的好处:多线程操作共同的队列时不需要额外的同步,另外就是队列会⾃动平衡负载,即哪边(⽣产与消费两边)处理快了就会被阻塞掉,从⽽减少两边的处理速度差距,⾃动平衡负载这个特性就造成它能被⽤于多⽣产者队列,因为你⽣成多了(队列满了)你就要阻塞等着,直到消费者消费使队列不满你才可以继续⽣产。 当许多线程共享访问⼀个公共 collection 时,ConcurrentLinkedQueue 是⼀个恰当的选择。

LinkedBlockingQueue 多⽤于任务队列(单线程发布任务,任务满了就停⽌等待阻塞,当任务被完成消费少了⼜开始负载 发布任务)

ConcurrentLinkedQueue 多⽤于消息队列(多个线程发送消息,先随便发来,不计并发的-cas特点)

  1. 单⽣产者,单消费者 ⽤ LinkedBlockingqueue
  2. 多⽣产者,单消费者 ⽤ LinkedBlockingqueue
  3. 单⽣产者 ,多消费者 ⽤ ConcurrentLinkedQueue
  4. 多⽣产者 ,多消费者 ⽤ ConcurrentLinkedQueue

PriorityBlockingQueue

PriorityBlockingQueue通过使⽤堆这种数据结构实现将队列中的元素按照某种排序规则进⾏排序,从⽽改变先进先出的队列顺序,提供开发者改变队列中元素的顺序的能⼒。队列中的元素必须是可⽐较的,即实现Comparable接⼝,或者在构建函数时提供可对队列元素进⾏⽐较的Comparator对象。不可以放null,会报空指针异常,也不可放置⽆法⽐较的元素;add⽅法添加元素时,是⾃下⽽上的调整堆,取出元素时,是⾃上⽽下的调整堆顺序。

SynchronousQueue、TransferQueue

SynchronousQueue⼀般来说如果线程a通过put⽅法存⼊数据到队列中,如果没有别的线程通过take⽅法去获取这个数据,那线程a进⼊阻塞状态;当有别的线程获取了这个值之后,线程a就恢复执⾏。

如果线程b获取这个队列的数据时队列是空的,那线程b进⼊阻塞状态。直到有线程往这个队列⾥添加数据。

队列LinkedTransferQueue和SynchronousQueue有些类似,但LinkedTransferQueue可以使⽤put、tryTransfer、transfer添加多个数据⽽不⽤等别的线程来获取。

tryTransfer和transfer与put不同的是,tryTransfer和transfer可以检测是否有线程在等待获取数据,如果检测到就⽴即发送新增的数据给这个线程获取⽽不⽤放⼊队列。所以当使⽤tryTransfer和transfer往LinkedTransferQueue添加多个数据的时候,添加⼀个数据后,会先唤醒等待的获取数据的线程,再继续添加数据。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

抽抽了

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

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

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

打赏作者

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

抵扣说明:

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

余额充值