java 多线程 练习2


一:多线程与并发基础知识

  1.什么是并发
    1.1 并发就是指程序同时处理多个任务的能力(百度上解释为:并发,在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行,但任一个时刻点上只有一个程序在处理机上运行。)
    1.2 并发编程的根源在于对多任务情况下对访问资源的有效控制
 
  2.程序、进程与线程
    2.1 程序是静态的概念,windows下通常指exe文件
    2.2 进程是动态的概念,是程序在运行状态,进程说明程序在内存中的边界
    2.3 线程是进程内的一个“基本任务”,每个线程都有自己的功能,是CPU分配与调度的基本单位
 
  3. 并行(多CPU)和并发(一个CPU),同步和异步
 
  4.临界区
    4.1 临界区用来表示一种公共资源与共享数据,可以被多个线程使用
    4.2 同一时间只能有一个线程访问临界区(阻塞状态),其他资源必须等待
    4.3 百度上说:
        临界区 [1]  指的是一个访问共用资源(例如:共用设备或是共用存储器)的程序片段,而这些共用资源又无法同时被多个线程访问的特性。当有线程进入临界区段时,其他线程或是进程必须等待(例如:bounded waiting 等待法),有一些同步的机制必须在临界区段的进入点与离开点实现,以确保这些共用资源是被互斥获得使用,例如:semaphore。只能被单一线程访问的设备,例如:打印机。 [1]

  5.死锁 饥饿 活锁
 
  6.线程安全
    在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的执行,不会出现数据污染等意外情况

  7.线程安全三大特性
    7.1 原子性
        即一个操作或者多个操作,要么全部执行并且执行的过程不会被任何元素打断,要么就都不执行
    7.2 可见性
        当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的的值
    7.3 有序性
        如果在本线程内观察,所有的操作都是有序的;如果在一个线程观察另一个线程,所有的操作都是无序的
        
  8.java内存模型  Java Memory Model
    JMM -> JVM -> 栈Stack(先进后出,后进先出) 堆Heap(方法区(静态区)),如下图


    
    
    
二:Java多线程

  1.创建线程的三种方式
    1.1 继承Thread类(ThreadSample1.java )
    
package com.jhy.www.thread;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Random;

public class ThreadSample1 {
    public static void main(String[] args) {
        Runner1 p1 = new Runner1();
        p1.setName("张三");
        Runner1 p2 = new Runner1();
        p2.setName("老王");
        Runner1 p3 = new Runner1();
        p3.setName("小张");


        p1.start();
        p2.start();
        p3.start();
    }
}

class Runner1 extends Thread{
    @Override
    public void run() {
        int speed = new Random().nextInt(100);  //获取一个随机0到100的数字
        for(int i = 1; i <= 100; i++){
            try {
                Thread.sleep(1000);  //让当前线程休眠1秒
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS").format(new Date())+" | "+this.getName()+"已经前行"+(i*speed)+"米,速度为:"+speed+"m/s");
        }

    }
}
------------------------------------
    1.2 实现 Runnable接口(ThreadSample2.java)

package com.jhy.www.thread;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Random;

public class ThreadSample2 {
    public static void main(String[] args) {
        Thread t1 = new Thread(new Runner2());
        t1.setName("选手一");
        Thread t2 = new Thread(new Runner2());
        t2.setName("选手2");
        Thread t3 = new Thread(new Runner2());
        t3.setName("选手3");

        t1.start();
        t2.start();
        t3.start();

    }
}
class Runner2 implements Runnable{

    @Override
    public void run() {
        int speed = new Random().nextInt(100);
        for(int i=1; i<=100; i++){
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS").format(new Date())+" | "+Thread.currentThread().getName()+"跑步"+(i*speed)+"米,速度为:"+speed+"m/s");

        }
    }
}
----------------------------------------------------  
    1.3 实现Callable 接口,搭配Future对象使用(ThreadSample.java)

package com.jhy.www.thread;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Random;
import java.util.concurrent.*;

public class ThreadSample3 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //使用线程调度器,定长线程池
        ExecutorService executorService = Executors.newFixedThreadPool(3);
        //实例化Runner3
        Runner3 runner1 = new Runner3();
        runner1.setName("小孙");
        Runner3 runner2 = new Runner3();
        runner2.setName("小王");
        Runner3 runner3 = new Runner3();
        runner3.setName("小李");

        //Future用于接受线程内部call方法的返回值
        Future<Integer> result1 = executorService.submit(runner1);  //将这个对象扔到线程池中,线程池自动分配一个线程来运行runner1这个对象的call方法
        Future<Integer> result2 = executorService.submit(runner2);
        Future<Integer> result3 = executorService.submit(runner3);


        Thread.sleep(3000);

        executorService.shutdown();//关闭线程池释放资源


        System.out.println(new Date()+"  小孙跑了"+result1.get()+"m");
        System.out.println(new Date()+"  小王跑了"+result2.get()+"m");
        System.out.println(new Date()+"  小李跑了"+result3.get()+"m");


    }
}
class Runner3 implements Callable<Integer> {
    private String name;
    public void setName(String name){
        this.name = name;
    }

    @Override
    public Integer call() throws Exception {
        Integer speed = new Random().nextInt(100);
        Integer distance = 0;
        for(int i=1; i<=100; i++){
            Thread.sleep(30);
            distance = i*speed;
            System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS").format(new Date())+" | "+this.name+"已经前行"+distance+",速度:"+speed+"m/s");
        }
        return distance;
    }
}
-------------------
    1.4 synchronized 线程同步机制
        synchronized (同步锁) 关键字的作用就是利用一个特定的对象设置一个锁lock(绣球),在多线程(游客)并发访问的时候,同时只允许一个线程(游客)可以获得这个锁,执行特定的代码(迎娶新娘)。执行后释放锁,继续由其他线程争抢
    
    1.5 Synchronized 使用场景
        1.5.1  synchronized代码块   -->锁对象 --> 任意对象 --> new Object()
        1.5.2  synchronized方法     -->锁对象 --> this当前对象 --> this
        1.5.3  synchronized静态方法 -->锁对象 --> 该类的字节码对象  --> Object.classw

    1.6 线程的5种状态
        新建(new)->start()-> 就绪(ready )->run()-> 运行状态 ->阻塞状态/死亡状态
        附图如下:
    
    1.7 死锁产生的原因和解决方法
        产生:多线程对公共资源(文件、数据)等进行操作时,彼此不释放自己的资源,而去试图操作其他线程的资源,而形成交叉引用,就会产生死锁
        解决建议: 减少对公共资源的引用;用完马上释放公共资源;减少synchronized使用,采用“副本”方式替代,后面会有CAS算法的部分解释什么是“副本”
        
 
    

三:JDK并发工具包-JUC
    1.1 ThreadPoll 线程池
         1.1.1 重用存在的线程,减少对象对象、消亡的开销
         1.1.2 线程总数可控,提高资源的利用率
         1.1.3 避免过多资源竞争,避免阻
         1.1.4 提供额外功能,定时执行、定期执行、监控
    
    1.2 线程池的种类
        在java.util.concurrent中,提供了工具类Executors(调度器)对象来创建线程池,可创建的线程池有四种:
            1) CachedThreadPool - 可缓存线程池
---------------------------------------------
package com.jhy.www.juc;

import com.sun.corba.se.impl.orbutil.CorbaResourceUtil;

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

public class ThreadPollSample1 {
    public static void main(String[] args) {
        //调度器对象
        //ExecutorService用于管理线程池
        ExecutorService threadPool = Executors.newCachedThreadPool();
        //可缓存线程池的特点是,无限大,如果线程池中没有可用的线程则创建,有空闲线程则利用起来

        for(int i=0;i<10000; i++){
            final int index = i;
            threadPool.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName()+"--"+index);
                }
            });
        }
        threadPool.shutdown();
        System.out.println("程序执行完成");


    }
}
---------------------------------------------

            2) FixedThreadPool - 定长线程池
---------------------------------------------
package com.jhy.www.juc;

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

public class ThreadPollSample2 {
    public static void main(String[] args) {
        ExecutorService threadPoll = Executors.newFixedThreadPool(3);
        //定长线程池的特点是固定线程总数,空间线程用于执行任务,如果线程都在使用后续任务则处于等待状态,在线程池中的线程
        //如果任务处于等待的状态,备选的等待算法默认为FIFO(先进先出) LIFO(后进先出)
        for(int i=0; i<10000; i++){
            final int index = i;

            threadPoll.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName()+"###"+index);
                }
            });
        }
        threadPoll.shutdown();
        System.out.println("程序运行完成");


    }
}
---------------------------------------------
            3) SingleThreadExecutor - 单线程池
---------------------------------------------
package com.jhy.www.juc;

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

public class ThreadPoolSample3 {
    public static void main(String[] args) {
        //线程调度器
        ExecutorService threadPool = Executors.newSingleThreadExecutor();  //单线程线程池(方便管理了,表现上来看是一个线程按照顺序依次执行)

        for(int i=0; i<10000; i++){
            final int index = i;

            threadPool.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName()+"###"+index);
                }
            });
        }
        threadPool.shutdown();
        System.out.println("程序运行完成");

    }
}
---------------------------------------------

            4) ScheduledThreadPool - 调度线程池
----------------------------------------------------
package com.jhy.www.juc;

import java.util.Date;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class ThreadPoolSample4 {
    public static void main(String[] args) {
        ScheduledExecutorService scheduledThreadPool =  Executors.newScheduledThreadPool(5);//可调度线程池
        /*//延迟三秒执行一次Run方法
        scheduledThreadPool.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("延迟3秒执行");
            }
        } , 3 , TimeUnit.SECONDS);*/
        //Timer , 项目实际开发中scheduledThreadPool与Timer都不会用到,应为有成熟的调度框架Quartz,或者Spring自带调度,
        //程序的调度框架支持一种表达式叫做Cron表达式
        scheduledThreadPool.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                System.out.println(new Date() + "延迟1秒执行,每三秒执行一次");
            }
        }, 1, 3, TimeUnit.SECONDS);

    }
}
-------------------------------------------------------------

    1.3 CountDownLatch - 倒计时锁
        1.3.1 CountDownLatch倒计时锁例如多线程计算后的数
              比如有一个任务A,它要等待其他3个任务执行完毕之后才能执行,此时就可以利用CountDownLatch来实现这种功能了。
------------------------------------------------
package com.jhy.www.juc;

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

public class CountDownLatchSample {
    private static int count = 0;
    public static void main(String[] args) {
        //线程调度器
        ExecutorService executorService = Executors.newFixedThreadPool(100);
        CountDownLatch   cdl = new CountDownLatch(10000); //这个10000需要同下面10000数值相等

        for(int i=0; i<=10000; i++){
            final int index = i;
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    synchronized (CountDownLatchSample.class){
                        try{
                            count += index;
                        }catch (Exception e){
                            e.printStackTrace();
                        }finally{
                            cdl.countDown();  // CountDownLatch数值减一
                        }
                    }
                }
            });
        }
//        try {
//            Thread.sleep(2000);  //为什么这里要睡2秒呢,是因为上面线程的10000次运行,还没有计算完,下面就输出count ,并发问题导致 得到的值总是小于 50005000
//        } catch (InterruptedException e) {
//            e.printStackTrace();
//        }
        //为了代替睡这个办法,用到了CountDownLatch锁
        try {
            cdl.await();  //这里阻塞住线程,知道 cdl的值为0 ,后面的线程才可以执行
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(count);
        executorService.shutdown();
    }
}
---------------------------------------------------------------------

    1.4 Semaphore信号量
         Semaphore信号量经常用于限制获取某种资源的线程数量。下面举个例比如说操场上有5个跑道,一个跑道一次只能有一个学生在上面跑步,一所有跑道在使用,那么后面的学生就需要等待,直到有一个学生不
-------------------------------------------------
package com.jhy.www.juc;

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

public class SemaphoreSample1 {
    public static void main(String[] args) {
        ExecutorService threadPool = Executors.newCachedThreadPool();
        Semaphore semaphore = new Semaphore(5); //信号量为5,表示一次只能塞五个线程

        for(int i=1; i<=20; i++){
            final int index = i;
            threadPool.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        semaphore.acquire();  //获取一个信号量,占用一个跑道
                        play();
                        semaphore.release();  // 释放信号量,释放跑道
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                }
            });
        }
        threadPool.shutdown();
        System.out.println("程序运行完毕");
    }

    public static void play(){
        try {
            System.out.println(new Date()+" - "+Thread.currentThread().getName()+"进入游戏服务器");
            Thread.sleep(2000);  //模拟玩家玩游戏2秒
            System.out.println(new Date()+" - "+Thread.currentThread().getName()+"退出游戏服务器");
            Thread.sleep(500);  //模拟玩家退出服务器所花时间
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }


}
-----------------------------------------------------
public class SemaphoreSample2 {
    public static void main(String[] args) {
        ExecutorService threadPool = Executors.newCachedThreadPool();
        Semaphore semaphore = new Semaphore(5); //信号量为5,表示一次只能塞五个线程

        for(int i=1; i<=20; i++){
            final int index = i;
            threadPool.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        //尝试获取一次信号量,5秒钟内获取到返回true,否则返回false
                        if(semaphore.tryAcquire(6, TimeUnit.SECONDS)){
                            play();
                            semaphore.release();
                        }else{
                            System.out.println(new Date()+" - "+Thread.currentThread().getName()+"sorry,服务器已满");
                        }

                    } catch (Exception e) {
                        e.printStackTrace();
                    }

                }
            });
        }
        threadPool.shutdown();
        System.out.println("程序运行完毕");
    }

    public static void play(){
        try {
            System.out.println(new Date()+" - "+Thread.currentThread().getName()+"进入游戏服务器");
            Thread.sleep(2000);  //模拟玩家玩游戏2秒
            System.out.println(new Date()+" - "+Thread.currentThread().getName()+"退出游戏");
            Thread.sleep(500);  //模拟玩家退出服务器所花时间
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }


}
-----------------------------------------------------------------------

    1.4 CyclicBarrier循环屏障
        CyclicBarrier是一个同步工具类,它允许一组线程互相等待,直到到达某个公共屏障点。与CountDownLatch不同的该barrier在释放等待线程后可以重用,所以称它为循环(Cyclic)的屏障(Barrier)。
---------------------------------------------------------------
package com.jhy.www.juc;

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

public class CyclicBarrierSample {
    private static CyclicBarrier cb = new CyclicBarrier(4);
    public static void main(String[] args) {
        ExecutorService threadPool = Executors.newCachedThreadPool();

        for(int i=0; i<20; i++){
            try {
                Thread.sleep(1000);   //隔一秒让他执行一下
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            threadPool.execute(new Runnable() {
                @Override
                public void run() {
                     go();
                }
            });
        }
        threadPool.shutdown();
    }

    public static void go(){
        System.out.println(Thread.currentThread().getName()+" 准备就绪");
        try {
            cb.await(); //设置屏障点,当累计5个线程都准备好后,才运行后面的代码
            System.out.println(Thread.currentThread().getName()+" 正在运行");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (BrokenBarrierException e) {
            e.printStackTrace();
        }

    }

}
---------------------------------------------------------------------------------
    
    1.4 ReentrantLock重入锁(了解)
        1.4.1 重入锁是指任意线程在获取到锁之后,再次获取该锁而不会被该锁所阻塞
        1.4.2 ReentrantLock设计的目标是用来替代synchronized关键字

    1.5 Condition等待与唤醒
        1.5.1 Condition条件唤醒
              1) 我们在并行程序中,避免不了某些线程要预先规定好的顺序执行,例如:先新增再修改,先买后卖,先进后出......,对于这类场景,使用JUC的Condition对象再合适不过了。
              2) JUC中提供了Condition对象,用于让指定线程等待与唤醒,按预期顺序执行。它必须和ReentrantLock重入锁配合使用。
              3) Condition用于替代wait()/notify()方法
                   – notify只能随机唤醒等待的线程,而Condition可以唤醒指定的线程,这有利于更好的控制并发程序
        1.5.2 Condition核心方法
              1) await() - 阻塞当前线程,直到singal唤醒
              2) signal() - 唤醒被await的线程,从中断处继续执行
              3) signalAll() - 唤醒所有被await()阻塞的线程
        
-----------------------------------------------
public class ConditionSample {
    public static void main(String[] args) {
        ReentrantLock lock = new ReentrantLock(); //Condition对象必须配合Lock一起使用
        Condition c1 = lock.newCondition();//创建Condition
        Condition c2 = lock.newCondition();
        Condition c3 = lock.newCondition();

        new Thread(new Runnable() { //T1
            @Override
            public void run() {
                lock.lock();//加锁
                try {
                    c1.await();//阻塞当前线程,c1.singal的时候线程激活继续执行
                    Thread.sleep(1000);
                    System.out.println("粒粒皆辛苦");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock(); //解锁
                }
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                lock.lock();//加锁
                try {
                    c2.await();
                    Thread.sleep(1000);
                    System.out.println("谁知盘中餐");
                    c1.signal();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock(); //解锁
                }

            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                lock.lock();//加锁
                try {
                    c3.await();
                    Thread.sleep(1000);
                    System.out.println("汗滴禾下土");
                    c2.signal();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock(); //解锁
                }
            }
        }).start();


        new Thread(new Runnable() {
            @Override
            public void run() {
                lock.lock();
                try {
                    Thread.sleep(1000);
                    System.out.println("锄禾日当午");
                    c3.signal();//T3线程继续执行
                } catch (Exception e) {
                    e.printStackTrace();
                }finally {
                    lock.unlock();
                }
            }
        }).start();


    }
}
-------------------------------------------------------
    
    1.6 Callable&Future
        1.6.1 Callable和Runnable一样代表着任务Callable有返回值并且可以抛出
        1.6.2 Future 是一个接口。它用于表示异步计算的结果。提供了检查计算是否完成的方法,以等待计算的完成,并获取计算的结果
--------------------------------------------------------------------
package com.jhy.www.juc;

import java.util.concurrent.*;

public class FutureSample {
    public static void main(String[] args) {
        //线程调度器,创造线程池
        ExecutorService threadPool = Executors.newCachedThreadPool();


        for(int i=0; i<100; i++){  //这个循环是为了模拟高并发一直调用线程的情景
            Coput c = new Coput();
            c.setNum(i);

            Future<Boolean> res = threadPool.submit(c);  //线程调度器将实现Callable接口的c对象放到线程池,当有空闲线程就去执行,然后异步获取到执行结果放到Future对象中
            try {
                Boolean isprimeNumber = res.get();
                if(isprimeNumber){
                    System.out.println(c.getNum()+"#######");
                }else{
                    System.out.println(i+"不是质数");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        }
    }
}

class Coput implements Callable<Boolean>{
    private Integer num ;
    public Integer getNum() {
        return num;
    }
    public void setNum(Integer num) {
        this.num = num;
    }
    @Override
    public Boolean call() throws Exception {
        Boolean flag = true;
        for(int i=2; i<num; i++){
            if(num % i == 0){  //表示可以除净 即不是质数
                flag = false;
                break;
            }
        }
        return flag;
    }
}
------------------------------------------------------------------------------------

    1.7 并发容器
        1.7.1 线程安全的类
              Vector是线程安全的,ArrayList、LinkedList是线程不
              Properties是线程安全的,HashSet、TreeSet是不安
              StringBuffer是线程安全的,StringBuilder是线程不安
              HashTable是线程安全的,HashMap是线程不安
              
        1.7.2 线程安全 - 并发容器
              ArrayList -> CopyOnWriteArrayList - 写复制列表
              HashSet -> CopyOnWriteArraySet - 写复制集合
              HashMap -> ConcurrentHashMap - 分段锁映射
            
    1.8 Atomic包与CAS算法
        1.8.1 Atomic包
              Atomic包是java.util.concurrent下的另一个专门为线程安全设计的Java包,包含多个原子操作类。
              Atomic常用类
                – AtomicInteger – AtomicIntegerArray
                – AtomicBoolean
                – AtomicLong
                – AtomicLongArray
        1.8.2 白话CAS算法
              1) 锁是用来做并发最简单的方式,当然其代价也是最高的。独占锁是一种悲观锁,synchronized就是一种独占锁,它假设最坏的情况,并且只有在确保其它线程不会造成干扰的情况下执行,会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁。
              2) 所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。其中CAS(比较与交换,Compare And Swap)是一种有名的无锁算法。
        1.8.3 Atomic的应用
              虽然基于CAS的线程安全机制很好很高效,但要说的是,并非所有线程安全都可以用这样的方法来实现,这只适合一些粒度比较小型,如计数器这样的需求用起来才有效,否则也不会有锁的存在了。

 

总结附图如下:

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 


    
    
    
    
    
    
    
    

   

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值