【Java SE】7. 多线程

7.1 程序、进程、线程

  1. 程序: 是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象
  2. 进程: 程序的一次执行过程,或是正在运行的一个程序。是一个动态的过程:有它自身的产生、存在和消亡的过程,进程作为资源分配的单位, 系统在运行时会为每个进程分配不同的内存区域
  3. 线程: 进程可进一步细化为线程,是一个程序内部的一条执行路径
    1. 若一个进程同一时间并行执行多个线程,就是支持多线程的
    2. 线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc),线程切换的开销小
    3. ⭐一个进程中的多个线程共享相同的内存单元/内存地址空间(方法区和堆空间) → \to 它们从同一堆中分配对象,可以访问相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程操作共享的系统资源可能就会带来安全的隐患
  4. 多线程的优点:
    1. 提高应用程序的响应。对图形化界面更有意义,可增强用户体验
    2. 提高计算机系统CPU的利用率
    3. 改善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和修改
  5. 何时需要多线程?
    1. 程序需要同时执行两个或多个任务。
    2. 程序需要实现一些需要等待的任务时,如用户输入、文件读写操作、网络操作、搜索等
    3. 需要一些后台运行的程序时

7.2 线程的创建和使用

  1. Java语言的JVM允许程序运行多个线程,它通过java.lang.Thread类来体现

  2. 多线程的创建:

    1. 方式一:继承于Thread类

      1. 创建一个继承于Thread类的子类

      2. 重写Thread类的run( )

      3. 创建Thread类的子类对象

      4. 通过此对象调用start( )

        class MyThread extends Thread{//1.创建一个继承于Thread类的子类
        
            //2.重写Thread类的run( )
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    if(i % 2 == 0){
                        System.out.println(i+"run()");
                    }
                }
            }
        }
        public class ThreadTest {
            public static void main(String[] args) {
                
                //3.创建Thread类的子类的对象
                MyThread myThread = new MyThread();
                
                //4.通过此对象调用start( ),start会去调run()
                myThread.start();
                
                //如何看出多线程同时进行
                for (int i = 0; i < 100; i++) {
                    if(i % 2 == 0){
                        System.out.println(i+"main()");
                    }
                }
                //两个遍历是同时进行的
            }
        }
        

        在这里插入图片描述

      5. 注意:

        1. start()作用:①启动当前线程②调用当前线程的run( ),如myThread.start();换成myThread.run();只是一个子类方法的调用,并不会启动线程,实际都是main()执行的

        2. 如果想在main()中再启动一个线程,需要再new一个MyThread对象

          class MyThread extends Thread{
              @Override
              public void run() {
                  for (int i = 0; i < 100; i++) {
                      if(i % 2 == 0){
                          System.out.println(i+Thread.currentThread().getName());
                      }
                  }
              }
          }
          public class ThreadTest {
              public static void main(String[] args) {
                  MyThread myThread1 = new MyThread();
                  myThread1.start();
          
                  MyThread myThread2 = new MyThread();
                  myThread2.start();
              }
          }
          //部分结果:
          //78Thread-0
          //80Thread-0
          //46Thread-1
          //82Thread-0
          //48Thread-1
          //84Thread-0
          //86Thread-0
          
    2. 方式二:实现Runnable接口

      1. 卖票场景:

        class MyThread extends Thread{
        
            private int ticket = 100;
            @Override
            public void run() {
                while(true){
                    if(ticket > 0){
                        System.out.println(getName()+":卖票,票号为:"+ticket);
                        ticket--;
                    }else {
                        break;
                    }
                }
            }
        }
        public class ThreadTest {
            public static void main(String[] args) {
                MyThread myThread1 = new MyThread();
                myThread1.start();
                myThread1.setName("窗口1");
        
                MyThread myThread2 = new MyThread();
                myThread2.start();
                myThread2.setName("窗口2");
        
                MyThread myThread3 = new MyThread();
                myThread3.start();
                myThread3.setName("窗口3");
            }
        }
        //三个窗口会将同一张票卖三次
        //窗口1:卖票,票号为:100
        //窗口3:卖票,票号为:100
        //窗口3:卖票,票号为:99
        //窗口3:卖票,票号为:98
        //窗口1:卖票,票号为:99
        

        修改:将ticket设为静态

        private static int ticket = 100;
        

        如果不使用这种方式呢?

      2. 实现Runnable接口

        1. 步骤:

          1. 创建一个实现Runnable接口的类
          2. 实现类去实现Runnable中的抽象方法run( )
          3. 创建实现类的对象
          4. 将此对象作为参数传入Thread类的构造器中,创建Thread类的对象
          5. 通过Thread类的对象调用start( )方法
        2. 其实Thread也是Runnable的实现类,方式二只是绕过了Thread

        3. 示例:

          class MyThread1 implements Runnable{
              @Override
              public void run() {
                  for (int i = 0; i < 100; i++) {
                      if(i % 2 == 0){
                          System.out.println(i+Thread.currentThread().getName());
                      }
                  }
              }
          }
          public class ThreadTest2 {
              public static void main(String[] args) {
                  MyThread1 r = new MyThread1();
          
                  Thread t1= new Thread(r);
                  t1.start();
                  t1.setName("线程1");
          
                  Thread t2= new Thread(r);
                  t2.start();
                  t2.setName("线程2");
              }
          }
          
        4. 回到卖票的场景:

          class MyThread implements Runnable {
          
              private int ticket = 100;
              @Override
              public void run() {
                  while(true){
                      if(ticket > 0){
                          System.out.println(Thread.currentThread().getName()+":卖票,票号为:"+ticket);
                          ticket--;
                      }else {
                          break;
                      }
                  }
              }
          }
          public class ThreadTest {
              public static void main(String[] args) {
                  MyThread myThread = new MyThread();
          
                  //⭐只new了一个MyThread对象,故ticket之有一个
                  //不用加static了
                  Thread t1 = new Thread(myThread);
                  t1.start();
                  
                  Thread t2 = new Thread(myThread);
                  t2.start();
          
                  Thread t3 = new Thread(myThread);
                  t3.start();
              }
          }
          
  3. Thread类中的常用方法

    1. start():启动当前线程、调用当前线程的run( )

    2. run():通常需要重写Thread类中的此方法,将创建的线程要执行的操作声明在此方法中

    3. currentThread():静态方法,返回当前代码执行的线程

    4. getName():获取当前线程的名字

    5. setName():设置当前线程的名字

      public class ThreadTest {
          public static void main(String[] args) {
              MyThread myThread1 = new MyThread();
              myThread1.start();
              //给线程1命名
              myThread1.setName("线程一");
              
              //给主线程main命名
              Thread.currentThread().setName("线程二");
          }
      }
      
    6. yield():线程让步,暂停当前执行的线程,将执行机会让给其他线程

      class MyThread extends Thread{
          @Override
          public void run() {
              for (int i = 0; i < 100; i++) {
                  if(i % 2 == 0){
                      System.out.println(i+Thread.currentThread().getName());
                  }
                  if(i % 20 == 0){
                    this.yield();			//让步
                  }
              }
          }
      }
      public class ThreadTest {
          public static void main(String[] args) {
              MyThread myThread1 = new MyThread();
              myThread1.start();
              myThread1.setName("线程一");
      
              MyThread myThread2 = new MyThread();
              myThread2.start();
              myThread2.setName("线程二");
          }
      }
      
      //40线程一
      //30线程二
      
      //40线程二
      //42线程一
      
    7. join() :在线程a中调用b的join(),此时将当前线程a阻塞,直到join()方法调入的线程b执行完成后,再执行被阻塞的线程a

      class MyThread extends Thread{
          @Override
          public void run() {
              for (int i = 0; i < 100; i++) {
                  if(i % 2 == 0){              System.out.println(i+Thread.currentThread().getName());
                  }
              }
          }
      }
      public class ThreadTest {
          public static void main(String[] args) {
              MyThread myThread1 = new MyThread();
              myThread1.start();
              myThread1.setName("线程一");
      
              for (int i = 0; i < 100; i++) {
                  if(i % 2 == 0){
                      System.out.println(i+Thread.currentThread().getName());
                  }
                  if(i == 20){
                      try {
                          Thread.currentThread().join();//插入
                      } catch (InterruptedException e) {
                          e.printStackTrace();
                      }
                  }
              }
          }
      }
      
      //16main
      //18main
      //20main
      //0线程一
      //2线程一
      //4线程一
      
    8. sleep()

      class MyThread extends Thread{
          @Override
          public void run() {
              for (int i = 0; i < 100; i++) {
                  if(i % 2 == 0){
                      try {//只能用try-catch,不能throws因为被重写的run()并没有throws
                          sleep(1000);//延迟
                      } catch (InterruptedException e) {
                          e.printStackTrace();
                      }
                      System.out.println(i+Thread.currentThread().getName());
                  }
              }
          }
      }
      
    9. suspend():挂起进程

    10. notify():唤醒进程

    11. resume():结束挂起得状态

    12. stop():强制线程生命期结束

    13. boolean isAlive():返回boolean,判断线程是否还活着

  4. 线程的优先级

    1. 线程的优先级等级

      1. MAX_PRIORITY: 10
      2. MIN _PRIORITY: 1
      3. NORM_PRIORITY: 5
    2. 如何获取和设置当前线程的优先级值

      1. getPriority():返回线程优先值
      2. setPriority(int newPriority):改变线程的优先级
    3. 注意:

      低优先级只是获得调度的概率低,并非一定是在高优先级线程之后才被调用

    4. 示例:

      class MyThread extends Thread{
          @Override
          public void run() {
              for (int i = 0; i < 100; i++) {
                  if(i % 2 == 0){
                      System.out.println(i+Thread.currentThread().getName()+Thread.currentThread().getPriority());//获取优先级
                  }
              }
          }
      }
      public class ThreadTest {
          public static void main(String[] args) {
              MyThread myThread1 = new MyThread();
              myThread1.start();
              myThread1.setPriority(Thread.MAX_PRIORITY);//设置
              myThread1.setName("线程一");
      
              MyThread myThread2 = new MyThread();
              myThread2.start();
              myThread2.setPriority(Thread.MIN_PRIORITY);//设置
              myThread2.setName("线程二");
          }
      }
      

7.3 线程的生命周期

  1. JDK用Thread.State类定义了线程的几种状态

    public enum State {
            
            NEW,
    
            RUNNABLE,
           
            BLOCKED,
            
            WAITING,
            
            TIMED_WAITING,
    
            TERMINATED;
    }
    
  2. 五种状态:

    1. 新建: 当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建
      状态
    2. 就绪: 处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已
      具备了运行的条件,只是没分配到CPU资源
    3. 运行: 当就绪的线程被调度并获得CPU资源时,便进入运行状态, run()方法定义了线
      程的操作和功能
    4. 阻塞: 在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中
      止自己的执行,进入阻塞状态
    5. 死亡: 线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束
  3. 状态转换:

在这里插入图片描述

7.4 线程的同步

  1. 问题的提出:

    1. 多个线程执行的不确定性引起执行结果的不稳定
    2. 多个线程对账本的共享,会造成操作的不完整性,会破坏数据
  2. 例子:创建三个卖票窗口,总票数为100张,使用实现Runnable接口的方式

    1. 问题:卖票的过程中,出现了冲票、错票(出现了线程的安全问题)

在这里插入图片描述

  1. 问题出现的原因:当某个线程操作车票的过程中,尚未操作完成时,其他线程参与进来,也操作车票

  2. 解决:对临界资源ticket上锁

  3. Java中使用同步机制来解决线程的安全问题:(mutex互斥信号量

    1. 方式一:同步代码块

      synchronized(同步监视器){
          需要被同步的代码(临界区:操作共享数据)
      }
      
      • 同步监视器:俗称 锁,任何一个类的对象都可以充当锁要求:多个线程必须要共用一把锁(唯一的对象),即对象声明在run()之外
        在这里插入图片描述
      class MyThread implements Runnable {
      
          private static int ticket = 100;
      
          Object obj = new Object();		//对象声明在run()之外
          
          @Override
          public void run() {
              while(true){
                  synchronized (obj) {	//同步代码块
                      if (ticket > 0) {
                          System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
                          ticket--;
                      } else {
                          break;
                      }
                  }
              }
          }
      }
      

      注意: 如果是使用继承Thread的方式,每个窗口都会造一个进程对象,故在使用同步代码块时,需要将同步监视器设为static

      private static int ticket = 100;
      
      static Object obj = new Object();
      

      简便写法:

      1. 实现Runnable接口方式

        synchronized (this)			//不用再new对象了
        
      2. 继承Thread类的方式

        synchronized (ThreadTest.class) //使用当前类
        
    2. 方式二:同步方法

      1. 如果操作共享数据的代码完整的声明在一个方法中,则可以将这个方法设置为同步的

      2. 同步方法仍然涉及到同步监视器,只是不需要显示声明

        1. 非静态的同步方法:同步监视器是:this
        2. 静态的同步方法:同步监视器是:当前类本身
      3. 实现Runnable接口方式

        class MyThread  implements Runnable {
        
            private static int ticket = 100;
        
            @Override
            public void run() {
                while(true){
                    show();
                }
            }
            
            //非静态的同步方法,同步监视器:this
            private  synchronized void show(){
                if (ticket > 0) {
                    System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
                    ticket--;
                }
            }
        }
        
      4. 继承Thread类的方式

        class MyThread  extends Thread {
        
            private static int ticket = 100;
        
            @Override
            public void run() {
                while(true){
                    show();
                }
            }
            
            //静态的同步方法,同步监视器:当前类
            private static synchronized void show(){
                if (ticket > 0) {
                    System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
                    ticket--;
                }
            }
        }
        
    3. 方式三:lock锁

      1. 步骤:

        1. 实例化ReentrantLock(ReentrantLock有两个构造方法:无参和有参,有参构造方法的形参是boolean fail,传入true时表示是公平的,不会出现刚使用完共享变量,弯回来继续占用的情况)
        2. 调用lock()
        3. 调用unlock()
      2. 示例:

        import java.util.concurrent.locks.ReentrantLock;
        
        class MyThread  implements Runnable {
        
            private static int ticket = 100;
        
            private ReentrantLock lock = new ReentrantLock();//1
        
            @Override
            public void run() {
                while(true){
                    try {
                        lock.lock();	//2
                        
                        if (ticket > 0) {
                            System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
                            ticket--;
                        }else {
                            break;
                        }
                    } finally {
                        lock.unlock();	//3
                    }
                }
            }
        }
        
      3. synchronized与lock的区别:

        1. synchronized机制在执行完相应的同步代码后,自动释放同步监视器
        2. lock需要手动启动同步lock( ),同时结束同步需要手动的实现unlock( )
  4. 线程安全的单例模式之懒汉模式:

    1. 同步代码块:

      public class Bank {
          
          private Bank() {
          }
          
          private static Bank bank = null;
      		
          public static Bank getInstance(){
              if(bank == null){
                  synchronized(Bank.class){
                      if(bank == null){
                      	return bank = new Bank(); 
                      }
                  }
              }
              return bank;
          }
      }
      
    2. 同步方法:

      public class Bank {
          
          private Bank() {
          }
          
          private static Bank bank = null;
      		
          public synchronized static Bank getInstance(){
              if(bank == null){    
                  return bank = new Bank();
              }
              return bank;
          }
      }
      
  5. 线程死锁问题:

    1. 不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁

    2. 出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续

    3. 死锁示例:

      public class DeadLock {
          public static void main(String[] args) {
              StringBuffer s1 = new StringBuffer();
              StringBuffer s2 = new StringBuffer();
      
              new Thread(){	//线程一
                  @Override
                  public void run(){
                      synchronized (s2){
                          s1.append("a");
                          try {
                              Thread.sleep(100);//增加死锁几率
                          } catch (InterruptedException e) {
                              e.printStackTrace();
                          }
                          synchronized (s1){
                              s2.append("b");
                              System.out.println(s1);
                              System.out.println(s2);
                          }
                      }
                  }
              }.start();
      
              new Thread(new Runnable() {  //线程二
                  @Override
                  public void run() {
                      synchronized (s1){
                          s1.append("c");
                          try {
                              Thread.sleep(100);
                          } catch (InterruptedException e) {
                              e.printStackTrace();
                          }
                          synchronized (s2){
                              s2.append("d");
                              System.out.println(s1);
                              System.out.println(s2);
                          }
                      }
                  }
              }).start();
          }
      }
      

7.5 线程的通信

通信示例:两个线程交替打印1-100(PV操作

package com.demo1;

class MyThread  implements Runnable {
    private static int number = 1;
    @Override
    public void run() {
        while (true){
            synchronized (this) {
                
                notify();//2. 线程2拿到锁后,唤醒线程1
                //notifyAll();//唤醒所有阻塞线程
                
                if(number<=100){
                    System.out.println(Thread.currentThread().getName()+":"+number);
                    number++;

                    try {
                        //1. 线程1打印后,进入阻塞
                        wait(); //wait()同时会释放锁
                        
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                }else {
                    break;
                }
            }
        }
    }
}
public class ThreadTest {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();

        Thread thread1 = new Thread(myThread);
        Thread thread2 = new Thread(myThread);

        thread1.start();
        thread2.start();
    }
}
  • wait() notify() notifyAll()只能在同步代码块或同步方法中使用

  • wait() notify() notifyAll()必须是同步监视器来调用

    this.notify();//法1
    obj.notify();//法2:新new一个对象
    
  • sleep()wait()的异同:

    • 同:一旦执行方法,都可以使当前线程进入阻塞状态
    • 异:
      1. 声明位置不同:Thread类中声明sleep( ),Object类中声明wait( )
      2. 调用的要求不同:sleep( )可以在任何情况下使用,wait( )必须使用在同步代码块或同步方法中
      3. 是否释放同步监视器:如果都使用在同步代码块或同步方法中时,sleep( )不会释放,wait( )会释放

7.6 生产者消费者问题

package com.demo1;
class Clerk{

    private int productNum = 0;

    public synchronized void produceProduct(){//⭐互斥信号量
        if(productNum < 20){
            productNum++;
            notify();//⭐V产品
            System.out.println(Thread.currentThread().getName()+"开始生产第"+productNum+"个产品");
        }else {
            try {
                wait();//⭐P空位
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public synchronized void consumeProduct() {//⭐互斥信号量
        if(productNum > 0){
            System.out.println(Thread.currentThread().getName()+"开始消费第"+productNum+"个产品");
            productNum--;
            notify();//⭐V空位
        }else {
            try {
                wait();//⭐P产品
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
class Producer extends Thread{
    private Clerk clerk;

    public Producer(Clerk clerk) {
        this.clerk = clerk;
    }
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"开始生产产品");

        while(true){
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            clerk.produceProduct();
        }
    }
}
class Consumer extends Thread{
    private Clerk clerk;

    public Consumer(Clerk clerk) {
        this.clerk = clerk;
    }

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"开始消费产品");

        while(true){
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            clerk.consumeProduct();
        }
    }
}
public class product{
    public static void main(String[] args) {
        Clerk clerk = new Clerk();

        Producer p1 = new Producer(clerk);
        Consumer c1 = new Consumer(clerk);

        p1.setName("消费者");
        p1.start();

        c1.setName("消费者");
        c1.start();
    }
}

7.7 JDK5.0 新增线程创建方式:Callable和线程池

  1. 新增方式一:实现Callable接口

    1. 与使用Runnable相比, Callable功能更强大些

      1. 相比run( ),call( )可以有返回值
      2. call( )可以抛出异常
      3. 支持泛型的返回值
      4. 需要借助FutureTask类,比如获取返回结果
    2. 步骤

      1. 创建一个实现Callable的实现类
      2. 实现call( ),将此线程需要执行的操作生命在方法中
      3. 创建Callable接口实现类的对象
      4. 将此实现类的对象作为参数传递到FutureTask构造器中,创建FutureTask对象
      5. 将FutureTask对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start( )
      6. 获取Callable中call( )的返回值
    3. 示例:

      import java.util.concurrent.Callable;
      import java.util.concurrent.ExecutionException;
      import java.util.concurrent.FutureTask;
      
      class MyThread  implements Callable {
      
          @Override
          public Object call() throws Exception {
              int sum = 0;
              for (int i = 1; i <= 100 ; i++) {
                  if(i % 2 == 0){
                      System.out.println(i);
                      sum += i;
                  }
              }
              return sum;
          }
      }
      public class ThreadTest {
          public static void main(String[] args) throws ExecutionException, InterruptedException {
              MyThread myThread = new MyThread();
      
              FutureTask future = new FutureTask(myThread);
      
              new Thread(future).start();
      
              //get()的返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值
              Object sum = future.get();
              System.out.println("总和为:"+sum);
      
          }
      }
      
  2. 新增方式二:使用线程池:

    1. 背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大

    2. 思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具

    3. 好处:

      1. 提高响应速度(减少了创建新线程的时间)
      2. 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
      3. 便于线程管理:
        1. corePoolSize:核心池的大小
        2. maximumPoolSize:最大线程数
        3. keepAliveTime:线程没有任务时最多保持多长时间后会终止
    4. 示例:

      package com.demo1;
      
      import java.util.concurrent.ExecutorService;
      import java.util.concurrent.Executors;
      import java.util.concurrent.ThreadPoolExecutor;
      
      class NumberThread1 implements Runnable {
      
          @Override
          public void run() {
              int sum = 0;
              for (int i = 0; i <= 100 ; i++) {
                  if(i % 2 == 0){
                      System.out.println(Thread.currentThread().getName()+":"+i);
                  }
              }
          }
      }
      class NumberThread2 implements Runnable {
      
          @Override
          public void run() {
              int sum = 0;
              for (int i = 0; i <= 100 ; i++) {
                  if(i % 2 != 0){
                      System.out.println(Thread.currentThread().getName()+":"+i);
                  }
              }
          }
      }
      public class threadPool  {
          public static void main(String[] args) {
              //提功指定线程数的线程池
              ExecutorService service = Executors.newFixedThreadPool(10);
              
              //设置线程池属性
              //ThreadPoolExecutor是ExecutorService的实现类
              //线程池的变量属性都在实现类中规定
              ThreadPoolExecutor service1 = (ThreadPoolExecutor) service; 
              service1.setCorePoolSize(15);
              service1.setMaximumPoolSize(30);
              
              //线程池的使用
              service.execute(new NumberThread1());//适合适用于Runnable
              //service.submit();//适合适用于Callable
              
              service.execute(new NumberThread2());
              
              service.shutdown();
          }
      }
      
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值