java线程、线程池

Thread线程

进程与线程

  1. 进程:进程就是操作系统运行的一个程序

  2. 线程:线程就是进程的一个执行单元,一条执行路径

     如启动360安全卫士就是打开一个进程, 它的电脑体检/木马查杀/系统清理等就是这个进程的几个执行单元, 每个执行单元就是一个线程
     迅雷就是一个进程, 可以同时下载多部电影, 每一部电影的下载就是一条执行路径,就是迅雷进程的一个线程
     一个进程至少有一个线程, 如果进程有多个线程,则它就是多线程应用程序
     每个线程都有独立的栈空间
    
  3. java线程栈是相互独立的,每个线程都有自己的栈空间,共享堆区和方法区

  4. 主线程
    JVM启动主线程,主线程运行main方法

  5. 用户线程
    开启的新的线程, 也称子线程

  6. 守护线程
    守护线程是为其他线程提供服务的线程,不能独立运行,当JVM中只有守护线程时,JVM会退出。垃圾回收器就是一个守护线程

线程的五种状态:

  1. 新建状态
    线程被创建后处于新建状态
  2. 就绪状态
    新建状态的线程调用start()进入就绪状态
    阻塞状态的线程接触阻塞后进入就绪状态
  3. 运行状态
    处于就绪状态的线程获得CPU的资源(时间片),该线程进入运行状态
  4. 阻塞状态
    是一个正在运行的线程因为一些原因让出CPU资源暂时中止而进入的状态
  5. 终止状态
    1)正常中止
    2)强制终止:stop()、destroy()
    3)异常终止:当线程执行过程中产生异常,线程就会中止
getState()状态注释
NEW状态是指线程刚创建, 尚未启动
RUNNABLE状态是线程正在正常运行中, 当然可能会有某种耗时计算/IO等待的操作/CPU时间片切换等, 这个状态下发生的等待一般是其他系统资源,而不是锁, Sleep等
BLOCKED这个状态下, 是在多个线程有同步操作的场景, 比如正在等待另一个线程的synchronized 块的执行释放, 或者可重入的 synchronized块里别人调用wait() 方法, 也就是这里是线程在等待进入临界区
WAITING这个状态下是指线程拥有了某个锁之后, 调用了他的wait方法, 等待其他线程/锁拥有者调用 notify / notifyAll 一遍该线程可以继续下一步操作, 这里要区分 BLOCKED 和 WATING 的区别, 一个是在临界点外面等待进入, 一个是在理解点里面wait等待别人notify, 线程调用了join方法 join了另外的线程的时候, 也会进入WAITING状态, 等待被他join的线程执行结束
TIMED_WAITING这个状态就是有限的(时间限制)的WAITING, 一般出现在调用wait(long), join(long)等情况下, 另外一个线程sleep后, 也会进入TIMED_WAITING状态
TERMINATED这个状态下表示 该线程的run方法已经执行完毕了, 基本上就等于死亡了(当时如果线程被持久持有, 可能不会被回收)
  • 状态之间的转换【线程的生命周期】
    在这里插入图片描述

创建线程

重写的public void run(){}方法,run方法的代码就是子线程执行的代码

  1. 继承Thread类创建线程

    class MyThread extends Thread{
        public MyThread(String name){
            super(name);
        } //调用父类构造方法取名,可不写
        public void	run(){
        //启用线程执行的内容
        //Thread.sleep(毫秒数):使线程休眠
        }
    }
    //main方法中开启线程
    MyThread mt = new MyThread();
    mt.start();
    

    注意:
    A:直接调用run()方法相当于调用普通方法会在主线程栈里分配空间,调用start()方法则会 创建 子线程栈区 ,在此栈区执行run()方法
    B:当接口的实现类只使用一次时, 或者抽象类的子类只使用一次时, 就不需要单独的再定义一个类,可以直接使用匿名内部类

    使用匿名内部类

    Thread thread = new Thread() {
        @Override
        public void run() {
        //启用线程执行的内容
        }
    };
    thread.start();
    
  2. 实现Runnable接口创建,以后主要是以此方法,灵活

    class MyThread implements Runnable{
        public void	run(){
            //启用线程执行的内容
            //Thread.sleep(毫秒数):使线程休眠
        }
    }
    //main方法中开启线程
    MyThread mt = new MyThread();
    Thread t = new Thread(mt);
    Thread t = new Thread(mt,"线程名"); //为线程取名
    t.start();
    

    使用匿名内部类

    Thread thread1 = new Thread(new Runnable() {
    @Override
    public void run() {
    //启用线程执行的内容
    }
        },"线程名");
    thread1.start();
    
  3. 实现Callable接口,主线程可以获取子线程执行结果(返回值)一般用于线程池当中,即在线程执行完毕后不释放线程,而存放于线程池中,当新任务到来后由这个空闲线程执行,省去了创建/释放线程的时间

    • 实现Callable接口和Runnable接口区别
      Callable接口中的call()有返回值, 通过Callable泛型 指定返回值的类型
      Runnable接口中的run()方法没有返回值 , Callable接口的call()有返回值

    可以获得子线程返回的结果实现Callable接口的类实例.get();

    class  MyThread  implements   Calllable<返回值类型>  {
        public  Integer  call()    throws   Exception{
            //子线程执行的代码
            return   返回值;
        }
    }
    //FutureTask实现了RunnableFuture<V>接口,该接口继承了Runnable接口, FutureTask类就是Runnable接口实现类
    FutureTask<Integer>  task = new FutureTask<>(  new  MyThread()  );
    Thread  t3 = new  Thread( task );
    t3.start();
    //可以获得子线程返回的结果
    task.get()
    

线程常用方法

返回值方法描述
static intactiveCount()当前活动线程的数量
static ThreadcurrentThread()返回当前线程
ClassLoadergetContextClassLoader()线程的上下文类加载器.
longgetId()返回线程的ID,每个线程都有唯一的id.
StringgetName()返回线程名称
intgetPriority()返回线程优先级
Thread.StategetState()返回线程状态
voidinterrupt()中断线程
static booleaninterrupted()测试线程是否被中断
booleanisAlive()测试线程是否结束
booleanisDaemon()是否守护线程.
booleanisInterrupted()测试线程是否被中断.
voidjoin()线程合并(加入)
voidrun()
voidsetDaemon(boolean on)设置线程为守护线程.
voidsetName(String name)设置线程名称
voidsetPriority(int newPriority)设置优先级
static voidsleep(long millis)线程休眠
voidstart()开启线程
voidstop()终止线程
StringtoString()
static voidyield()线程让步

多线程并发执行

java对于线程启动后唯一能保证的是每个线程都被启动且结束。但对于哪个线程先执行,什么时候执行,是没有保证的

Java优先级

Java中优先级高的有更大可能获得CPU,但不是优先级高的总是先执行,也不是优先级低的总不执行。

优先级从小到大为1~10;默认优先级5最大优先级为Thread.MAX_PRIORITY; 最小优先级为Thread.MIN_PRIORITY

获得优先级:线程对象.getPriority();

设置优先级:线程对象.setPriority();

线程调度的五个方法

  1. 线程休眠(静态方法): sleep(毫秒数); sleep(毫秒数,纳秒数)
    静态方法, 通过类名直接调用
    睡眠的单位是毫秒
    sleep()有受检异常需要预处理
    sleep()方法所在的线程睡眠

  2. 线程中断: interrupt()
    注意:interrupt()是非静态方法需要 线程对象.interrupt() 来调用
    一般情况下,是把处于睡眠/等待中的线程给中断,强制使其 立即结束 睡眠/等待 状态,恢复运行
    强制性,会发生异常,记得处理异常(如,不让他在控制台输出异常信息)

  3. 线程让步(静态方法):yield()
    将线程转换为就绪状态
    注意:调用此方法的线程,释放资源,让出CPU使用权,但是接下来这个线程仍然会和其他线程竞争CPU资源

  4. 线程合并【又名线程加入,线程挂起】(静态方法): join()
    谁调用谁等待,谁先等待谁最后被激活;A.join()方法使调用此方法的线程等待,直到线程A完成,此线程转换为就绪状态

    例如,三个线程,主线程调用A.join(),主线程等待,并不影响AB两个线程,AB间继续竞争CPU资源,此时,若A线程调用B.join()此时,A线程等待,等待B线程终止,A线程将转换为就绪状态,直至A线程中止,主线程将转换为就绪状态

  5. 线程中止
    stop(); 非静态方法,方法过时,不安全,不建议再使用

    建议使用此方法
    想办法让run()运行结束;即在线程中设置一个标志, 定期判断这个标志是否发生变化 , 标志发生变化就退出run()方法
    注意:如果线程处于冻结状态(wait()),由于run方法同步,无法读取标记,无法退出,需要进行线程中断(interrupt()),强制使其结束睡眠/等待状态,恢复执行权

线程同步

  1. 线程安全问题

    当多个线程同时操作堆区或者方法区的某个数据时, 可能会出现数据不一致 的现象, 称为线程安全问题

  2. 出现线程安全问题怎么办?

    A、每个线程都 访问自己的局部变量

    B、如果多个线程必须同时操作实例变量/静态变量时, 可以采用线程同步技术

  3. 线程同步技术解决什么问题?

    当一个线程在操作期间,不允许其他的线程加入

    某一段代码在某一时刻只能由一个线程执行

  4. 如何同步?
    语法:

    synchronized(  锁对象  ) {
    同步代码块
    }
    
    • 工作原理:

      1. 线程要执行同步代码块, 必须先获得锁对象
      2. 任意对象都可以作为锁对象, 每个对象有一个内置锁
      3. 某一时刻, 锁对象最多只能被一个线程持有
      4. 如果线程获得了锁对象后,会一直持有, 直到执行完同步代码块后才释放
    • 场景描述: 假设有线程A和线程B两个线程都想要执行同步代码块

      1. 线程A获得CPU执行权, 获得了锁对象后, 开始执行同步代码块
      2. 线程A在执行同步代码块期间, CPU执行权被线程B抢走了, 线程A转为就绪状态
      3. 线程B获得CPU执行权, 也想要执行同步代码块, 必须先获得锁对象, 现在锁对象被线程A持有, 线程B转到等待锁对象池中进行 阻塞
      4. 线程A重新获得CPU执行权, 执行完同步代码块后释放锁对象
      5. 等待锁对象池中的线程B获得了锁对象,转为就绪状态
    • 线程一旦获得对象锁,则其他线程不能再执行被锁对象的其他任何同步方法。只有再同步方法执行完毕之后释放锁,其他线程才能执行。

  5. 同步代码块

  • 同步代码块想要实现同步,必须使用同一个锁对象;只要使用了同一个锁对象的同步代码块就可以实现同步
    • 经常定义一个常量 对象作为锁对象 private static final Object OBJ = new Object();
    • 有时使用this对象作为锁对象,调用该方法的对象就是this对象,不能用于静态方法的同步代码块
    • 有时也使用当前类的运行时类对象作为锁对象, 如 当前类名.class, 有人称它为类锁
  1. 同步方法

    • 直接使用synchronized修饰的方法, 把整个方法体都作为同步代码块
    • 同步实例方法,修饰实例方法, 默认的锁对象是this对象
    • 同步静态方法,修饰静态方法, 默认的锁对象是当前类的运行时类, 即 当前类名.class
  2. 死锁
    在线程同步时, 由于获得锁对象的顺序不一致导致线程出现相互等待的情况,称为死锁
    以下代码会出现死锁

    public class Test extends Thread {
           @Override
            public void run() {
            if ("a".equals(Thread.currentThread().getName())) {
                synchronized ("资源1") {
                    System.out.println("线程a获得了资源1, 还想获得资源2");
                    synchronized ("资源2") {
                        System.out.println("线程a同时获得资源1与资源2,可以做爱做的事");
                    }
                }
      }
    
            if ("b".equals(Thread.currentThread().getName())) {
                synchronized ("资源2") {
                    System.out.println("线程b获得了资源2, 还想获得资源1");
                    synchronized ("资源1") {
                        System.out.println("线程b同时获得资源1与资源2,可以做爱做的事");
                    }
                }
            }
        }
    }
    
    • 如何避免出现死锁?

    保证获得 锁对象的顺序一致即可

生产者消费者设计模式

  • 设计模式就是别人总结的一套解决方案,这大解决方案被大多数人熟知与认可.

  • 生产者消费者设计模式解决了数据的平衡问题

    • 生产者线程类

      public class ProducerThread extends Thread {
          private MyStorage storage; 			//仓库	
          public ProducerThread(MyStorage storage) {
              super();
              this.storage = storage;
          }
          @Override
          public void run() {
              //每个月工作30天,每天存1次
              for( int i = 1;  i <=  30 ; i++){
                  String product = "产品编号:" + new Random().nextInt(100);
                  storage.store(product);
              }
          }
      }
      
    • 定义仓库类

      public class MyStorage {
            LinkedList<Object> list = new LinkedList<>(); 		//定义集合,作为存储产品的容器
            vprivate static final int MAX_CAPACITY = 100;
      
            //向仓库中存储产品 
            public synchronized void store(String  product) {
                  //如果仓库已满 ,生产者等待
                  while ( list.size() > MAX_CAPACITY) {
                        try {
                              this.wait(); 		//wait()/notify()这两个需要在同步代码块中,通过锁对象调用
                        } catch (InterruptedException e) {
                              e.printStackTrace();
                        }
                  }
                  System.out.println( Thread.currentThread().getName() + "+++++++++存储了:" + product);
                  list.offer(product);	 	//把产品 存储到后面
                  //通知消费者消费
                  this.notifyAll();
            }
      
            //从仓库中取产品 
            public synchronized void take() {
            //判断仓库是否已空, 当仓库为空时,需要等待
                  while ( list.size() <= 0 ) {
                        try {
                              this.wait();
                        } catch (InterruptedException e) {
                              e.printStackTrace();
                        }
                  }
                  Object product = list.poll(); 		//把前面的产品取出来
                  System.out.println( Thread.currentThread().getName() + " ------消费了:" + product);
                  //通知生产者继续生产
                  this.notifyAll();
            }
      }
      
    • 消费者线程类

      package com.thread;
      /**
       * 消费者线程类
       *
       */
      public class ConsumerThread  extends Thread{
      	private MyStorage storage; 			//仓库
      	
      	public ConsumerThread(MyStorage storage) {
      		super();
      		this.storage = storage;
      	}
      
      	@Override
      	public void run() {
      		//从仓库中取产品
      		for( int i = 1; i <=30 ; i++){
      			storage.take();
      		}
      	}
      }
      
    • 主类

      package com.thread;
      /**
       * 测试生产者消费者
       * 线程通信
       *
       */
      public class Test17 {
      
      	public static void main(String[] args) {
      		//先创建仓库
      		MyStorage myStorage = new MyStorage();
      		
      		//创建三个生产者线程模拟三个人
      		ProducerThread p1 = new ProducerThread(myStorage);
      		ProducerThread p2 = new ProducerThread(myStorage);
      		ProducerThread p3 = new ProducerThread(myStorage);
      		p1.setName("lisisi");
      		p2.setName("wangwu");
      		p3.setName("chenqi");
      		p1.start();
      		p2.start();
      		p3.start();
      		
      		//创建三个消费者
      		ConsumerThread c1 = new ConsumerThread(myStorage);
      		ConsumerThread c2 = new ConsumerThread(myStorage);
      		ConsumerThread c3 = new ConsumerThread(myStorage);
      		c1.setName("吃货1号");
      		c2.setName("吃货2号");
      		c3.setName("吃货3号");
      		c1.start();
      		c2.start();
      		c3.start();
      	}
      
      }
      

在这里插入图片描述

线程通信

  1. 锁池和等待池
    A: 锁池:假设线程A已经拥有了某个对象(注意:不是类)的锁,而其它的线程想要调用这个对象的某个synchronized方法(或者synchronized块),由于这些线程在进入对象的synchronized方法之前必须先获得该对象的锁的拥有权,但是该对象的锁目前正被线程A拥有,所以这些线程就进入了该对象的锁池中。
    B: 等待池:假设一个线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁后,进入到了该对象的等待池中

  2. 线程在运行的时候,如果发现某些条件没有被满足,可以调用wait方法暂停自己的执行,并且放弃已经获得的锁,然后进入等待状态。当该线程被其他线程唤醒并获得锁后,可以沿着之前暂停的地方继续向后执行,而不是再次从同步代码块开始的 地方开始执行。但是需要注意的一点是,对线程等待的条件的判断要使用while而不是if来进行判断。

  3. wait与notify(随机唤醒一个 wait线程)/notifyAll(唤醒所有 wait 线程) 方法必须在同步代码块中使用,通过锁对象调用;

  4. 当线程执行wait()时,会把当前的锁释放,然后让出CPU,进入等待状态。那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁;当执行notify/notifyAll方法时,被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。也就是说,调用了notify后只有一个线程会由等待池进入锁池,而notifyAll会将该对象等待池内的所有线程移动到锁池中,等待锁竞争优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁,它还会留在锁池中,而竞争到对象锁的线程则继续往下执行,直到执行完了 synchronized 代码块,它会释放掉该对象锁,这时锁池中的线程会继续竞争该对象锁。

  5. wait 和 sleep 区别
    A:wait 可以指定最长等待时间,也可以不指定;sleep 必须指定时间
    B:在同步中时,对cpu的执行权和锁的处理方式不同
    wait 释放cpu执行权,释放锁。
    sleep 释放cpu执行权,不释放锁。


创建两个线程, 一个线程打印奇数,一个线程打印偶数,实现交替打印

  • 主类

    public class Test {
        public static void main(String[] args) {
          // TODO 自动生成的方法存根
          Test18_1 t = new Test18_1();
          new Thread(new Runnable() {
            @Override
            public void run() {
              // TODO 自动生成的方法存根
              int i = 0;
              while(i++ < 50)
                t.print1();
            }
          },"打印单数").start();
    
          new Thread(new Runnable() {
            @Override
            public void run() {
              // TODO 自动生成的方法存根
              int i = 0;
              while(i++ < 50)
                t.print2();
            }
          },"打印双数").start();
        }
    }
    
  • 打印类

    public class Test_print{
        private int num = 0;
        public synchronized void print1() {
          while(num%2 == 0) {
            try {
              this.wait();
            } catch (InterruptedException e) {
              // TODO 自动生成的 catch 块
              e.printStackTrace();
            }
          }
          System.out.println(Thread.currentThread().getName()+"----"+num++);
          this.notifyAll();
        }
    
        public synchronized void print2() {
          while(num%2 == 1) {
            try {
              this.wait();
            } catch (InterruptedException e) {
              // TODO 自动生成的 catch 块
              e.printStackTrace();
            }
          }
          System.out.println(Thread.currentThread().getName()+"------------"+num++);
          this.notifyAll();
        }
    }
    

守护线程

守护线程理解为后台线程,当虚拟机只剩下守护线程时Java虚拟机将会退出,无需手动结束
setDaemon(boolean on) 必须在执行start()前,设置线程为守护线程.

定时器类Timer

public class Test19 {

	public static void main(String[] args) throws InterruptedException {
		System.out.println( "main....begin....");
		
		//创建定时器对象
//		Timer timer = new Timer();
		Timer timer = new Timer( true ); 		//把定时器设置守护线程
		
//		timer.schedule(task, time); 		//在指定的时间time, 执行任务task
//		timer.schedule(task, delay);  		//延迟delay毫秒后, 执行task任务
//		timer.scheduleAtFixedRate(task, firstTime, period);  	//指定task任务第一次执行的时间, 以后每隔period毫秒再执行一次
		
		timer.schedule(new TimerTask() {
			@Override
			public void run() {
				System.out.println( new Date() );
			}
		}, 3000, 1000);
		
		//main线程睡眠10秒
		Thread.sleep(10000);
		System.out.println( "main....end....");
		
	}

}

线程池

线程池的思想

  • 我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题:

    如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。

  • 那么有没有一种办法使得线程可以复用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务?

    在Java中可以通过线程池来达到这样的效果。今天我们就来详细讲解一下Java的线程池。

线程池概念

线程池:其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。

由于线程池中有很多操作都是与优化资源相关的,我们在这里就不多赘述。我们通过一张图来了解线程池的工作原理:
在这里插入图片描述
合理利用线程池能够带来三个好处:

  1. 降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
  2. 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
  3. 提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。

线程池的使用

Java里面线程池的顶级接口是java.util.concurrent.Executor,但是严格意义上讲Executor 并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是 java.util.concurrent.ExecutorService

要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,很有可能配置的线程池不是较优的,因此在java.util.concurrent.Executors线程工厂类里面提供了一些静态工厂,生成一些常用的线程池。官方建议使用Executors工程类来创建线程池对象。

Executors类中有个创建线程池的方法如下:

  • public static ExecutorService newFixedThreadPool(int nThreads):返回线程池对象。(创建的是有界线程池,也就是池中的线程个数可以指定最大数量)

获取到了一个线程池ExecutorService 对象,那么怎么使用呢,在这里定义了一个使用线程池对象的方法如下:

  • public Future submit(Runnable task) :获取线程池中的某一个线程对象,并执行

    Future接口:用来记录线程任务执行完毕后产生的结果。

使用线程池中线程对象的步骤:

  1. 创建线程池对象。
  2. 创建Runnable接口子类对象。(task)
  3. 提交Runnable接口子类对象。(take task)
  4. 关闭线程池(一般不做)。

Runnable实现类代码:

public class MyRunnable implements Runnable {
    @Override
    public void run() {
    	System.out.println("我要一个教练");
    try {
    	Thread.sleep(2000);
    } catch (InterruptedException e) {
    	e.printStackTrace();
    }
    System.out.println("教练来了: " + Thread.currentThread().getName());
    System.out.println("教我游泳,交完后,教练回到了游泳池");
    }
}

线程池测试类:

public class ThreadPoolDemo {
    public static void main(String[] args) {
        // 创建线程池对象
        ExecutorService service = Executors.newFixedThreadPool(2);//包含2个线程对象
        // 创建Runnable实例对象
        MyRunnable r = new MyRunnable();
        //自己创建线程对象的方式
        // Thread t = new Thread(r);
        // t.start(); ‐‐‐> 调用MyRunnable中的run()
        // 从线程池中获取线程对象,然后调用MyRunnable中的run()
        service.submit(r);
        // 再获取个线程对象,调用MyRunnable中的run()
        service.submit(r);
        service.submit(r);
        // 注意:submit方法调用结束后,程序并不终止,是因为线程池控制了线程的关闭。
        // 将使用完的线程又归还到了线程池中
        // 关闭线程池
        //service.shutdown();
    }
}

Callable测试代码:

  • Future submit(Callable task) : 获取线程池中的某一个线程对象,并执行.

    Future : 表示计算的结果.

  • V get(): 获取计算完成的结果

public class ThreadPoolDemo2 {
    public static void main(String[] args) throws Exception {
        // 创建线程池对象
        ExecutorService service = Executors.newFixedThreadPool(2);//包含2个线程对象
        // 创建Runnable实例对象
        Callable<Double> c = new Callable<Double>() {
            @Override
            public Double call() throws Exception {
            return Math.random();
            }
        };
        // 从线程池中获取线程对象,然后调用Callable中的call()
        Future<Double> f1 = service.submit(c);
        // Futur 调用get() 获取运算结果
        System.out.println(f1.get());
        Future<Double> f2 = service.submit(c);
        System.out.println(f2.get());
        Future<Double> f3 = service.submit(c);
        System.out.println(f3.get());
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值