浅谈多线程(2020-11-29)

线程的创建

1.继承Thread重写run()方法。
2.new Thread()传入Runnable。
3.线程池,传入Runnable或Callable。

import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

//创建线程三种方式(继承Thread类、创建Thread、线程池)
public class ThreadTest001 {

    public static void main(String [] args)throws  Exception{
        Thread.currentThread().setName("这是 main 线程");
        System.out.println(""+Thread.currentThread().getName());

        //1.继承Thread类
        ThreadDemo1 thread01 = new ThreadDemo1();
        thread01.start();

        //2.创建Thread
        Thread thread02 = new Thread(){
            public void run(){
                Thread.currentThread().setName("new Thread的线程");
                System.out.println(""+Thread.currentThread().getName());
            }
        };
        thread02.start();

        //3.线程池
        ThreadPoolExecutor threadPool = (ThreadPoolExecutor) Executors.newFixedThreadPool(3);
            // 创建unnable
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    try {
                        while(!Thread.currentThread().isInterrupted()){
                            TimeUnit.SECONDS.sleep(2);
                            System.out.println("做一些工作");
                        }
                    }catch (InterruptedException e){
                        System.out.println("线程在sleep或wait就interrupt,会报InterruptedException异常");
                    }finally {
                        System.out.println("做一些必要的结尾工作");
                    }
                }
            };
        threadPool.execute(runnable);
        threadPool.shutdown();      //关闭线程池
    }

    static class  ThreadDemo1 extends Thread{
        @Override
        public void run() {
            Thread.currentThread().setName("继承Thread的线程");
            System.out.println(""+Thread.currentThread().getName());
        }
    }

}

线程的构造

1.构造线程对象Thread,默认一个线程名,以Thread-开头,从0开始计数。Thread-0、Thread-1.....
2.如果在构造Thread时没有传递Runnable或者没有复写Thread的run()方法,该Thread将不会调用任何方法,如果传递Runnable接口实例,会执行该逻辑单元(Runnable的代码、run()方法里代码)。
3.如果构造线程对象时未传入ThreadGroup,Thread会默认获取父线程的ThreadGroup为该线程的ThreadGroup,此时子线程和父线程在同一个ThreadGroup中。
4.构造Thread时传入stacksize代表该线程占用stack大小(默认0)。该参数一些平台无效。
5.线程的生命周期分为new、runnable、running、block、terminated。

线程状态

1、New
new一个Thread对象时,并不处于执行状态,没调start启动线程,仅仅是创建了对象,开辟了空间(开辟了对象头信息、程序计数器、虚拟机内外的栈。Java进程内存=堆内存你+线程数量*栈内存)。
2、Runnable
当线程调用start时,进入可执行状态Runnable,等待CPU调度。此时对此线程调用wait、sleep、block的IO等操作也不会进入blocked和terminated状态,需获得CPU执行权进入Running才可以。
3、Running
线程获得CPU执行权,此状态下可发生如下转换。
   进入Terminated状态。
   进入Blocked状态,调用了sleep、wait,某个阻塞的IO操作(网络的数据的读写),抢锁失败等。
   进入Runnable状态。如CPU执行时间到了,调用yield放弃CPU执行权。
4、Blocked
当线程调用了sleep、wait、阻塞的IO操作、抢锁等会进入Blocked状态。
   进入Terminated状态。
   进入Runnable状态,休眠时间到了、被唤醒、获得锁资源、阻塞被打断(调用interrupt)。
5、Terminated
线程生命结束。

Thread与Runnable

Thread:     负责线程本身相关的职责和控制
Runnable: 逻辑执行单元的部分

线程的打断

void interrupt():   请求线程中断,中断状态设为true,如果正sleep()阻塞,抛InterruptedException异常。
static boolean interrupted():  测试当前线程是否中断。会将当前线程中断状态设为false。
boolan isInterrupted():  测试线程是否终止。
static Thread currentThread(): 返回当前线程。

没有可强制终止线程的方法,interrupt()只是请求终止。
线程被阻塞(sleep、wait)就无法检测中断状态,
在线程阻塞(sleep、wait)时调用interrupt方法,线程会停止阻塞并被打断同事抛出InterruptedException异常。
如果中断状态被调用sleep()时,不会休眠,会清除这一状态并抛InterruptedException异常。

//线程的打断
public class ThreadTest002 {
    /**
     * 1.通过 interrupt 、 interrupted
     */
    private static class Work1 extends Thread {
        @Override
        public void run() {
            while (true) {
                System.out.println("子线程运行中.....");
                if(Thread.interrupted()) {
                    try {
                        Thread.sleep(200);
                    }catch (InterruptedException e){
                        e.printStackTrace();
                    }
                    System.out.println("子线程被打断.....");
                    break;
                }
            }
            System.out.println(".....");
        }
    }

    /**
     * 2.通过 volatile 开关打断
     */
    private static class Work2 extends Thread {
        private volatile boolean start = true;
        @Override
        public void run() {
            while (start) {
                try {
                    Thread.sleep(200);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
                System.out.println("子线程在运行中.....");
            }
        }

        public void shutDown() {
            this.start = false;
            System.out.println("子线程被停止.....");
        }
    }

    /**
     * 3.通过守护线程方式打断
     */
    private static class Work3 {
        private Thread executeThread;
        //用来判断守护线程中的任务是否运行完成,如果运行结束了,那么可以将主线程打断
        private volatile boolean flag = false;
        public void execute(Runnable runnable) {
            executeThread = new Thread(() -> {
                Thread workerThread = new Thread(runnable);
                //工作线程设置为守护线程
                workerThread.setDaemon(true);
                //启动工作线程
                workerThread.start();
            /*
             * 主线程等待工作线程执行完成,使用join()方法,如果在,
             * join的过程中,线程被打断,那么主线程(executeThread)将结束,
             * 当然守护线程也就结束了。
             */
                try {
                    workerThread.join();
                    //守护线程工作完成
                    flag = true;
                    System.out.println("任务完成,正常退出......");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
            executeThread.start();
        }


        public void shutdown(){
            //定义开始时间和结束时间
            while (!flag) {
                //如果超时,打断主线程
                System.out.println("主线程打断......");
                executeThread.interrupt();
                break;
            }
        }
    }
    
    public static void main(String [] args)throws  Exception{
        //1.通过 interrupt 、 interrupted 打断
        Work1 work1 = new Work1();
        work1.start();
        work1.join(100L);
        work1.interrupt();
        Thread.sleep(1000L);
        System.out.println("主线程结束.....");
        
        //2.通过 volatile 开关打断
        Work2 work2 = new Work2();
        work2.start();
        //主线程等待3秒,3秒之后work还没有执行完成,那么就停止它
        work2.join(1000L);
        if(work2.isAlive()) {
            work2.shutDown();
        }
        System.out.println("主线程结束.....");

        //3.通过守护线程方式打断
        Work3 workThread1 = new Work3();
        workThread1.execute(() -> {
            while (true){

            }
        });
        workThread1.shutdown();
        System.out.println("主线程结束.....");

    }
}

悲观锁

悲观的认为每一次操作时加上排他锁。这样别人想拿这个数据就会block直到它拿到锁。
传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。

乐观锁

乐观锁会乐观的认为每次查询都不会造成更新丢失,利用版本字段控制。

重入锁

锁作为并发共享数据,保证一致性的工具,在JAVA平台有多种实现(如 synchronized 和 ReentrantLock)。
这些已经写好提供的锁为我们开发提供了便利。
重入锁,也叫做递归锁,指的是同一线程 外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。

Monitorenter

Java对象都与一个monitor关联,一个monitor的lock锁在同一时刻只能被一个线程获得。一个线程尝试获取monitor所有权时会有如下
1.若monitor计数器为0,意味monitor的lock没有被其它线程获得,此线程会获得或得lock后对计数器加一。
2.若已拥有该monitor所有权线程重入,会导致计数器再加一。
3.若monitor被其他线程所拥有,当前线程会阻塞,直到计数器减为0。
Monitorexit
释放monitor所有权,就是将计数器减一。

synchronized

synchronized可有四种修饰用法

  1. 修饰代码块:大括号括起代码,作用于调用对象。
  2. 修饰方法:整个方法,作用于调用对象。
  3. 修饰静态方法:整个静态方法,作用于对象。
  4. 修饰类:括号括起来的部分,作用于对象。

Synchronized注意事项:无法控制阻塞时长、阻塞不可中断(不像sleep、wait能捕捉中断信号)。

  • 与monitor关联的对象为null。
  • synchronized作用域太大(越大效率越低)。
  • 不同的monitor锁相同方法(每个线程都用new的新对象进行关联)。
  • 多所交叉导致死锁。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

// synchronized的 使用
public class ThreadTest003 {
    
    //修饰一个代码块。锁定当前对象,对于多个对象互相不影响。
    public void test1(int j) {
        synchronized (this) { }
    }

    // 修饰一个方法 作用于对象
    public synchronized void test2(int j) {}

    // 修饰一个类
    public void test3(int j) {
        synchronized (ThreadTest003.class) { }
    }

    // 修饰一个静态方法
    public synchronized void test4(int j) {}

    public static void main(String [] args)throws  Exception{
        ThreadTest003 example1 = new ThreadTest003();
        ThreadTest003 example2 = new ThreadTest003();
        ExecutorService executorService = Executors.newCachedThreadPool();
        executorService.execute(() -> {
            example1.test2(1);
        });
        executorService.execute(() -> {
            example2.test2(2);
        });
    }
}

ReentrantLock(重入锁)

// ReentrantLock 重入锁
public class ThreadTest004 {
    private Lock lock = new ReentrantLock();
    //公平锁:先来先服务的原则
    //非公平锁保证:无法保证新线程抢占已经在排队的线程的锁。
    //Lock lock=new ReentrantLock(true);//公平锁
    //Lock lock=new ReentrantLock(false);//非公平锁
    private Condition condition = lock.newCondition();//创建 Condition
    
    public void testMethod() {
        try {
            lock.lock();//加锁
            //通过创建 Condition 对象来使线程 wait,必须先执行 lock.lock 方法获得锁
            condition.await();  //等待
            condition.signal();//唤醒 wait 线程
            for (int i = 0; i < 5; i++) {
                System.out.println("ThreadName=" + Thread.currentThread().getName() + (" " + (i + 1)));
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();  //解锁
        }
    }
}

ReentrantReadWriteLock(读写锁)

//ReentrantReadWriteLock 读写锁(可能会使写入线程遭遇饥饿)
public class ThreadTest005 {

    /*
     StampedLock 不是可重入锁,
     StampedLock 无Condition。
    如果是线程使用writeLock()或者readLock()获得锁之后,线程还没执行完就被interrupt()的话,StampedLock内部的死循环没有处理中断的逻辑 会导致CPU飙升
    */
    private final static ReentrantReadWriteLock ReadWriteLock = new ReentrantReadWriteLock(false);
    private final static Lock readLock = ReadWriteLock.readLock();
    private final static Lock writeLock = ReadWriteLock.writeLock();
    private final static List<Long> data = new ArrayList<>();

    public static void write(){
        try {
            writeLock.lock();
            data.add(System.currentTimeMillis());
        }finally {
            writeLock.unlock();
        }
    }

    public static void reader(){
        try {
            readLock.lock();
            System.out.println(Thread.currentThread().getName()+"读线程"+data.size());
        }finally {
            readLock.unlock();
        }
    }

    public static void main(String [] args)throws  Exception{
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (;;)write();
            }
        });

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (;;)
                    reader();
            }
        });
        t1.start();
        t2.start();

    }
}

StampedLock(读写锁的改进)

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.StampedLock;

// StampedLock 带 戳 的读写锁(写,悲观读,乐观读)
public class ThreadTest006 {
    /*
    StampedLock把读分为了悲观读和乐观读,
    悲观读,等价于ReadWriteLock的读。
    乐观读, 也就是若读的操作远大于写的操作,程序查看共享变量是否遭到写入执行的变更,再采取后续的措施。
    */
    private final static StampedLock lock = new StampedLock();
    private final static List<Long> data = new ArrayList<>();

    private static void write(){
        long stamp = -1;
        try {
            stamp = lock.writeLock();
            data.add(System.currentTimeMillis());
            System.out.println(Thread.currentThread().getName()+"写线程,stamped: "+stamp);
        }finally {
            lock.unlockWrite(stamp);
        }
    }

    //乐观读
    private static void read(){
        long stamp = lock.tryOptimisticRead();  //获得一个乐观读锁
        if (!lock.validate(stamp)){             //判断共享变量是否已经被其他线程写
            try {
                stamp = lock.readLock();        //加读悲观锁
                System.out.println(Thread.currentThread().getName()+"读线程: "+   data.size());
            }finally {
                lock.unlockRead(stamp);
            }
        }
    }

    public static void main(String [] args)throws  Exception{
        final ExecutorService executor = Executors.newFixedThreadPool(7);
        Runnable readTask = () ->{
            while (true){
                read();
            }
        };
        Runnable writeTask = () ->{
            for (;;){
                write();
            }
        };
        executor.submit(readTask);
        executor.submit(readTask);
        executor.submit(readTask);
        executor.submit(readTask);
        executor.submit(readTask);
        executor.submit(readTask);
        executor.submit(writeTask);
    }

}

ReentrantLock 与 synchronized 

  1. ReentrantLock通过方法 lock()与 unlock()来进行加锁与解锁操作,synchronized会自动解锁机制不同,ReentrantLock 需手动进行解锁。使用 ReentrantLock 必须在 finally 控制块中进行解锁。
  2. ReentrantLock 相比 synchronized 的优势是可中断、公平锁、多个锁。这种情况下需要 使用 ReentrantLock。

Condition 类和 Object 类锁方法区别

1. Condition 类的 awiat 方法和 Object 类的 wait 方法等效
2. Condition 类的 signal 方法和 Object 类的 notify 方法等效
3. Condition 类的 signalAll 方法和 Object 类的 notifyAll 方法等效
4. ReentrantLock 类可以唤醒指定条件的线程,而 object 的唤醒是随机的

tryLock 和 lock 和 lockInterruptibly 的区别

  • tryLock 能获得锁就返回 true,不能就立即返回 false,tryLock(long timeout,TimeUnit unit),可以增加时间限制,如果超过该时间段还没获得锁,返回 false
  • lock 能获得锁就返回 true,不能的话一直等待获得锁
  •  lock 和 lockInterruptibly,如果两个线程分别执行这两个方法,但此时中断这两个线程,lock 不会抛出异常,而 lockInterruptibly 会抛出异常。

sleep与 wait

wait: 需要拿锁,可中断方法被打断后会报InterruptedException,interrupt标识会被擦除。
1.sleep是Thread的方法,wait是Object方法。
2.sleep不会释放锁,wait会释放锁并且并把Object加到等待的队列中。
3.sleep不需要定义synchronized ,但是wait要。
4.sleep不需要唤醒,但是wait需要唤醒。

线程的一些方法

1.Thread.sleep(long millis),一定是当前线程调用此方法,当前线程进入阻塞,但不释放对象锁,millis后线程自动苏醒进入可运行状态。
2.Thread.yield(),一定是当前线程调用此方法,当前线程放弃获取的cpu时间片,由运行状态变会可运行状态,让OS再次选择线程。让相同优先级的线程轮流执行,但并不保证一定会轮流执行。
实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。Thread.yield()不会导致阻塞。
3.thread.join()/thread.join(long millis),当前线程里调用其它线程1的join方法,当前线程阻塞,但不释放对象锁,直到thread线程执行完毕或者millis时间到,当前线程进入可运行状态。
4.obj.wait(),当前线程调用对象的wait()方法,当前线程释放对象锁,进入等待队列。
依靠notify()/notifyAll()唤醒或者wait(long timeout)timeout时间到自动唤醒。
5.obj.notify()唤醒在此对象监视器上等待的单个线程,选择是任意性的。
6.notifyAll()唤醒在此对象监视器上等待的所有线程。

ThreadGroup

为线程服务,用户通过使用线程组的概念批量管理线程,如批量停止或挂起等。
每个线程创建时,都会纳入线程组的树形结构。

线程控制的工具

Condition(线程间的通信工具)

import java.util.LinkedList;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

// Condition
public class ThreadTest007 {

    // 调用Condition的await()和 signal() 方法,都必须在lock保护之内,就是说必须在lock.lock()和lock.unlock之间才可以使用
    private final static Lock lock = new ReentrantLock();
    private final static Condition produceCond = lock.newCondition();
    private final static Condition consumeCond = lock.newCondition();
    private final static LinkedList<Long> TimeStamp_Pool = new LinkedList<>();
    private final static int MAX_Capacity = 100;

    private static void produce(){
        try {
            lock.lock();
            while (TimeStamp_Pool.size()>=MAX_Capacity){
                produceCond.await();
            }
            long value = System.currentTimeMillis();
            System.out.println(Thread.currentThread().getName()+" 线程,添加"+value+"到 TimeStamp_Pool");
            TimeStamp_Pool.addLast(value);
            consumeCond.signalAll();
        }catch (InterruptedException e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }

    private static void cusume(){
        try {
            lock.lock();
            while (TimeStamp_Pool.isEmpty()){
                consumeCond.await();
            }
            long value = TimeStamp_Pool.removeFirst();
            System.out.println(Thread.currentThread().getName()+" 线程,消费了"+value);
            produceCond.signalAll();
        }catch (InterruptedException e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }

    public static void main(String [] args)throws  Exception{
        Thread P1 =new Thread(new Runnable() {
            @Override
            public void run() {
                for (;;)produce();
            }
        });
        Thread C1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (;;) cusume();
            }
        });
        P1.start();
        C1.start();
    }

}

CountDownLatch(倒计时器)

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

//线程执行的 倒计时器
public class Test003 {
  /*
    CountDownLatch(int count) //实例化一个倒计数器,count指定计数个数
    countDown() // 计数减一
    await() //等待,当计数减到0时,所有线程并行执行
    */
    //线程
    private static ExecutorService executor = Executors.newFixedThreadPool(9);
    //计数器(一般是与线程数量相同,也可少于线程)
    private static final CountDownLatch latch = new CountDownLatch(9);
    public static void main(String[]args) throws InterruptedException{
        //并行处理
        for(int i=0;i<9;i++){
            executor.execute(new SimpleRunnable(i,latch) {});
        }
        //计数器未归零,将等待(串行处理)。
        latch.await();
        executor.shutdown();
        System.out.println("全部 Over 了");
    }
    static class SimpleRunnable implements  Runnable{
        private int index;
        private CountDownLatch latch;
        public SimpleRunnable(int index,CountDownLatch latch){
            this.index=index;
            this.latch=latch;
        }
        @Override
        public void run(){
            int value = index+1;
            System.out.println(Thread.currentThread().getName()+":线程结束了 "+" value: "+value);
            // 计数器减一
            latch.countDown();
        }
    }
}

CyclicBarrier(循环利用的屏障)

//循环利用的屏障
public class Test004 {
    public static void main(String[]args) throws InterruptedException{
        ExecutorService executor = Executors.newFixedThreadPool(5);
        CyclicBarrier cyclicBarrier = new CyclicBarrier(5);
        executor.execute(()->{
                asleep(2,cyclicBarrier);
        });
        executor.execute(()->{
                asleep(5,cyclicBarrier);
        });
        // 复位(重新计数)
        cyclicBarrier.reset();
        Thread.sleep(10000);
        System.out.println("main线程休眠10秒");
        executor.shutdown();
    }
    public static void asleep(int number,CyclicBarrier cyclicBarrier){
        try {
            System.out.println(Thread.currentThread().getName()+"工作"+number+"秒");
            cyclicBarrier.await(); //统一进入下一步
            TimeUnit.SECONDS.sleep(number);
            System.out.println(Thread.currentThread().getName()+"工作结束");
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

Phaser

import java.util.concurrent.Phaser;
import java.util.concurrent.TimeUnit;

//阶段器(多个线程中的分批次统一执行)
public class Test005 {
    public static void main(String[]args) {
        final Phaser phaser = new Phaser(5);
        for (int i=1;i<6;i++){
            new Athletes(i,phaser){}.start();
        }
    }
    static class Athletes extends Thread{
        private final int no;
        private final Phaser phaser;
        Athletes(int no, Phaser phaser){
            this.no=no;
            this.phaser = phaser;
        }
        @Override
        public void run(){
            try {
                System.out.println(no+":号运动员开始跑步PPPPP");
                TimeUnit.SECONDS.sleep(1);
                System.out.println(no+":号运动员跑步结束PPPPP");
                phaser.arriveAndAwaitAdvance();

                TimeUnit.SECONDS.sleep(3);
                System.out.println(no+":号运动员开始骑车CCC");
                TimeUnit.SECONDS.sleep(1);
                System.out.println(no+":号运动员骑车结束CCC");
                phaser.arriveAndAwaitAdvance();

                TimeUnit.SECONDS.sleep(1);
                System.out.println(no+":号运动员开始游泳~~~~~~~");
                TimeUnit.SECONDS.sleep(1);
                System.out.println(no+":号运动员游泳结束~~~~~~~");
                phaser.arriveAndAwaitAdvance();
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }
}

Exchanger

import java.util.concurrent.Exchanger;

//两个线程间相互交换信息的工具
public class Test006 {
    public static void main(String[]args)throws InterruptedException{
        final Exchanger<String> exchanger = new Exchanger<>();
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                MyWxchange(exchanger,"T1");
            }
        },"T1");
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                MyWxchange(exchanger,"T2");
            }
        },"T2");
        t1.start();
        t2.start();
    }
    public static void MyWxchange(Exchanger<String> exchanger,String name){
        try{
            String result = exchanger.exchange("我来自: "+name);
            System.out.println(Thread.currentThread().getName()+" 线程获的的value:"+result);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
}

Semaphore(信号灯)

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;

//信号灯
public class Test007 {
    public static void main(String[]args){
        ExecutorService executor = Executors.newFixedThreadPool(3);
        Semaphore semaphore = new Semaphore(2);

        executor.execute(()->{
            MySemaphore(semaphore,2);
        });
        executor.execute(()->{
            MySemaphore(semaphore,5);
        });
        executor.shutdown();
    }

    public static void MySemaphore(Semaphore semaphore,int mumb){
        try {
            semaphore.acquire();
            System.out.println(Thread.currentThread().getName()+"开始工作.");
            TimeUnit.SECONDS.sleep(mumb);
        }catch (InterruptedException e){
            e.printStackTrace();
        }finally {
            semaphore.release();
            System.out.println(Thread.currentThread().getName()+"开始unlock.");
        }
    }
}

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值