教你了解多线程

多线程

学习多线程之前,我们要先了解什么是线程,因为线程是依赖于进程存在的

1.进程

概述:正在运行的程序叫进程。进程是系统进行资源分配和调用的独立单位。每一个进程都有它自己的内存空间和系统资源。

多进程的意义

​ 单进程计算机只能最一件事,而现代的计算机都是多进程的,也就是可以同时做很多事情

举例:一边做笔记(typora),一边聊天,一边敲代码

​ 所以多进程的意义:同一时间可以同时进行多个任务,提高工作效率,并提高CPU利用率。

思考:

我电脑单核处理器,我一边做笔记,一边音乐,他们是同时进行的吗?

​ 不是,一个CPU在一个时间点上只能做一件事情,他在执行多个进程时,进行了程序间的高速切换,这个时间特别短,所以给我们的感官是进程是同时执行。

2.线程

在同一个进程中,可以执行多个任务,而每一个任务,就是一个线程

概述:

线程就是程序的执行单元,也是执行路径,线程也是程序使用CPU资源的基本单位。

单线程:

只有一条执行路径或一个执行单元

多线程:

有多条执行路径或多个执行单元。

多线程的意义:

1.线程的执行是抢占式的,他要去抢占CPU的资源,一个多线程在执行时,如果一些线程必须等待时,CPU资源就可以交给其他线程去使用,这样就提高了CPU使用率

2.对于进程来说,如果他是多线程的,在抢占CPU资源时,就有更大的几率抢占到CPU资源,提高CPU执行效率

3.在抢占CPU资源时,是具有随机性,我们不保证哪一个线程在哪一段时间可以抢到CPU资源

3.并行,并发

并行:

多个处理器或多个核处理器同时处理多个不同任务

并发:

一个处理器在一段时间内处理多个任务

举例:

1.并发相当于一个人同时吃3个馒头,并行相当于3个人分别吃一个馒头

2.我正在吃饭,突然来电话,但是我直到吃完饭,我才去接电话,就代表我不支持并发也不支持并行

我正在吃饭,突然来电话,我停止吃饭,接完电话,继续吃饭,我支持并发

我正在吃饭,突然来电话,我一边吃饭一边打电话,我支持并行(耳朵和嘴两个处理器)

4.如何实现多线程

​ 由于线程是依赖于进程存在的,所以首先我要创建一个进程,但是进程是由系统来创建的,所以我们需要调用系统某些功能来创建进程,但是Java不能调用系统功能,虽然Java不能调用系统功能,但是他可以调用C/C++写好的程序来创建进程,进而实现多线程。

​ Java将C/C++写好的代码封装到一个类中,然后通过这个类就可以调用创建进程,线程的这些功能,然后间接的实现多线程

Java提供了一个类Thread

通过ApI,我们知道创建线程有两种方式:

1.一种方法是将类声明为 Thread 的子类。该子类应重写 Thread 类的 run 方法。

2.创建线程的另一种方法是声明实现 Runnable 接口的类。该类然后实现 run 方法。

4.0创建线程方式1:

1.自定义线程MyThread类,继承Thread类

2.MyThread类重写run()方法

3.创建MyThread类对象

4.启动线程start();

package org.wdit.unit10.线程;
/* 创建多线程
* 1.自定义线程MyThread类,继承Thread类
   2.MyThread类重写run()方法
   3.创建MyThread类对象
  4.启动线程start();
  *
  * 线程重命名:1.setName(String name)
  * 2.有参构造;在子类中声名有参构造,并显示指定访问父类有参构造
  *
  * 获取当前执行的线程名:Thread.currentThread().getName()
* */
public class MyThread extends Thread{
    public MyThread(String name){
        super(name);
    }

    @Override
    public void run() {
        //线程一般是来执行比较耗时的工作
        for(int i=0;i<10;i++){
            System.out.println(getName()+":"+i);
        }
    }
}
class MyThreadDemo{
    public static void main(String[] args) {
        MyThread myThread=new MyThread("hx");
        MyThread myThread2=new MyThread("hx1");
        myThread.setName("hx:");
        myThread2.setName("hx1:");
       // myThread.run();普通方法调用
        //myThread2.run();
        myThread.start();//启动线程
        myThread2.start();
        System.out.println(Thread.currentThread().getName());
    }
}

4.1设置线程优先级

两种线程调度模型

1.分时调度:所有线程轮流使用CPU的使用权,平均分配每个线程占用的CPU时间片。

2.抢占式调度:优先让优先级高的线程使用CPU,如果线程的优先级相同,那么随机选择一个,优先级高的线程获取的CPU时间片相对多一些。

获取优先级的方法

public final int getPriority():返回此线程的优先级

设置优先级的方法

public final void setPriority(int newPriority)

**默认优先级:**5

优先级范围:

1~10

4.2线程控制-线程睡眠sleep()

public static void sleep(long millis)

package org.wdit.unit10.线程;
/*
* 线程睡眠
* sleep()在哪个线程中调用,阻塞那个方法
* */
public class SleepDemo {
    public static void main(String[] args) {
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        MyThread3 myThread3=new MyThread3("hx3");
        myThread3.start();

    }
}
 class MyThread3 extends Thread{
    public MyThread3(String name){
        super(name);
    }

    @Override
    public void run() {
        //线程一般是来执行比较耗时的工作
        for(int i=0;i<10;i++){
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(getName()+":"+i);
        }
    }
}

4.3线程控制-加入线程join()

public final void join(long mills)

该方法是让调用该方法的线程执行完毕后,再去执行其他线程,不设置毫秒值则等该线程执行完毕后,其他线程才开始执行

package org.wdit.unit10.线程;


 class MyThread4 extends Thread{
    public MyThread4(String name){
        super(name);
    }

    @Override
    public void run() {
        //线程一般是来执行比较耗时的工作
        for(int i=0;i<10;i++){
            try {
               Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(getName()+":"+i);
        }
    }
}
public class JoinDemo {
    public static void main(String[] args) {
        MyThread myThread=new MyThread("hx");
        MyThread myThread2=new MyThread("hx1");
        MyThread myThread3=new MyThread("hx2");
        System.out.println("程序开始");
        myThread.start();

        try {
            myThread.join(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        myThread2.start();
        myThread3.start();

    }}

4.4线程控制-线程礼让yield()

public static void yield():线程礼让可以让线程间的抢占趋于和平,就是你运行一下,我运行一下但仅仅是抢占不激烈,并不是每一个线程运行的次数和时间相同

package org.wdit.unit10.线程;

public class YieldDemo extends Thread{
    public YieldDemo(String name){
        super(name);
    }

    @Override
    public void run() {
        for(int i=0;i<100;i++){
            System.out.println(getName()+":"+i);
            Thread.yield();
        }
    }
}
class Demo{
    public static void main(String[] args) {
        YieldDemo s1=new YieldDemo("hx1");
        YieldDemo s2=new YieldDemo("hx2");
        YieldDemo s3=new YieldDemo("hx3");
        s1.start();
        s2.start();
        s3.start();
    }
}

4.5线程控制-后台(守护setDaemon)线程

public final void setDaemon(boolean on)

注意事项:

1.当线程只有守护线程,JVM会退出

2.在线程开始前设置守护线程

package org.wdit.unit10.线程;

class MyThtread extends Thread {
    public MyThtread(String name){
        super(name);
    }

    @Override
    public void run() {
        for(int i=0;i<100;i++){
            System.out.println(getName()+":"+i);

        }
    }
}
class MyThreadDemo5 {
    public static void main(String[] args) {
        MyThread myThread = new MyThread("亚索");
        MyThread myThread2 = new MyThread("劫");

        myThread.setDaemon(true);
        myThread2.setDaemon(true);

        myThread.start();
        myThread2.start();

        Thread.currentThread().setName("主基地");
        for (int i = 0; i < 3; i++) {
            System.out.println(Thread.currentThread().getName()+"---"+i);
        }

    }

}

4.6线程控制-终止线程

public final void stop()
public void interrupt()方法与stop不同,他走的是异常处理机制,如果你在线程终止前还有一些代码必须执行的话,你可以把代码写在finally中或try…catch外

public class MyThread extends Thread{

    public MyThread(String name) {
        super(name);
    }

    @Override
    public void run() {
        System.out.println("线程开始:"+new Date());
        //线程一般用来执行比较耗时的工作

            try {
                Thread.sleep(5000);
                for (int i = 0; i < 20; i++) {
                System.out.println(getName()+":"+i);
                }
            } catch (InterruptedException e) {
//                e.printStackTrace();
                System.out.println("线程被终止了");
            }


        System.out.println("线程结束:"+new Date());

    }
}

class MyThreadDemo {
    public static void main(String[] args) {
         //创建线程对象
        MyThread myThread = new MyThread("线程1:");
        //启动线程
        myThread.start();

        try {
            Thread.sleep(3000);//5秒太长了,我主线程只等你3秒
//            myThread.stop();
            myThread.interrupt();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

4.7创建多线程的方式2-Runable

package org.wdit.unit10.线程;
/*1.自定义线程类,实现Runable
2.重写run()方法
3.创建自定义线程类对象
* 4.创建Thread对象并将自定义线程类对象作为参数传递
* 5,start()
* */
public class Mythread6 implements Runnable{
    @Override
    public void run() {
        for(int i=0;i<50;i++){

            System.out.println(Thread.currentThread().getName()+"----"+i);
        }
    }
}
class Mythread6Demo{
    public static void main(String[] args) {
        Mythread6 mythread6=new Mythread6();

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

        thread1.setName("hx1");
        thread2.setName("hx2");

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

4.8.线程的生命周期

1.创建:创建线程
2.就绪:线程对象已经启动,但是没有获取到CPU的执行权
3.运行:获取到了CPU执行权
阻塞:没有CPU执行权,回到就绪
4.死亡:代码运行完毕,线程死亡

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Do6F70NU-1616838126198)(E:\JAVA上课笔记\img\线程生命周期完整.png)]

(E:\JAVA上课笔记\img\image-20201209183411569.png)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kLkWJZeq-1616838126199)(E:\JAVA上课笔记\img\线程生命周期.png)]

4.9ctrl+alt+T------->try…catch

5电影院三个窗口卖票练习

package org.wdit.unit10.线程;
/*
* 使用多线程模拟电影院售票
*    需求:电影院上映了一部电影,该电影同时可容纳100人观影,有三个窗口
* 同时售票,模拟售票过程
* */
//方式1:继承Thread
/*public class SellTicket extends Thread{
  *//*为什么票要设置为Static
  * 因为每创建一个对象就会有一个100,把它设为静态,就可以被对象共同应用*//*
   public static int ticket=100;
    @Override
    public void run() {
        while(true){
            if(ticket>0){
                System.out.println(getName()+"正在售票"+"第"+(ticket--)+"张");
            }
        }
    }
}*/
public class SellTicket implements  Runnable{
    public static int ticket=100;
    @Override
    public void run() {
        while(true){
            if(ticket>0){
                try {
                    Thread.sleep(500);//网络延迟
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"正在售票"+"第"+(ticket--)+"张");
            }
        }
    }
}
class SeellTicketDemo{
    public static void main(String[] args) {
        /*SellTicket sellTicket=new SellTicket();
        SellTicket sellTicket2=new SellTicket();
        SellTicket sellTicket3=new SellTicket();
        sellTicket.setName("窗口1:");
        sellTicket2.setName("窗口2:");
        sellTicket3.setName("窗口3:");

        sellTicket.start();
        sellTicket2.start();
        sellTicket3.start();*/


        //2.
        SellTicket sellTicket=new SellTicket();
        Thread thread=new Thread(sellTicket);
        Thread thread2=new Thread(sellTicket);
        Thread thread3=new Thread(sellTicket);

        thread.setName("窗口1:");
        thread2.setName("窗口2:");
        thread3.setName("窗口3:");


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

1.出现一票多卖:

CPU每一次执行都具有原子性(不可分割)

2.出现0和负数

5.1解决问题-同步代码块

解决多线程安全问题:

1.是否是多线程

2.是否有共享数据

3.是否有多条语句操作共享数据

思路:

考虑把这多条操作共享数据的语句进行包裹,当某一个程序在执行被包裹的语句时,其他线程不能执行这些语句。

怎么包裹?

Java为我们提供了一种机制可以解决我们的需求:同步机制

格式:

synchronized(对象){

多条操作共享数据的语句;

}

**对象:**任意对象

5.1.1.同步的前提

多个线程
多个线程使用的是同一个锁对象

5.1.2.同步的好处

同步的出现解决了多线程的安全问题。

5.1.3.同步的弊端

当线程相当多时,因为每个线程都回去判断同步上的锁,这是很消耗资源的,无形中会降低程序运行效率。

package org.wdit.unit10.线程;
/*
* 使用多线程模拟电影院售票
*    需求:电影院上映了一部电影,该电影同时可容纳100人观影,有三个窗口
* 同时售票,模拟售票过程
* */
//方式1:继承Thread
/*public class SellTicket extends Thread{
  *//*为什么票要设置为Static
  * 因为每创建一个对象就会有一个100,把它设为静态,就可以被对象共同应用*//*
   public static int ticket=100;
    @Override
    public void run() {
        while(true){
            if(ticket>0){
                System.out.println(getName()+"正在售票"+"第"+(ticket--)+"张");
            }
        }
    }
}*/
public class SellTicket implements  Runnable{
    public static int ticket=100;
    private  Object object=new Object();
    @Override
    public void run() {

            while (true) {
                synchronized (object) {
                if (ticket > 0) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "正在售票" + "第" + (ticket--) + "张");
                       Thread.yield();
                }
            }
        }
    }
}
class SeellTicketDemo{
    public static void main(String[] args) {
        /*SellTicket sellTicket=new SellTicket();
        SellTicket sellTicket2=new SellTicket();
        SellTicket sellTicket3=new SellTicket();
        sellTicket.setName("窗口1:");
        sellTicket2.setName("窗口2:");
        sellTicket3.setName("窗口3:");

        sellTicket.start();
        sellTicket2.start();
        sellTicket3.start();*/


        //2.
        SellTicket sellTicket=new SellTicket();
        Thread thread=new Thread(sellTicket);
        Thread thread2=new Thread(sellTicket);
        Thread thread3=new Thread(sellTicket);

        thread.setName("窗口1:");
        thread2.setName("窗口2:");
        thread3.setName("窗口3:");


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

5.2死锁

什么是死锁?

死锁就是指两个或两个以上的线程在执行过程中,因为争夺资源而产生的一种互相等待的现象、

2.嵌套更容易产生死锁

package org.wdit.unit10.线程;
/*死锁
*
* */
public class DeadLockDemo {
    public static void main(String[] args) {
        Dielock dielock=new Dielock(true);
        Dielock dielock2=new Dielock(false);
        dielock.start();
        dielock2.start();
    }
}
class Dielock extends Thread{
    private  boolean flag;

    public Dielock(boolean flag){
        this.flag=flag;
    }

    @Override
    public void run() {
        if(flag){
            synchronized (MyLock.objectA){
                System.out.println("锁:objectA");

            synchronized (MyLock.objectB){
                System.out.println("锁:objectB");
            }}
        }else{
            synchronized (MyLock.objectB){
                System.out.println("锁:objectB");
                synchronized (MyLock.objectA){
                    System.out.println("锁:objectA");
                }
            }
        }
    }
}
class MyLock{
    public static  Object objectA=new Object();
    public static  Object objectB=new Object();
}

5.2同步方法

1.同步方法的锁对象:this

2.Synchronized 权限修饰符 返回值类型 方法名(参数列表)

3.权限修饰符 Synchronized 返回值类型 方法名(参数列表)

4.被static修饰的同步方法的锁对象:该类的字节码文件:类名.class

6.生产者消费者模型

**需求:**不同线程操作一个学生对象,一个线程设置学生对象,一个线程用来获取学生对象

分析:

​ 1.资源类:Student

  1. 设置线程:setThread(生产者)

  2. 获取线程:getThread(消费者)

    1. 测试类

      package org.wdit.unit10.线程.消费和生产模型;
      
      public class Student {
          private String name;
          private int age;
      
          public String getName() {
              return name;
          }
      
          public void setName(String name) {
              this.name = name;
          }
      
          public int getAge() {
              return age;
          }
      
          public void setAge(int age) {
              this.age = age;
          }
      
          public Student(String name, int age) {
              this.name = name;
              this.age = age;
          }
      
          public Student() {
          }
      
          @Override
          public String toString() {
              return "Student{" +
                      "name='" + name + '\'' +
                      ", age=" + age +
                      '}';
          }
      }
      
      
      
      package org.wdit.unit10.线程.消费和生产模型;
      
      import org.wdit.unit10.线程.消费和生产模型.Student;
      
      public class setThread implements Runnable {
          private Student student;
          public  setThread(Student student){
              this.student=student;
          }
          @Override
          public void run() {
      Student student=new Student("Tom",18);
          }
      }
      

读取线程

package org.wdit.unit10.线程.消费和生产模型;

import org.wdit.unit10.线程.消费和生产模型.Student;

public class getThread implements Runnable {
    private Student student;
    public  getThread(Student student){
        this.student=student;
    }
    @Override
    public void run() {
        System.out.println(student.getName());
        System.out.println(student.getAge());

    }
}

测试

package org.wdit.unit10.线程.消费和生产模型;

import org.wdit.unit10.线程.消费和生产模型.Student;
import org.wdit.unit10.线程.消费和生产模型.getThread;
import org.wdit.unit10.线程.消费和生产模型.setThread;

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

        Student student=new Student();
        setThread setThread=new setThread(student);
        getThread getThread=new getThread(student);

       Thread thread=new Thread(setThread);
       Thread thread2=new Thread(getThread);
       thread2.start();
         thread.start();


    }
}

问题:

1.重复数据

a. 线程执行的随机性,b.CPU执行效率很高

2.数据不匹配

CPU执行的原子性和线程执行的随机

根本原因:

1.是否是多线程 Y

2.是否有共享资源 Y

3.是否有多条语句操作共享数据 Y

解决方案:

加锁

等待唤醒机制代码实现

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CfE3weaK-1616838126201)(E:\JAVA上课笔记\img\等待唤醒机制.png)]

wait()

notify()

7.线程组

8.线程池

为什么学习线程池?

因为我们在创建线程时,成本是比较高的,因为每一次创建线程都需要和操作系统进行交互,而线程池会在程序启动时提前会创建一些线程放在线程池中等待被使用。

特点:

线程池中的线程执行完毕后,不会死亡,而是会回到线程池中成为空闲状态,等待下一个对象来使用,JDK5前手动,JDK5后,java内置支持线程池

问题:

​ 线程池初始容量应该设多大?

这个设置与各种测试之后综合,决定

JDK5新增了一个Executors工厂类来产生线程池,有如下几个方法
public static ExecutorService newCachedThreadPool()
public static ExecutorService newFixedThreadPool(int nThreads)
public static ExecutorService newSingleThreadExecutor()
这些方法的返回值是ExecutorService对象,该对象表示一个线程池,可以执行Runnable对象或者Callable对象代表的线程。它提供如下方法
Future<?> submit(Runnable task)
Future submit(Callable task)
案例演示
创建线程池对象
创建Runnable实例
提交Runnable实例
关闭线程池

package org.wdit.unit10.线程;

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

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

      ExecutorService pool= Executors.newFixedThreadPool(2);
      Mythread mythread=new Mythread();
      pool.submit(mythread);
      pool.submit(mythread);
      pool.shutdown();

    }
}


class Mythread implements Runnable {

    @Override
    public void run() {
        for(int i=0;i<10;i++) {
            System.out.println(Thread.currentThread().getName()+"-----"+i);
        }
    }
}

9实现多线程方式3-Callable接口

1.与runable区别

1.可以实现多线程,但有局限性,它只能依赖于线程池来实现多线程

2.有返回值可以抛异常

3.重写call方法

1.创建线程池对象
2.创建Callable实例
3.提交Callable实例
4.关闭线程池

package org.wdit.unit10.线程;

import java.util.concurrent.*;

public class MyThreadDemoTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
     ExecutorService pool= Executors.newFixedThreadPool(2);
        MyThread9 mythread1=new MyThread9(100);
        MyThread9 mythread2=new MyThread9(200);
        Future<Integer> s1=pool.submit(mythread1);
        Future<Integer> s2= pool.submit(mythread2);

        Integer integer = s1.get();
        Integer integer2 = s2.get();


        System.out.println(integer);
        System.out.println(integer2);
        pool.shutdown();

    }
}


class MyThread9 implements Callable<Integer>{
    private int num;

    public MyThread9(int num){
        this.num=num;
    }

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

10.定时器

1.概述:

可以指定程序在某个时间做规定的工作(可以是重复的工作)

public class MyTask extends TimerTask {
    private Timer timer;
    public MyTask(){

    }
    public MyTask( Timer timer){
        this.timer=timer;
    }
    @Override
    public void run() {
        System.out.println("内鬼,停止交易");
        timer.cancel();
    }
}
class MyTaskDemo{
    public static void main(String[] args) {
        Timer timer=new Timer();
        //timer.schedule(new MyTask(),3000);
        timer.schedule(new MyTask(timer),3000,1000);

    }
}

2.综合练习

public class DeleteFolder extends TimerTask {
    private Timer timer;

    public DeleteFolder(Timer timer) {
        this.timer = timer;
    }

    @Override
    public void run() {
        File file=new File("D:\\hx");
        deleteFolder(file);
        timer.cancel();

    }
    private void deleteFolder(File srcFolder){
        //获取指定文件夹下的所有file对象
        File[]files=srcFolder.listFiles();
        if (files != null) {
            for (File file : files) {
                if (file.isDirectory()) {
                    //文件夹
                    deleteFolder(file);
                } else {
                    //文件
                    System.out.println(srcFolder.getName() + "---" + srcFolder.delete());
                }
            }
        }
        System.out.println(srcFolder.getName() + "---" + srcFolder.delete());

    }
}
class DeleteFolderDemo{
    public static void main(String[] args) {
        Timer timer=new Timer();

        String time="2020-12-13 09:46:00";
        SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        Date date=null;
        try {
             date=sdf.parse(time);
        } catch (ParseException e) {
            e.printStackTrace();
        }
        timer.schedule(new DeleteFolder(timer),date);
    }
}
private void deleteFolder(File srcFolder){
    //获取指定文件夹下的所有file对象
    File[]files=srcFolder.listFiles();
    if (files != null) {
        for (File file : files) {
            if (file.isDirectory()) {
                //文件夹
                deleteFolder(file);
            } else {
                //文件
                System.out.println(srcFolder.getName() + "---" + srcFolder.delete());
            }
        }
    }
    System.out.println(srcFolder.getName() + "---" + srcFolder.delete());

}

}
class DeleteFolderDemo{
public static void main(String[] args) {
Timer timer=new Timer();

    String time="2020-12-13 09:46:00";
    SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    Date date=null;
    try {
         date=sdf.parse(time);
    } catch (ParseException e) {
        e.printStackTrace();
    }
    timer.schedule(new DeleteFolder(timer),date);
}

}


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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值