从零说起: 5.多线程初步了解

多线程

​ 从几年前实习开始, 陆陆续续写了好多的markdown笔记, 早期的笔记大多是在B站和一些大牛博客分享而自己整理的笔记, 早期的笔记体系比较清晰,也是我的学习成长路线.

​ 后续的笔记也有一些在业界大佬的分享,官网论坛,github学习到的更深层次一点的东西以及一些工作的经验和踩到的坑, 以后我会把早期记下的笔记和目前在学习以及工作中遇到的整理成模块不定期分享出来,如果大家有不同的见解也希望能在评论区说出大家的看法.

​ 希望我们永远保持这份思考与热爱.

1. 概念

  1.线程与进程
    进程:
        Process[进程]
        正在执行的程序,我们称之为进程

    线程:
        Thread[线程]
        是进程中的一条执行的路径,一个控制单元
        线程是一个程序内部的一条执行路径。java虚拟机允许应用程序并发的运行着多条执行路径

    多进程:
        在操作系统上同时运行着多个程序

    多线程:
        一个进程中同时执行着多条执行路径

  2.java的应用程序本身就是多线程程序
	一条执行路径:从main方法入手的那条执行路径,这条执行路径称之为主线程
	另外一条执行路径:垃圾回收机制(gc())
	主线程: main()
	子线程: 工作线程

  3.并发执行
	看起来是同时执行
	其实内部操作是 CPU在执行路径间做着瞬间切换的动作
	CPU一次只能执行一个任务 假如有3个任务A,B,C
	CPU的瞬间切换不是随机的 按照时间片的方式进行切换的
	时间片:CPU分配给各个线程的时间 每个线程被分配一个时间段 这个时间段称之为时间片
	
  4.什么时候使用多线程??
	操作期间有耗时任务 可以把耗时任务放在另外一个线程中
		比如数据的请求【耗时的操作】
		上拉加载新数据【新数据的请求】 ---> 线程中		
		不能影响其他的操作 比如看其他的商品

2. 线程的创建方式

JDK也为线程提供了一个类Thread - 线程类

记住:java中表示线程的类要么Thread本类或者其子类

线程创建方式:
​ 1.继承Thread类
​ 2.实现Runnable接口

1、继承 Thread 类创建线程

    1.声明一个类A继承自Thread类,并且重写run方法

    2. 在测试类中创建A类对象,调用start方法启动线程
        如果线程想被执行 必须先进行启动start
       run方法:
        方法体中存放的是该执行路径的任务

    具体步骤:
        step1: 创建一个类, 继承 Thread 类
        step2:  重写父类的方法  run()  ---- 线程体
        step3:  启动线程对象  -- start()
        start() 方法是去通知cpu, 有一个线程准备好了, 你可以来执行了。
        什么时候被执行, 取决于cpu

        start() 方法的作用:
        1, 开启线程, 让线程可以执行
        2, 让 cpu 可以去调用run() 方法

    示例:
    class FirstThread extends Thread{
        @Override
        public void run() {
            for(int i = 0;i< 1000;i++){
                System.out.println("*");
            }
        }
    }

    class SecondThread extends Thread{
        @Override
        public void run() {
            for(int i = 0;i< 1000;i++){
                System.out.println("\t\t" + i);
            }
        }
    }

    public class Demo03_Thread {
        public static void main(String[] args) {
            FirstThread firstThread = new FirstThread();
            SecondThread secondThread = new SecondThread();
            firstThread.start();//两条线程
            secondThread.start();//三条线程
            //主线程执行完成, 子线程有可能还没有完成
            //一个线程只能执行一次, 也就相当于是人的辈子
        }
    }

2、实现 Runnable 接口

1.声明一个类A实现Runnable接口,重写接口中的run方法
	类A就相当于线程任务的包含者
2.创建Thread类对象 绑定类A 【其实就是绑定任务run方法】
3.调用start方法启动线程

具体步骤:
	step1: 定义一个类, 实现Runnable 接口
	step2:  重写run()方法, 线程体
	step3:  启动线程, 通过实现类对象了,创建一个线程类的对象, 在调用线程类的对象start()
		Thread thread = new Thread(实现了Runnable 接口的对象);
		thread.start();

示例:
class MyRunnable  implements Runnable{
	@Override
	public void run() {
		for(int i = 0;i<1000;i++){
			System.out.println("\t" + i);
		}
	}
}
public class Demo04_Runnable {
	public static void main(String[] args) {
		//启动线程
		MyRunnable myRunnable = new MyRunnable();
		//启动线程必须要调用start()方法,而Runnable对象中没有该方法,该方法只在Thread 类中存在,
		//所以必须要尽量的往Thread类的方向去靠,而Thread类的构造方法中,只需要传入一个Runnable 
		//对象就可以的到Thread类
		Thread thread = new Thread(myRunnable);
		thread.start();
	}
}

3、比较两种创建方式的区别

两种创建方式的对比:
	第二种比第一种更灵活,建议使用第二种方案。
	java的类继承机制:单继承
		第一种继承自Thread类 就不可以再继承其他类 实现其他接口
		第二种实现类即可继承其他类 也可以实现其他接口
		
1. 通过Thread 类创建的线程, 因为创建了多个Thread类对象, 所以成员变量的数据就有多份
2. 通过Runnable接口创建的线程, 线程对象mRunnable2, 通过该对象启动多个线程, 这些线程中是共享了该对象中的成员变量数据		

对 1 说明:
class MyThread3 extends Thread{
	int i = 0;//成员变量,  类里, 方法之外 ,属于对象所有, 创建对象只有一份
	@Override
	public void run() {
		//线程体
		while (i<10) {
			System.out.println(i++);
		}
	}
}
public class Demo05_Thread {
	public static void main(String[] args) {
		MyThread3 t1 = new MyThread3();
		MyThread3 t2 = new MyThread3();
		MyThread3 t3 = new MyThread3();
		t1.start();
		t2.start();
		t3.start();
	}
}
结果为:0123456789012345678901234567892 说明:
class MyRunnable2 implements Runnable{
	int i = 0;//成员变量
	@Override
	public void run() {
		while (i<10) {
			System.out.print(i++);
		}
	}
}
public class Demo06_Runnable {
	public static void main(String[] args) {
		MyRunnable2 mRunnable2 = new MyRunnable2();
		Thread t1 = new Thread(mRunnable2);
		Thread t2 = new Thread(mRunnable2);
		Thread t3 = new Thread(mRunnable2);
		t1.start();
		t2.start();
		t3.start();
	}
}
结果为:0123456789

3. 线程常用的方法

1、线程的名字及当前线程的获取

	获取当前线程对象的方法:Thread Thread.currentThread() 
	
	获取线程名字: String Thread.currentThread().getName()
	
	获取当前线程的ID: long Thread.currentThread().getId()
	
	给线程自定义名称:
		第一种创建线程的方式
		Thread.setName(String)
		第二种创建线程的方式
		通过构造方法 在父类中有一个构造方法
		public Thread(String name)
		第三种创建线程的方式
		通过构造方法
		public Thread(Runnable runnable, String name)

2、线程休眠

	静态方法 static void sleep(long mills)
	
	在当前线程中 如果调用sleep方法 表示当前线程进入到休眠状态 放弃CPU的执行权
	进入到线程阻塞状态。
	什么时候阻塞状态结束??? ----> 睡的时间到了 
	该线程阻塞状态结束 进入等待状态 等待CPU分配时间片	

3、线程合并

	对象方法 void join()
			void join(long mills)
        
    在线程A中调用线程B的join方法,使线程A放弃CPU的执行权,将CPU执行线程B。这个时候对于A线程来说A线程处于阻塞状态 (放弃CPU的执行权) 等待B线程任务执行完(或者合并时间结束)  A线程才能解除阻塞状态 等待CPU分配时间片
	线程A什么时候解阻塞?
		1.线程B的任务执行完
		2.线程B的加塞时间到了
		3.在其他中调用了线程A的interrupt方法
		
    public static void main(String[] args) {
        Runnable runnable = new Runnable() {
			@Override
            public void run() {
            	for(int i = 0; i < 1000; i++){
                	System.out.println(Thread.currentThread().getName() + "~" + i);
				}
			}
		};
        Thread thread = new Thread(runnable, "patter");
        thread.start();
        for(int i = 1000; i < 2000; i++){
        	System.out.println("main"+ "~" + i);
            if (i == 1010) {
            	try {
                	thread.join(5000);
				} catch (InterruptedException e) {
                	e.printStackTrace();
                }
            }
        }
	}

4、线程礼让

	静态方法 static void yield()
	暂停当前正在执行的线程对象 把执行机会让给相同或者更高优先级的其他线程
		
	public static void main(String[] args) {
    	Runnable target =new Runnable() {
        	public void run() {
            	for(int i = 0; i < 1000; i++){
                	System.out.println(Thread.currentThread().getName() + "~" + i);
                }
			}
		};
            
        Thread thread = new Thread(target, "线程一");
        thread.start();
        for(int i = 0; i < 1000; i++){
        	System.out.println("main" + "~" + i);
            if(i == 100){
            	Thread.yield();
            }
        }
	}

5、线程优先级

	主线程的优先级默认为5
	
	设置一个线程的优先级的方法
		void setPriority(int priority)
		priority取值范围 1-10 值越大优先级别越高
	获得一个线程的优先级别
		int getPriority()
		
    注意:
		优先级别高的线程获得CPU执行的几率比优先级别低的大
		在A线程中创建的线程B的对象,线程B的对象的优先级与A线程优先级一致	
		
	Thread类中提供了线程优先级别的3个字段
		MAX_PRIORITY   10
		NORM_PRIORITY   5
		MIN_PRIORITY    1 

6、线程中断

	对象方法 void interrupt()
	在A线程中调用B线程的interrupt方法,可以将B线程的阻塞状态中断 使B进入到
	准备状态 等待CPU分配时间片

7、后台线程 [精灵线程/守护线程]

	垃圾回收线程 ---- 最经典的后台线程
	后台线程的作用:为前台线程提供服务
	
    将一个前台线程设置为后台线程
		void setDaemon(true)
	
	注意:
		1.当前台线程死亡后,后台线程也会随之死亡
		  前台线程消亡之后 后台没有立即消亡的原因:当前台线程死亡之后 JVM会通知后台线程消亡 再到后台线程接收到 并作出反应 之间是有时间间隔的 在这个时间间隔中 后台线程又执行了一段时间
		2.设置为后台线程时 设置语句必须在线程启动之前 否则会爆发异常
		3.在前台线程中创建线程默认为前台线程

4. 线程的五种状态

新建状态  -- 就绪状态  -- 运行状态  --  阻塞状态  -- 死亡状态
创建:  new()
就绪:  start()
运行:  被执行
阻塞:  暂时放弃执行, 比如 sleep() ----  解除阻塞, 进入到了就绪的状态
死亡:  执行结束

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5NiVikhF-1617204025975)(img\线程的运行状态.png)]

5. 线程同步

​ 多个线程, 访问共同的数据, 会导致该数据不安全, 叫做线程之间的数据安全问题(临界资源问题)

​ 同步: 就是让某一部分代码, 一次只能被一个线程执行, 中间不能被其他线程插入执行

1、synchronized 关键字

1) 同步代码块
	格式:
		synchronized(对象){
			包含的是线程共享的数据
		}
	synchronized 同步关键字 是java的内置特定 在虚拟机层面上实现了对资源同步互斥访问
	
	同步的原理:
		将需要同步的代码进行封装在同步代码块中,同步代码块中的对象相当于在同步代码块的基础上加了一把锁和监视器 【锁和监视器是一个对象】
		坐火车 --> 火车上有卫生间
		监视器变红【是因为卫生间上锁】 ---> 有人 
		当其他人想访问时 看到监视器为红色 稍等片刻 等待监视器变绿
						
	当一个线程获得同步锁和监事器的时候 什么时候释放?
		当同步代码块执行完 自动释放锁和监视器。如果不释放,其他线程无法访问	
	当一个线程A获得同步锁和监视器,其他线程需在外面等待,对于其他线程来说,这种情况表示线程处于阻塞状态。
	什么时候解阻塞? 
	当线程A释放同步监视器和锁之后,其他线程解阻塞,进入准备状态,等待CPU分配时间片
						
	注意:
		synchronized后的对象确实可以为任意对象,但是要保证对于多个线程来说这个对象是唯一的  ===> 锁只有一把	
2) 同步函数
	当一个方法的内容都需要同步时 可以将该方法置为同步方法
	在方法前面 添加同步关键字即可
			
	方法同步之后 同步锁和监视器是当前被共享的对象:this
			
	静态方法被同步之后 同步锁和监视器是类对象
	
	每次只能有一个线程执行该方法
	public synchronized void add(int x){
		//被同步的代码
	}
				
同步原理:
	同步的对象:可以为任意对象,要求:必须是所有线程都能共同访问的对象
	就是利用对象的互斥
	
	注意: 
		1, 多线程中, 存在共享数据的安全问题
		2, 一定是多个线程锁定的是同一个对象
		同步对象:  多个线程访问同一个对象
				A, 静态的
				B, 外部创建好的一个对象, 传入线程中
				C, 字符串常量
				D, 类.class 
		
	优点:可以解决线程之间共享数据安全的问题
	缺点: 1. 降低了执行效率, 2. 容易造成死锁
	解决死锁的方法:1. 扩大锁的范围, 2. 尽量避免成员变量
3) 示例:售票问题

1.实现 Runnable 接口的同步方法

class MyRunnable1 implements Runnable{
	private int tickes =  100;
	private boolean flag  = true;//标记: 是否可以继续卖票
	@Override
	public void run() {
		while (flag) {
			sale();
		}
	}
	//卖票
	public synchronized void sale(){
		if (tickes>0) {
			try {
				Thread.sleep(100);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println(Thread.currentThread().getName() + "\t" + tickes--);
		}else {
			flag  = false;
		}
	}
}

public class Demo01_Tickes {
	public static void main(String[] args) {
		MyRunnable1 runnable1 = new MyRunnable1();
		Thread t1 = new Thread(runnable1, "售票口1");
		Thread t2 = new Thread(runnable1, "售票口2");
		Thread t3 = new Thread(runnable1, "售票口3");
		Thread t4 = new Thread(runnable1, "售票口4");
		t1.start();
		t2.start();
		t3.start();
		t4.start();
	}
}
  1. 继承 Thread 类的同步代码块
class MyThread2 extends Thread{
	private static int tickes = 100;
    
    //必须是同一个obj
	private Object obj ;
	//private static Object object = new Object();
    
	public MyThread2(String name,Object obj){
		super(name);
		this.obj = obj;
	}
	
	@Override
	public void run() {
		while (true) {
			synchronized (MyThread2.class) {
				//this 锁不住   new 了很多次, 
				if (tickes>0) {
					try {
						Thread.sleep(100);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					System.out.println(Thread.currentThread().getName() + "\t" + tickes--);
				}else{
					break;
				}
			}
		}
	}
}

public class Demo02_Tickes {
	public static void main(String[] args) {
		Object obj = new Object();
		MyThread2 t1  = new MyThread2("售票口1",obj);
		MyThread2 t2  = new MyThread2("售票口2",obj);
		MyThread2 t3  = new MyThread2("售票口3",obj);
		MyThread2 t4  = new MyThread2("售票口4",obj);
		t1.start();
		t2.start();
		t3.start();
		t4.start();
	}
}

  1. 继承 Thread 类的同步方法
class MyThread3 extends Thread{
	private static int tickes =  100;
	private static  boolean flag  = true;//标记: 是否可以继续卖票
	@Override
	public void run() {
		while (flag) {
			sale();
		}
		
	}
	public  static synchronized void sale(){
		if (tickes>0) {
			try {
				Thread.sleep(100);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println(Thread.currentThread().getName() + "\t" + tickes--);
		}else {
			
			flag  = false;
		}
	}
}

public class Demo03_Tickes {
	public static void main(String[] args) {
		new MyThread3().start();
		new MyThread3().start();
		new MyThread3().start();
		new MyThread3().start();
	}
}

2、Lock锁

Lock锁
	锁和监视器不是同一个对象,锁和监视器分开的
	Lock接口 使用ReentrantLock
		ReentrantLock lock = new ReentrantLock()
	监视器对象	
		Condition con = lock.newCondition();
	如何进行唤醒和等待????
		con.await(); 等待
		con.signal();唤醒一个
		con.signalAll(); 唤醒等待的所有
	
使用方法:
	class X{
		// 锁的对象
		private final ReentrantLock lock = new ReentrantLock();
		// 与锁绑定的监视器
		Condition con = lock.newCondition();
		// 修改资源的方法
		void method(){
			lock.lock();
			try{
				//需要同步的代码
			}finally{
				// 解锁
				lock.unlock();
			}
		}
	}

多线程的好处与弊端
	好处:充分利用CPU资源,提高了代码的执行效率
	弊端:
		过多的线程 降低代码的执行效率
		多条线程访问同一资源,并对资源的数据进行修改,这种情况会出现临界资源问题【将修改的资源同步化】
		同步化资源时要注意同步的适当性,不能过度同步
Lock
				JDK5.0
				
1) 示例:存取问题
class Account{
	private double balance = 10000;
	// 锁对象
	private final ReentrantLock lock = new ReentrantLock();
	// 监视器对象
	Condition condition = lock.newCondition();
	// 增加
	synchronized void save(double money){
		//synchronized (this) {
			balance = balance + money;
			System.out.println("当前余额为:" + balance);
		//}
	}
	// 取钱
	void get(double money){
		// 加锁
		lock.lock();
		try{
			balance = balance - money;
			System.out.println("取走之后剩余余额:" + balance);
		}finally {
			// 解锁
			lock.unlock();
		}
	}
}

class Person extends Thread{
	Account account;
	double money; // 这个人要存/取的钱数
	Person(){}
	Person(String name, Account account, double money){
		super(name);
		this.account = account;
		this.money = money;
	}
	
	public void run() {
		// 存钱
		//account.save(money);
		// 取钱
		account.get(money);
	}
}

public class ThreadSafe2 {
	public static void main(String[] args) {
		Account account = new Account();
		Person person = new Person("路飞", account, 1000);
		Person person2 = new Person("女帝",account, 5000);
		person.start();
		person2.start();
		Person person3 = new Person("娜美", account, 1000);
		Person person4 = new Person("索隆", account, 8000);
		person3.start();
		person4.start();
	}
}
2) 示例: 死锁

​ 详解死锁: https://blog.csdn.net/hd12370/article/details/82814348

class MyRunnable5 implements Runnable{
	private static String milk = "牛奶";
	private static String bread = "面包";
	boolean flag = true;//1 张三, 2 李四
	@Override
	public void run() {
		//t1 张三
		if (flag) {
			synchronized (milk) {//锁定牛奶
				System.out.println(Thread.currentThread().getName() + 
						" \t已经拥有了"+milk+", 还想要:"+ bread);
				synchronized (bread) {//锁定面包
					System.out.println(Thread.currentThread().getName() + 
							" \t即拥有了"+milk+", 又拥有了:"+ bread);
				}
			}
		}
		
		//t2 李四
		else {
			synchronized (bread) {
				System.out.println(Thread.currentThread().getName() + 
						" \t已经拥有了"+bread+", 还想要:"+ milk);
				try {
					Thread.sleep(100);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
				synchronized (milk) {
					System.out.println(Thread.currentThread().getName() + 
							" \t即拥有了"+bread+", 又拥有了:"+ milk);
				}
			}
		}
	}
}

public class Demo05_DeadLock {
	public static void main(String[] args) {
		MyRunnable5 mt1 = new MyRunnable5();
		mt1.flag = true;
		Thread t1 = new Thread(mt1,"张三");
		MyRunnable5 mt2 = new MyRunnable5();
		mt2.flag = false;
		Thread t2 = new Thread(mt2,"李四");
		t1.start();
		t2.start();
	}
}

6. 线程的唤醒和等待

1、等待和唤醒方法

	需要用到线程的等待和唤醒(Object中的方法)
	等待的方法
		void wait()
			调用者:同步监视器对象
			这个方法是与synchronized结合使用的
			作用:使当前线程进入到休眠状态
	
	唤醒的方法
		void notify()
			唤醒通过wait方法进入到休眠状态的某一个线程 【随机的取其中一个】
		void notifyAll()
			唤醒通过wait方法进入到休眠状态的所有线程
			调用者:同步监视器的对象 ---> 要与调用wait方法的对象是同一个
			需要与synchronized结合使用
			
	注意:
		唤醒要在等待之前,避免等待了无法唤醒的情况
			
	wait() 和  sleep()的区别
		出处:
			wait()  -- > Object
			sleep()  -- > Thread
		唤醒:
			wait()  -- > 要被唤醒: notify()  notifyAll()
			sleep() -- > 时间到了自然醒

2、经典案例: 生产者消费者问题

​ 馒头:

public class ManTou {
	private int id;
	public ManTou() {
		super();
	}
	public ManTou(int id) {
		super();
		this.id = id;
	}
	public int getId() {
		return id;
	}
	public void setId(int id) {
		this.id = id;
	}
	@Override
	public String toString() {
		return "ManTou [id=" + id + "]";
	}
}

​ 筐:

public class Basket {

    private ManTou[] manTous = new ManTou[5];
    private int index = 0;

    public synchronized void addManTou(ManTou manTou) throws InterruptedException {
        if (index == manTous.length){
            System.out.println("篮子已满,生产者等待");
            wait();
        }else{
            manTous[index] = manTou;
            index++;
            System.out.println("生产者生产了一个馒头: " + manTou + ", 篮子中已有: " + index + " 个馒头");
        }
        Thread.sleep(100);
        notify();//唤醒 , 如果没有wait() 的线程, 则是一个空唤醒, 等于白白唤醒一次
		//notifyAll();//生产者有多个, 那么使用该方法, 唤醒所有等待的线程
    }
    public synchronized ManTou getManTou() throws InterruptedException {
        ManTou manTou;
        if (index == 0){
            manTou = null;
            System.out.println("篮子已空,消费者等待");
            wait();
        }else{
            index--;
            manTou = manTous[index];
            System.out.println("消费者消费了一个馒头: " + manTou + ", 篮子中还有: " + index + " 个馒头");
        }

        Thread.sleep(100);
        notify();
        return manTou;
    }
}

​ 生产者与消费者:

//生产者
class Producer implements Runnable{
	private Basket basket;
	private int id = 1;//用户生产的馒头个数, 默认从1开始
	
	public Producer(Basket basket){
		this.basket = basket;
	}
	@Override
	public void run() {
		while (true) {
			//生产馒头, 并且把生产的馒头添加到篮子中
			ManTou manTou = new ManTou(id++);
			basket.add(manTou);
		}
	}
}

//消费者
class Customer implements Runnable{
	private Basket basket;
	public Customer(Basket basket){
		this.basket = basket;
	}
	@Override
	public void run() {
		while (true) {
			basket.get();
		}
	}
}

//生产消费过程
public class ProAndCus {
	public static void main(String[] args) {
		Basket basket  = new Basket();
		Producer producer = new Producer(basket);
		Customer customer = new Customer(basket);
		Thread t1 = new Thread(producer, "大厨");
		Thread t2 = new Thread(customer, "吃货");
		t1.start();
		t2.start();
	}
}

7. 线程池

class Task implements Runnable{
	@Override
	public void run() {
		for(int i = 0;i<10;i++){
			System.out.println(Thread.currentThread().getName() +"\t" + i);
		}
	}
}

public class Demo_ThreadPool {
	public static void main(String[] args) {
		
		//如果要使用线程池: 那么线程就不用自己创建了, 线程池帮我们创建好了
		//1, 创建线程池对象
		//newSingleThreadExecutor   该池子中只有一个线程, 
		//ExecutorService es = Executors.newSingleThreadExecutor();
		
		//指定数量的线程 --- 常用
		//如果需要5个任务,则先执行3个任务, 然后在执行2个任务 
		//ExecutorService es = Executors.newFixedThreadPool(3);
		
		//带有缓存的线程池, 根据系统性能, 创建线程
		//ExecutorService es = Executors.newCachedThreadPool();
		
		Task t1 = new Task();
		Task t2 = new Task();
		Task t3 = new Task();
		Task t4 = new Task();
		Task t5 = new Task();
		
//		es.execute(t1);
//		es.execute(t2);
//		es.execute(t3);
//		es.execute(t4);
//		es.execute(t5);
		
		//延迟执行
		ScheduledExecutorService es = Executors.newScheduledThreadPool(1);
		es.schedule(t1, 10, TimeUnit.SECONDS);
	}
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值