Java Semaphore CountDownLatch CyclicBarrier

Semaphore CountDownLatch CyclicBarrier 详解



1. Semaphore

Semaphore翻译成字面意思为 信号量,Semaphore可以控同时访问的线程个数,通过 acquire() 获取一个许可,如果没有就等待,而 release() 释放一个许可。

1.1 Semaphore是什么?

emaphore(信号量)是用来控制同时访问特定资源的线程数量,它通过协调各个线程,以保证合理的使用公共资源。把它比作是控制流量的红绿灯,比如一条马路要限制流量,只允许同时有一百辆车在这条路上行使,其他的都必须在路口等待,所以前一百辆车会看到绿灯,可以开进这条马路,后面的车会看到红灯,不能驶入马路,但是如果前一百辆中有五辆车已经离开了马路,那么后面就允许有5辆车驶入马路,这个例子里说的车就是线程,驶入马路就表示线程在执行,离开马路就表示线程执行完成,看见红灯就表示线程被阻塞,不能执行。

1.2 Semaphore 原理分析

场景引入

ReentrantReadWriteLock 里有读锁和写锁,其中读锁是共享锁,其核心是对AQS里的"state"变量进行操作,每获取一次锁,将state加1,释放锁将state减1。从这里可以看出,将state作为共享资源能够实现线程间的协作。
现在有个需求:资源是共享的,但是数量有限,因此没拿到资源的需要等待别人释放资源。

将state作为标记共享资源的数量,那么就有:

1、线程占有资源后将state减1,线程释放资源后将state加1。 2、若线程没拿到资源(资源都被其它线程占有了),那么挂起等待。
3、线程释放资源后,唤醒其它等待该资源的线程。

这样子,不用synchronized+wait/notify与ReentrantLock+await/signal,也依然能够实现线程间同步。
具体到现实场景:

如停车场只能容纳一定数量的车子,当停车场停满了车(入场许可发放完了),其它想进来的车子必须等待有其它车从停车场开出(释放入场许可)。

1.3 Semaphore 构造

Semaphore类位于java.util.concurrent包下,它提供了2个构造器:

public Semaphore(int permits) {          //参数permits表示许可数目,即同时可以允许多少线程进行访问
    sync = new NonfairSync(permits);
}
public Semaphore(int permits, boolean fair) {    //这个多了一个参数fair表示是否是公平的,即等待时间越久的越先获取许可
    sync = (fair)? new FairSync(permits) : new NonfairSync(permits);
}

可以看出许可的个数就是state的值。

1.4 Semaphore 占有许可

通过调用acquire(xx)占有许可:

public void acquire() throws InterruptedException {  }     //获取一个许可
public void acquire(int permits) throws InterruptedException { }    //获取permits个许可
	#Semaphore.java
    public void acquire(int permits) throws InterruptedException {
        if (permits < 0) throw new IllegalArgumentException();
        //交给AQS处理,可中断
        sync.acquireSharedInterruptibly(permits);
    }

	#AbstractQueuedSynchronizer.java
    public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        //发生了中断,直接返回
        if (Thread.interrupted())
            throw new InterruptedException();
        //尝试修改state(减)
        if (tryAcquireShared(arg) < 0)
        	//修改失败,则挂起等待
            doAcquireSharedInterruptibly(arg);
    }

acquire()用来获取一个许可,若无许可能够获得,则会一直等待,直到获得许可。

具体的操作state在tryAcquireShared(xx)里实现,此处以非公平模式说明:

		#Semaphore.java
        final int nonfairTryAcquireShared(int acquires) {
        	//死循环确保修改state成功,或者state已经获取完了
            for (;;) {
            	//获取state
                int available = getState();
                //减少state
                int remaining = available - acquires;
                if (remaining < 0 ||
                	//CAS 操作
                    compareAndSetState(available, remaining))
                    return remaining;
            }
        }

这几个方法都会被阻塞,如果想立即得到执行结果,可以使用下面几个方法:

public boolean tryAcquire() { };    //尝试获取一个许可,若获取成功,则立即返回true,若获取失败,则立即返回false
public boolean tryAcquire(long timeout, TimeUnit unit) throws InterruptedException { };  //尝试获取一个许可,若在指定的时间内获取成功,则立即返回true,否则则立即返回false
public boolean tryAcquire(int permits) { }; //尝试获取permits个许可,若获取成功,则立即返回true,若获取失败,则立即返回false
public boolean tryAcquire(int permits, long timeout, TimeUnit unit) throws InterruptedException { }; //尝试获取permits个许可,若在指定的时间内获取成功,则立即返回true,否则则立即返回false

每次可以占有多个许可,若占有成功则直接返回,否则挂起等待。

1.5 Semaphore 释放许可

占有许可做了相应的任务后,就可以释放许可了。

通过调用release(xx)释放许可

public void release() { }          //释放一个许可
public void release(int permits) { }    //释放permits个许可
	#Semaphore.java
    public void release(int permits) {
        if (permits < 0) throw new IllegalArgumentException();
        //AQS 实现
        sync.releaseShared(permits);
    }

	#AbstractQueuedSynchronizer.java
    public final boolean releaseShared(int arg) {
    	//尝试修改state(加)
        if (tryReleaseShared(arg)) {
        	//成功修改state,唤醒后继节点
            doReleaseShared();
            return true;
        }
        //修改失败
        return false;
    }

具体的操作state在tryReleaseShared(xx)里实现:

		#Semaphore.java
        protected final boolean tryReleaseShared(int releases) {
            for (;;) {
            	//获取state
                int current = getState();
                //增加
                int next = current + releases;
                if (next < current) // overflow
                    throw new Error("Maximum permit count exceeded");
                //修改
                if (compareAndSetState(current, next))
                    return true;
            }
        }

可以看出:

释放许可,增加state,占有许可,减少state。

1.6 Semaphore 与Lock 区别

与ReentrantLock、ReentrantReadWriteLock 区别在于从不同的角度看待state:

1ReentrantLockReentrantReadWriteLock 获取锁的过程是将state值增大,而Semaphore 占有许可是将state值减小。
2ReentrantLockReentrantReadWriteLock 释放锁的过程是将state值减小,而Semaphore 释放许可是将state值增大。
3、这也是AQS的灵活之处,将具体的"state"锁代表的意义由子类实现,可实现不同场景的应用。

1.7 Semaphore使用例子

假若一个工厂有5台机器,但是有8个工人,一台机器同时只能被一个工人使用,只有使用完了,其他工人才能继续使用。那么我们就可以通过Semaphore来实现:

public class Test {
    public static void main(String[] args) {
        int N = 8;            //工人数
        Semaphore semaphore = new Semaphore(5); //机器数目
        for(int i=0;i<N;i++)
            new Worker(i,semaphore).start();
    }
     
    static class Worker extends Thread{
        private int num;
        private Semaphore semaphore;
        public Worker(int num,Semaphore semaphore){
            this.num = num;
            this.semaphore = semaphore;
        }
         
        @Override
        public void run() {
            try {
                semaphore.acquire();
                System.out.println("工人"+this.num+"占用一个机器在生产...");
                Thread.sleep(2000);
                System.out.println("工人"+this.num+"释放出机器");
                semaphore.release();           
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

执行结果:

工人0占用一个机器在生产...
工人1占用一个机器在生产...
工人2占用一个机器在生产...
工人4占用一个机器在生产...
工人5占用一个机器在生产...
工人0释放出机器
工人2释放出机器
工人3占用一个机器在生产...
工人7占用一个机器在生产...
工人4释放出机器
工人5释放出机器
工人1释放出机器
工人6占用一个机器在生产...
工人3释放出机器
工人7释放出机器
工人6释放出机器

回到顶部


2. CountDownLatch

正如每个Java文档所描述的那样,CountDownLatch 是一个同步工具类,它允许一个或多个线程一直等待,直到其他线程的操作执行完后再执行。

2.1 CountDownLatch是什么?

CountDownLatch是在java1.5被引入的,跟它一起被引入的并发工具类还有CyclicBarrier、Semaphore、ConcurrentHashMap和BlockingQueue,它们都存在于java.util.concurrent包下。CountDownLatch这个类能够使一个线程等待其他线程完成各自的工作后再执行。例如,应用程序的主线程希望在负责启动框架服务的线程已经启动所有的框架服务之后再执行。

CountDownLatch是通过一个计数器来实现的,计数器的初始值为线程的数量。每当一个线程完成了自己的任务后,计数器的值就会减1。当计数器值到达0时,它表示所有的线程已经完成了任务,然后在闭锁上等待的线程就可以恢复执行任务。

在这里插入图片描述

2.2 CountDownLatch 原理分析

场景引入

A、B、C三个线程协作:

A 等待B、C完成任务后再进行下一步操作。

这场景我们可能会想到用Thread.join(),A调用B.join(),C.join(),A阻塞等待,当B、C线程执行结束后唤醒A。这种方式虽然能够解决问题,但是有些不尽人意的地方:比如说A不一定要等待B、C执行完成,而是B、C中途完成某个任务后通知A;又比如,B、C线程不止执行一次任务,而是一定的次数后才会唤醒A,这个时候使用Thread.join() 就无法解决问题了。
而CountDownLatch 可以很好地解决这问题。

2.3 CountDownLatch 构造

CountDownLatch类只提供了一个构造器:

public CountDownLatch(int count) {  };  //参数count为计数值
	#CountDownLatch.java
    //初始化次数
    public CountDownLatch(int count) {
        if (count < 0) throw new IllegalArgumentException("count < 0");
        this.sync = new Sync(count);
    }

    Sync(int count) {
    	//设置state
        setState(count);
    }

可以看出,count的值最终反馈到state上。

然后下面这3个方法是CountDownLatch类中最重要的方法:

public void await() throws InterruptedException { };   //调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行
public boolean await(long timeout, TimeUnit unit) throws InterruptedException { };  //和await()类似,只不过等待一定的时间后count值还没变为0的话就会继续执行
public void countDown() { };  //将count值减1

构造器中的计数值(count)实际上就是闭锁需要等待的线程数量。这个值只能被设置一次,而且CountDownLatch没有提供任何机制去重新设置这个计数值。

与CountDownLatch的第一次交互是主线程等待其他线程。主线程必须在启动其他线程后立即调用CountDownLatch.await()方法。这样主线程的操作就会在这个方法上阻塞,直到其他线程完成各自的任务。

其他N 个线程必须引用闭锁对象,因为他们需要通知CountDownLatch对象,他们已经完成了各自的任务。这种通知机制是通过 CountDownLatch.countDown()方法来完成的;每调用一次这个方法,在构造函数中初始化的count值就减1。所以当N个线程都调 用了这个方法,count的值等于0,然后主线程就能通过await()方法,恢复执行自己的任务。

2.4 CountDownLatch 等待

通过await(xx)等待state变为0,调用的方法即是await():

	#CountDownLatch.java
    public boolean await(long timeout, TimeUnit unit)
        throws InterruptedException {
       	//超时返回
        return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
    }

	#AbstractQueuedSynchronizer.java
    public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout)
            throws InterruptedException {
        //该方法响应中断
        if (Thread.interrupted())
            throw new InterruptedException();
        //主要工作在tryAcquireShared(xx)里
        return tryAcquireShared(arg) >= 0 ||
            doAcquireSharedNanos(arg, nanosTimeout);
    }

又是AQS的套路,具体的操作state在tryAcquireShared(xx)里实现:

	#CountDownLatch.java
    protected int tryAcquireShared(int acquires) {
    	//若state == 0,则返回1,否则-1
    	//外层判断>=0,说明当前state还有数量,则需要阻塞等待,否则不阻塞
        return (getState() == 0) ? 1 : -1;
    }

与其它子类实现的tryAcquireShared(xx)方法不同的是,CountDownLatch里的Sync并没有修改state的值,仅仅只是判断state?=0进而做具体的操作而已。
由此可知:CountDownLatch 是基于AQS的共享模式。

2.5 CountDownLatch 倒数计数

既然调用await(xx)可能会使得线程阻塞等待,那么势必有其它线程唤醒它,调用的方法即是countDown():

	#CountDownLatch.java
    public void countDown() {
        sync.releaseShared(1);
    }

	#AbstractQueuedSynchronizer.java
    public final boolean releaseShared(int arg) {
    	//子类实现
        if (tryReleaseShared(arg)) {
        	//AQS里实现,唤醒阻塞的线程
            doReleaseShared();
            return true;
        }
        return false;
    }

同样的,具体的操作state在tryReleaseShared(xx)里实现:

		#CountDownLatch.java
        protected boolean tryReleaseShared(int releases) {
            for (;;) {
            	//获取state
                int c = getState();
                //若当前state==0,说明已经没有可以释放的了
                if (c == 0)
                    return false;
                int nextc = c-1;
                //CAS修改
                if (compareAndSetState(c, nextc))
                    //说明可以唤醒其它线程了
                    return nextc == 0;
            }
        }

也即是说,当线程调用await(xx)阻塞后,其它线程通过countDown()修改state值,若是发现state最终变为0了,那么唤醒阻塞的线程。

用图表示CountDownLatch主要结构如下:

在这里插入图片描述

2.6 CountDownLatch与其它AQS子类封装器的区别

基于AQS的封装器:ReentrantLock、ReentrantReadWriteLock、Semaphore,它们对state值的修改包括增加与减少,而CountDownLatch 只是减小state的值,用以实现倒数计数的功能。
可类比场景如下:

1、田径运动场开始百米赛跑。
2、运动员在跑道上各就各位(多个线程调用await 阻塞等待)。
3、裁判喊倒数3、2、1(线程调用countDown)。
4、等待倒数结束,发令枪响,运动员就开始跑(线程被唤醒,继续做事)。

可以看出,运动员不会去干涉裁判的倒数(修改state值)。

2.7 CountDownLatch使用例子

比如对于马拉松比赛,进行排名计算,参赛者的排名,肯定是跑完比赛之后,进行计算得出的,翻译成Java识别的预发,就是N个线程执行操作,主线程等到N个子线程执行完毕之后,再继续往下执行。

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
 * @Description:马拉松比赛
 */
public class CountdownLatchTest {

    public static void main(String[] args) {
        ExecutorService service = Executors.newCachedThreadPool();
        final CountDownLatch cdOrder = new CountDownLatch(1);
        final CountDownLatch cdAnswer = new CountDownLatch(3);
        for(int i=0;i<3;i++){
            Runnable runnable = new Runnable(){
                @Override
                public void run(){
                    try {
                        System.out.println("运动员" + Thread.currentThread().getName() + "等待信号枪");
                        cdOrder.await();
                        System.out.println("运动员" + Thread.currentThread().getName() + "开跑");
                        Thread.sleep((long)(Math.random()*10000));
                        System.out.println("运动员" + Thread.currentThread().getName() + "到达终点!");
                        cdAnswer.countDown();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            };
            service.execute(runnable);
        }
        try {
            Thread.sleep(5000);

            System.out.println("裁判" + Thread.currentThread().getName() + "即将鸣信号枪");
            cdOrder.countDown();
            System.out.println("裁判" + Thread.currentThread().getName() + "已经鸣枪,等待运动员跑完");
            cdAnswer.await();
            System.out.println("三个运动员都跑到了终点,裁判"+ Thread.currentThread().getName() +"统计名次" );
        } catch (Exception e) {
            e.printStackTrace();
        }
        service.shutdown();
    }
}

运行结果:
在这里插入图片描述

回到顶部


3. CyclicBarrier

字面意思回环栅栏,通过它可以实现让一组线程等待至某个状态之后再全部同时执行。

3.1 CyclicBarrier是什么?

CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。CyclicBarrier默认的构造方法是CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞。

3.2 CyclicBarrier 原理分析

场景引入

在CountDownLatch 场景里说到运动员需要裁判,想想可以不需要裁判吗?运动员之间自发倒数,倒数结束就一起跑。

更普遍的场景是:

1、几个驴友想去某个景点旅游,约定了在某个地方集合后再一起出发。
2、每个驴友到达集合点时打卡并看人都到齐了没,没到齐则等待。
3、若最后一个参与者过来后发现人到齐了,于是告诉大家不用等了,出发吧。

CyclicBarrier 可满足该场景的需求。

3.3 CyclicBarrier 构造

	#CyclicBarrier.java
    public CyclicBarrier(int parties, Runnable barrierAction) {
    	//必须要有参与者
        if (parties <= 0) throw new IllegalArgumentException();
        this.parties = parties;
        //临时变量count
        this.count = parties;
        //参与者都到达了后执行的动作
        this.barrierCommand = barrierAction;
    }

可以看出,此处并没有AQS介入,也就是没有直接修改state。
CyclicBarrier是通过ReentrantLock + Condition 来实现线程间同步的:

	#CyclicBarrier.java
    //独占锁,为了互斥修改count
    private final ReentrantLock lock = new ReentrantLock();
    //线程等待条件
    private final Condition trip = lock.newCondition();
    //修改的共享变量
    private int count;

3.4 CyclicBarrier 等待参与者

接着来分析,如何实现线程间的同步的。

	#CyclicBarrier.java
    public int await() throws InterruptedException, BrokenBarrierException {
        try {
        	//实际调用doWait(),此处是不限时等待
            return dowait(false, 0L);
        } catch (TimeoutException toe) {
            throw new Error(toe); // cannot happen
        }
    }

    private int dowait(boolean timed, long nanos)
        throws InterruptedException, BrokenBarrierException,
               TimeoutException {
        final ReentrantLock lock = this.lock;
        //先锁住
        lock.lock();
        try {
            final Generation g = generation;
            //等待过程被中断
            if (g.broken)
                throw new BrokenBarrierException();
            //中断了线程
            if (Thread.interrupted()) {
                breakBarrier();
                throw new InterruptedException();
            }
            //等待个数-1
            int index = --count;
            if (index == 0) {
            	//都到齐了,无需等待了
                boolean ranAction = false;
                try {
                	//执行既定的方法
                    final Runnable command = barrierCommand;
                    if (command != null)
                        command.run();
                    ranAction = true;
                    //开始下一轮
                    nextGeneration();
                    return 0;
                } finally {
                    if (!ranAction)
                        breakBarrier();
                }
            }

            //走到这,说明还需要等待
            for (;;) {
                try {
                    if (!timed)
                    	//不限时等待
                        trip.await();
                    else if (nanos > 0L)
                    	//限时等待,时间到了就返回
                        nanos = trip.awaitNanos(nanos);
                } catch (InterruptedException ie) {
                	//等待过程被中断,则抛出异常
                    if (g == generation && ! g.broken) {
                        //不等了,唤醒其它线程
                        breakBarrier();
                        throw ie;
                    } else {
                    	...
                        Thread.currentThread().interrupt();
                    }
                }

                //醒来后发现已经被中断了,则直接抛出异常
                if (g.broken)
                    throw new BrokenBarrierException();

                //已经开启了下一轮,说明前面一轮都到齐了结束了
                if (g != generation)
                    return index;

                if (timed && nanos <= 0L) {
                	//超时了还是没到齐,不等了,唤醒其它线程
                    breakBarrier();
                    throw new TimeoutException();
                }
            }
        } finally {
            lock.unlock();
        }
    }

线程先获取独占锁,然后修改count值,若发现修改后count !=0,那么还需要等待,等待借助的是Condition.await(xx)方法。
有等待,自然有唤醒的地方:

	#CyclicBarrier.java
    private void breakBarrier() {
    	//置为true,表示已经结束等待了
        generation.broken = true;
        //重置count,复用的关键
        count = parties;
        //唤醒其它在等待的线程
        trip.signalAll();
    }

用图表示,等待/唤醒过程如下:

在这里插入图片描述
来看看CyclicBarrier 主要方法:

在这里插入图片描述

3.5 CyclicBarrier的应用场景

CyclicBarrier可以用于多线程计算数据,最后合并计算结果的应用场景。比如我们用一个Excel保存了用户所有银行流水,每个Sheet保存一个帐户近一年的每笔银行流水,现在需要统计用户的日均银行流水,先用多线程处理每个sheet里的银行流水,都执行完之后,得到每个sheet的日均银行流水,最后,再用barrierAction用这些线程的计算结果,计算出整个Excel的日均银行流水。

3.6 CyclicBarrier与CountDownLatch 区别

看到这,你也许已经发现了CyclicBarrier 和CountDownLatch 实现的功能很相似,都是等待某个条件满足后再进行下一步的动作,两者不同之处在于:

1、CountDownLatch 参与的线程分为两类:一个是等待者,另一个是计数者;CyclicBarrier 参与的线程既是等待者,也是计数者。
2、CountDownLatch 完成一次完整的协作过程后不能再复用,CountDownLatch 可以复用(不用重新新建CountDownLatch 对象)。
3、CountDownLatch 的计数值与线程个数没有必然联系,CyclicBarrier 的初始计数值与线程个数一致。
4、CountDownLatch 基于AQS实现,CyclicBarrier 基于ReentrantLock&Condition实现(内部也是基于AQS)。

  • CountDownLatch的计数器只能使用一次。而CyclicBarrier的计数器可以使用reset()
    方法重置。所以CyclicBarrier能处理更为复杂的业务场景,比如如果计算发生错误,可以重置计数器,并让线程们重新执行一次。
  • CyclicBarrier还提供其他有用的方法,比如getNumberWaiting方法可以获得CyclicBarrier阻塞的线程数量。isBroken方法用来知道阻塞的线程是否被中断。

3.7 CyclicBarrier使用例子

下面我们来看看Barrier循环使用的例子,下面例子中getNumberWaiting方法可以获得CyclicBarrier阻塞的线程数量

周末公司组织大巴去旅游,总共有三个景点,每个景点约定好游玩时间,一个景点结束后需要集中一起出发到下一个景点。

import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class CyclicBarrierTest {

    public static void main(String[] args) {
        ExecutorService service = Executors.newCachedThreadPool();
        final  CyclicBarrier cb = new CyclicBarrier(3);
        for(int i=0;i<3;i++){
            Runnable runnable = new Runnable(){
                public void run(){
                    try {
                        Thread.sleep((long)(Math.random()*10000));
                        System.out.println("线程" + Thread.currentThread().getName() + "即将到达集合地点1,当前已有" + (cb.getNumberWaiting()+1) + "个已经到达," + (cb.getNumberWaiting()==2?"都到齐了,继续走啊":"正在等候"));
                        cb.await();

                        Thread.sleep((long)(Math.random()*10000));
                        System.out.println("线程" + Thread.currentThread().getName() + "即将到达集合地点2,当前已有" + (cb.getNumberWaiting()+1) + "个已经到达," + (cb.getNumberWaiting()==2?"都到齐了,继续走啊":"正在等候"));
                        cb.await();
                        Thread.sleep((long)(Math.random()*10000));
                        System.out.println("线程" + Thread.currentThread().getName() + "即将到达集合地点3,当前已有" + (cb.getNumberWaiting() + 1) + "个已经到达," + (cb.getNumberWaiting()==2?"都到齐了,继续走啊":"正在等候"));
                        cb.await();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            };
            service.execute(runnable);
        }
        service.shutdown();
    }
}

运行结果:
在这里插入图片描述

回到顶部


4. 三者适用场景总结

在这里插入图片描述

回到顶部


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

GeGe&YoYo

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

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

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

打赏作者

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

抵扣说明:

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

余额充值