java高级之线程

目录

1.基本原理

1.1 何为进程

1.2 何为线程

1.3 两者区别

2.线程

2.1 线程的组成

2.2 线程的特点

2.3 线程的创建方式

2.3.1 获取和设置线程的名称

2.3.2 继承Thread类

2.3.3 实现Runnable接口

2.4 线程的状态

2.5 常见方法

2.5.1 休眠(sleep)

2.5.2 放弃(yield)

2.5.3 加入(join)

2.5.4 守护线程(setDaemon)

2.6 线程的状态

 3.线程安全

3.1 线程安全问题

3.2 解决方法 

3.2.1 同步代码块

3.2.2 线程的等待

 3.2.3 同步方法

4.死锁

5.线程通信

6.线程池

6.1 何为线程池

6.2 常用方法

6.3 线程池方法的使用

6.4 通过自定义阻塞队列创建线程池

7.创建线程的第三种方式

8.手动锁

8.1 Thread方法手动锁

8.2 Runnable方法手动锁


1.基本原理

1.1 何为进程

        正在运行的程序,是系统进行资源分配的基本单位

        目前操作系统都是支持多进程,可以同时执行多个进程,通过进程ID区分

        单核CPU在同一时刻,只能有一个进程;宏观并行,微观串行

1.2 何为线程

        线程,又称轻量级进程(Light Weight Process)。进程中的一条执行路径,也是CPU的基本调度单位。一个进程由一个或多个线程组成,彼此间完成不同的工作,同时执行,称为多线程。

        例如:迅雷是一个进程,当中的多个下载任务即为线程;Java虚拟机是一个进程,当中默认包含主线程(main),可通过代码创建多个独立线程,与main并发执行

1.3 两者区别

        1. 进程是操作系统资源分配的基本单位,而线程是CPU的基本调度单位。
        2. 一个程序运行后至少有一个进程。
        3. 一个进程可以包含多个线程,但是至少需要有一个线程,否则这个进程是没有意义。
        4. 进程间不能共享数据段地址,但是同进程的线程之间可以。

2.线程

2.1 线程的组成

任何一个线程都具有基本的组成部分   
          CPU时间片: 操作系统(OS)会为每个线程分配执行时间  

           运行数据:
                堆空间: 存储线程需要的对象,多个线程可以共享堆中的数据。
                栈空间:  存储线程需使用的局部变量,每个线程都拥有独立的栈。

            线程的逻辑代码。

2.2 线程的特点

        1. 线程抢占式执行

                效率高
                可防止单一线程长时间独占CPU

        2. 在单核CPU中,宏观上同时执行,微观上顺序执行

2.3 线程的创建方式

        1. 【继承Thread类,重写run方法】
        2. 【实现Runnable接口】
        3.  实现Callable接口

2.3.1 获取和设置线程的名称

1.获取线程ID和线程名称

        (1) 在Thread的子类中调用this.getId()或this.getName()
        (2) 使用Thread.currentThread().getId()和Thread.currentThread().getName()

2.修改线程名称

        (1)调用线程对象的setName()方法
        (2)使用线程子类的构造方法赋值

2.3.2 继承Thread类

方法:

public class ThreadTest extends Thread{
    @Override
    public void run() {
        //写自己的任务代码
        for (int i = 0; i < 20; i++) {
            //1通过Thread对象中getName();有缺点只能在Thread子类中获取
            //System.out.println(this.getName()+"======================="+i);

            //2在Thread类中存在一个静态方法可以获取当前线程对象,再通过该对象调用getName()
            System.out.println(Thread.currentThread().getName()+"==============="+i);
        }
    }
}

测试类:

public class MainTest {
    public static void main(String[] args) {
        //创建一个线程对象
        ThreadTest threadTest=new ThreadTest();

        threadTest.setName("副线程");//设置线程名称
        //开启线程---执行时会调用run方法 时间片
        threadTest.start();

        for (int i = 0; i < 20; i++) {
            System.out.println("主线程----------------"+i);
        }
    }
}

2.3.3 实现Runnable接口

任务代码:

public class MyRunnable implements Runnable {
    //任务代码
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            System.out.println(Thread.currentThread().getName()+"============"+i);
        }
    }
}

测试类:

public class RunnableTest {
    public static void main(String[] args) {
        //1.创建任务对象
        MyRunnable myRunnable=new MyRunnable();
        //2.创建线程对象并执行要执行的任务 可直接命名 myRunnable此处必写上边对应的创建任务对象名字
        Thread t1=new Thread(myRunnable,"副线程");
        t1.start();

        for (int i = 0; i < 20; i++) {
            System.out.println("main线程---------------------"+i);
        }
    }
}

2.4 线程的状态

2.5 常见方法

2.5.1 休眠(sleep)

     public static void sleep(long millis)
     当前线程主动休眠millis毫秒。

例子:实现一个时间显示,每隔一秒钟更新一次显示。例如时间秒数显示

public class SleepTest {
    public static void main(String[] args) {

        DateFormat date=new SimpleDateFormat("yyyy-MM-dd E HH:mm:ss");//自定义时间模式

        while (true){
            Date d=new Date();//获取当前时间
            String format = date.format(d);
            System.out.println(format);
            try {
                Thread.sleep(1000);//当前线程主动休眠1秒进行打印 此处有异常要抛出
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }
    }
}

2.5.2 放弃(yield)

      public static void yield()
      当前线程主动放弃时间片,回到就绪状态,竞争下一次时间片线程;调用yield会让出cpu的执行权,但别的线程不一定接受礼让。在cpu比较繁忙的时候效果明显,空闲的时候没什么影响。

例子:A B两个线程,不断对i进行输出,B执行yield方法,B线程的i出现次数频率明显减少。没有yield方法的话,A B两个线程的出现频率次数差不多

public class YieldTest {

    public static void main(String[] args) {
        A a=new A();
        B b=new B();
        a.setName("A");
        b.setName("B");
        a.start();
        b.start();
    }
}
class A extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            System.out.println(Thread.currentThread().getName()+"======"+i);
        }
    }
}
class B extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            Thread.yield();
            System.out.println(Thread.currentThread().getName()+"============="+i);
        }
    }
}

2.5.3 加入(join)

      public final void join()
      允许其他线程加入到当前线程中

例子:

      在A线程中调用了B线程的join()方法时,表示只有当B线程执行完毕时,A线程才能继续执行

      join方法必须在线程start方法调用之后调用才有意义

      join方法可以在start方法前调用时,并不能起到同步的作用

      开启多条线程同时爬取不同网站的信息,进行处理,然后统一返回给前台,这里面要注意的就是,我们必须等待前面的线程都执行结束,才能返回给前端

public class JoinTest {

    public static void main(String[] args) throws InterruptedException {
        Runnable a=new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 4; i++) {
                    System.out.println("A:"+i);
                }
            }
        };
        Runnable b=new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 4; i++) {
                    System.out.println("B:"+i);
                }
            }
        };
        Thread t1=new Thread(a);
        Thread t2=new Thread(b);

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

        t1.join();
        t2.join();
        System.out.println("爬取结束");
    }
}

2.5.4 守护线程(setDaemon)

     线程对象.setDaemon(true);设置为守护线程。
     线程有两类:用户线程(前台线程)和守护线程(后台线程)
     如果程序中所有前台线程都执行完毕了,后台线程也会自动结束。
     垃圾回收线程属于守护线程。

举例: 将一个线程以守护线程方式来运行,即主线程已经执行完毕,守护线程程序也结束

public class DaemonTest {
    public static void main(String[] args) throws InterruptedException {
        Daemon d=new Daemon();

        Thread t=new Thread(d);
        t.setDaemon(true);
        t.start();
        Thread.sleep(3000);
        System.out.println("线程结束");

    }
}
class Daemon implements Runnable {

    @Override
    public void run() {
        for (int i = 0; i < 50; i++) {
            System.out.println("运行副线程"+i);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }

    }
}

2.6 线程的状态

 3.线程安全

3.1 线程安全问题

代码展示:

public class Safe {
    private static String [] arr=new String[5];
    private static int index=0;
    public static void main(String[] args) throws Exception {

        //匿名函数
        Runnable hello=new Runnable() {
            @Override
            public void run() {
                if(arr[index]==null){
                    arr[index]="hello";
                    index++;
                }
            }
        };
        //匿名函数
        Runnable world=new Runnable() {
            @Override
            public void run() {
                if(arr[index]==null){
                    arr[index]="world";
                    index++;
                }
            }
        };

        Thread t1=new Thread(hello);
        Thread t2=new Thread(world);
         t1.start();
         t2.start();

         t1.join();
         t2.join();

        System.out.println(Arrays.asList(arr));
        //此处结果有4种
        //[hello, world, null, null, null]
        //[world, hello, null, null, null]
        //[hello, null,  null, null, null]
        //[world, null,  null, null, null]
    }
}

出现问题

 (1)当多线程并发访问临界资源时,如果破坏原子操作,可能会造成数据不一致。

 (2)临界资源:共享资源(同一对象),一次仅允许一个线程使用,才可保证其正确性。

 (3)原子操作:不可分割的多步操作,被视作一个整体,其顺序和步骤不可打乱或缺省。

3.2 解决方法 

3.2.1 同步代码块

synchronized(临界资源对象){//临界资源对象加锁

        //代码(原子操作)

}

注意:

        每个对象都有一个互斥锁标记,用来分配给线程

        只有拥有对象互斥锁标记的线程,才能进入对该对象加锁的同步代码块

        线程退出同步代码块时,会释放相应的互斥锁标记

举例1:四个窗口分别卖100张票

public class Buy extends Thread{
    private static int ticked=100;
    @Override
    public void run() {
        while (true){
            if(ticked>0){
                System.out.println(this.getName()+"正在卖第"+ticked+"张票");
                ticked--;
            }else{
                break;
            }
        }

    }
}

测试类

public class BuyTest {
    public static void main(String[] args) {

        Buy buy1=new Buy();
        buy1.setName("窗口1");
        Buy buy2=new Buy();
        buy2.setName("窗口2");
        Buy buy3=new Buy();
        buy3.setName("窗口3");
        Buy buy4=new Buy();
        buy4.setName("窗口4");
        buy1.start();
        buy2.start();
        buy3.start();
        buy4.start();
    }
}

可能会出现负数,多线程共享一个资源时就有可能出现线程安全问题

举例2:四个窗口共卖100张票

(1)使用Runable

public class BuyOnly implements Runnable{
    private static int ticked=100;
    @Override
    public void run() {
        while (true){
            synchronized (this){//必须保证多个线程使用的是同一个锁,此处不能new一个对象,ticked不是一个对象
                if(ticked>0){
                    System.out.println(Thread.currentThread().getName()+"正在卖第"+ticked+"张票");
                    ticked--;
                    try{
                        Thread.currentThread().sleep(100);
                    }catch (InterruptedException e){
                        e.printStackTrace();
                    }
                }else{
                    break;
                }
            }

        }
    }
}

测试类

public class BuyOnlyTest {
    public static void main(String[] args) {

        BuyOnly buyOnly=new BuyOnly();

        Thread thread=new Thread(buyOnly,"窗口A");
        Thread thread1=new Thread(buyOnly,"窗口B");
        Thread thread2=new Thread(buyOnly,"窗口C");
        Thread thread3=new Thread(buyOnly,"窗口D");

        thread.start();
        thread1.start();
        thread2.start();
        thread3.start();
    }
}

(2)使用Thread

public class BuyOnlyThread extends Thread{
    private static int ticked=100;
    @Override
    public void run() {
        while(true){
            //使用本类作为锁对象
            synchronized (BuyOnlyThread.class){
                if(ticked>0){
                    try{
                        Thread.sleep(10);
                    }catch (InterruptedException e){
                        e.printStackTrace();
                    }
                    System.out.println(this.getName()+"正在卖第"+ticked+"张票");
                    ticked--;
                }else{
                    break;
                }

            }
        }
    }
}

测试类

public class BuyOnlyThreadTest {
    public static void main(String[] args) {
        BuyOnlyThread buyOnlyThread=new BuyOnlyThread();
        buyOnlyThread.setName("A窗口");
        BuyOnlyThread buyOnlyThread1=new BuyOnlyThread();
        buyOnlyThread1.setName("B窗口");
        BuyOnlyThread buyOnlyThread2=new BuyOnlyThread();
        buyOnlyThread2.setName("C窗口");
        BuyOnlyThread buyOnlyThread3=new BuyOnlyThread();
        buyOnlyThread3.setName("D窗口");

        buyOnlyThread.start();
        buyOnlyThread1.start();
        buyOnlyThread2.start();
        buyOnlyThread3.start();
    }
}

3.2.2 线程的等待

 3.2.3 同步方法

synchronized 返回值类型 方法名称(形参列表){//对当前对象(this)加锁

        //代码(原子操作)

}

注意:

只有拥有对象互斥锁的标记的线程,才能进入该对象加锁的同步方法中

线程退出同步方法时,会释放相应的互斥锁标记

4.死锁

        死锁:当第一个线程拥有A对象锁标记,并等待B对象锁标记,同时第二个线程拥有B对象锁标记,并等待A对象锁标记时,产生死锁

        一个线程可以同时拥有多个对象的锁标记,当线程阻塞时,不会释放已经拥有的锁标记,由此可能造成死锁

        出现原因:锁的嵌套

举例:吃饭时需要一双筷子,现在有两人一人分一根筷子,如何完成吃饭

定义锁的类型

public class LockObject {
    public static Object a=new Object();
    public static Object b=new Object();
}

定义Boy,Girl类

public class Boy extends Thread{
    @Override
    public void run() {
        synchronized (LockObject.a){
            System.out.println(Thread.currentThread().getName()+"获取一根筷子a");
            synchronized (LockObject.b){
                System.out.println(Thread.currentThread().getName()+"获取一根筷子b");
                System.out.println("可以吃饭了");
            }
        }
    }
}
public class Girl extends Thread{
    @Override
    public void run() {
        synchronized (LockObject.b){
            System.out.println(Thread.currentThread().getName()+"获取一根筷子b");
            synchronized (LockObject.a){
                System.out.println(Thread.currentThread().getName()+"获取一根筷子a");
                System.out.println("可以吃饭了");
            }
        }
    }
}

定义测试类

public class DeadLock {
    public static void main(String[] args) {

        Boy b=new Boy();
        Girl g=new Girl();

        b.setName("Tom");
        g.setName("Marry");

        b.start();
        g.start();
    }
}

5.线程通信

两者都不是在Thread类中,而是在Object类中

等待:

public final void wait()

public final void wait(long timeout)

必须在对obj加锁的同步代码块中。在一个线程中,调用obj.wait()时,此线程会释放其拥有的所有锁标记。同时此线程阻塞在o的等待队列中。释放锁,进入等待队列。

通知:

public final void notify()

public final void notifyAll()

唤醒等待队列中线程,进入就绪队列,参与CPU的竞争

举例:男友存钱,女友取钱问题,必须先存才可以取钱

银行卡类

public class BankCard {
    private double balance;
    //false代表没有钱  true代表有钱
     private boolean flag=false;

    public double getBalance() {
        return balance;
    }

    public void setBalance(double balance) {
        this.balance = balance;
    }
    //存钱
    public synchronized void save(double money){
        if(flag==true){
            //进入等待队列
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //进入存钱
        this.balance=this.balance+money;
        System.out.println(Thread.currentThread().getName()+"存入"+money+",余额为:"+this.balance);
        flag=true;
        //唤醒等待队列的线程
        this.notify();
    }
    //取钱
    public synchronized void take(double money){

        if(flag==false){
            //进入等待队列
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //进入取钱
        this.balance=this.balance-money;
        System.out.println(Thread.currentThread().getName()+"取出"+money+",余额为:"+this.balance);
        flag=false;
        //唤醒等待队列的线程
        this.notify();
    }
}

定义男,女主

public class Boy extends Thread{
    private BankCard bankCard;

    public Boy(BankCard bankCard){
        this.bankCard=bankCard;
    }

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            bankCard.save(1000);
        }
    }
}
public class Girl extends Thread{
    private BankCard bankCard;

    public Girl(BankCard bankCard){
        this.bankCard=bankCard;
    }

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            bankCard.take(1000);
        }
    }
}

定义测试类

public class MoneyTest {
    public static void main(String[] args) {
        BankCard bankCard=new BankCard();
        Boy b=new Boy(bankCard);
        Girl g=new Girl(bankCard);
        b.setName("Tom");
        g.setName("Marry");
        b.start();
        g.start();
    }
}

6.线程池

6.1 何为线程池

出现的问题:

(1)线程是宝贵的内存资源、单个线程约占1MB空间,过多分配易造成内存溢出。

(2)频繁的创建及销毁线程会增加虚拟机回收频率、资源开销,造成程序性能下降。

线程池:

(1)线程容器,可设定线程分配的数量上限。

(2)将预先创建的线程对象存入池中,并重用线程池中的线程对象。

(3)避免频繁的创建和销毁。

6.2 常用方法

        Executor:线程池的根类。里面有一个方法。execute执行线程任务的方法Runnable类型的任务

        ExecutorService:线程池的子接口

        shutdown():关闭线程池。需要等待线程池中任务执行完毕后才会关闭

        shutdownNow():立即关闭线程池

        isTerminated():判断线程池是否 终止了

        submit():提交任务给线程池中线程对象。Runable和Callable类型的任务

6.3 线程池方法的使用

public class CreatMethods {
    public static void main(String[] args) {
        //单一线程池 使用场景:队列要求线程有序执行
        //ExecutorService executorService = Executors.newSingleThreadExecutor();

        //创建固定长度的线程池对象
        //ExecutorService executorService = Executors.newFixedThreadPool(3);

        //创建可变长度的线程池 不会根据i输出数字一致,如果是100可能就会创建40多个
        //ExecutorService executorService = Executors.newCachedThreadPool();

        //创建延迟线程池对象
        ScheduledExecutorService executorService = Executors.newScheduledThreadPool(3);
//前三个方法使用
//        for (int i = 0; i < 5; i++) {
//            executorService.submit(new Runnable() {
//                @Override
//                public void run() {
//                    System.out.println(Thread.currentThread().getName()+"=========================");
//                }
//            });
        for (int i = 0; i < 100; i++) {
            executorService.schedule(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName()+"=========================");
                }
            },10, TimeUnit.SECONDS);
        }
        executorService.shutdown();
    }
}

6.4 通过自定义阻塞队列创建线程池

        上面讲解的使用Executors创建线程池的方式,都是使用底层ThreadPoolExecutor,而阿里开发手册,建议使用最原始的方式。

        线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

*  int corePoolSize,核心线程数
*  int maximumPoolSize,最大线程数
*  long keepAliveTime,空闲时间
*  TimeUnit unit,时间单位
*  BlockingQueue<Runnable> workQueue 堵塞队列
public class CreatMethodsTest {
    public static void main(String[] args) {
        //LinkedBlockingDeque:可以设置等待的个数,如果不设置默认为Integer的最大值
        BlockingQueue blockingQueue=new LinkedBlockingDeque(3);
        ThreadPoolExecutor threadPoolExecutor=new ThreadPoolExecutor(2,5,10, TimeUnit.SECONDS,blockingQueue);
        for (int i = 0; i < 5; i++) {
            threadPoolExecutor.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName()+"---------");
                }
            });
        }
        threadPoolExecutor.shutdown();
    }
}

7.创建线程的第三种方式

        实现Callable接口,它和实现Runnable接口差不多,只是该接口种的方法有返回值和异常抛出。 比较适用于大文件的上传。

举例:进行1-100的值相加

public class CallableTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {

        //自创建线程对象并提交Callable类型的任务是比较麻烦的,需要封装到一个FutureTask类种
        //比较麻烦,建议使用线程池
//        My task=new My();
//        FutureTask futureTask=new FutureTask(task);
//        Thread t1=new Thread(futureTask);
//        t1.start();
//        System.out.println(futureTask.get());

        My t=new My();
        My2 t2=new My2();

        ExecutorService executorService = Executors.newFixedThreadPool(5);

        Future<Integer> submit = executorService.submit(t);
        Future<Integer> submit2 = executorService.submit(t2);

        Integer integer = submit.get();//需要等待线程执行完毕后,才会把结果返回该变量
        Integer integer2 = submit2.get();//需要等待线程执行完毕后,才会把结果返回该变量

        System.out.println(integer+integer2);
        executorService.shutdown();
    }
}
class My implements Callable<Integer>{

    @Override
    public Integer call() throws Exception {
        int sum=0;
        for (int i = 0; i <=100; i++) {
            sum+=i;
        }
        return sum;
    }
}
class My2 implements Callable<Integer>{

    @Override
    public Integer call() throws Exception {
        int sum2=0;
        for (int i = 0; i <=100; i++) {
            sum2+=i;
        }
        return sum2;
    }
}

8.手动锁

举例:四个窗口买票

8.1 Thread方法手动锁

public class LockTest {
    public static void main(String[] args) {
        Lock lock = new ReentrantLock();
        TicketTest ticketTest=new TicketTest();
        ticketTest.setMyLock(lock);//注意此处的锁,每new一次都会有一把新锁,要使用同一把锁
        ticketTest.setName("窗口A");
        TicketTest ticketTest1=new TicketTest();
        ticketTest1.setMyLock(lock);
        ticketTest1.setName("窗口B");
//        TicketTest ticketTest2=new TicketTest();
//        ticketTest2.setName("窗口C");
//        TicketTest ticketTest3=new TicketTest();
//        ticketTest3.setName("窗口D");

        ticketTest.start();
        ticketTest1.start();
//        ticketTest2.start();
//        ticketTest3.start();
    }
}
class TicketTest extends Thread{
    private static int ticket=100;
    //定义手动锁
    Lock myLock;

    public Lock getMyLock() {
        return myLock;
    }

    public void setMyLock(Lock myLock) {
        this.myLock = myLock;
    }

    @Override
    public void run() {

        try{myLock.lock();//上锁
            while (true){
                    if(ticket>0){
                        System.out.println(Thread.currentThread().getName()+"正在卖第"+--ticket+"张");
                    }else {
                        break;
                    }
                }

            }finally {
            myLock.unlock();//开锁
         }
    }
}

8.2 Runnable方法手动锁

public class RunnableLock {
    public static void main(String[] args) {
        RunnableTest runnableTest=new RunnableTest();

        Thread t1=new Thread(runnableTest,"窗口A");
        Thread t2=new Thread(runnableTest,"窗口B");
        Thread t3=new Thread(runnableTest,"窗口C");
        Thread t4=new Thread(runnableTest,"窗口D");
        t1.start();
        t2.start();
        t3.start();
        t4.start();

    }
}
class RunnableTest implements Runnable{

    private static int ticket=100;

    Lock myLock=new ReentrantLock();
    @Override
    public void run() {
            while(true){
              try{
                // myLock.lock();//要是放在while(true)上边,相当于对整个while锁住,只能有一个窗口进入
                  //boolean b=myLock.tryLock();//查看是否获取锁资源
                myLock.lock();
                if(ticket>0){
                    System.out.println(Thread.currentThread().getName()+"正在卖第"+ticket--+"张票");
                }else {
                    System.out.println(Thread.currentThread().getName()+"正在卖第"+ticket+"张票");
                    break;
                }
            }finally {
             myLock.unlock();
           }
        }
    }
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值