Java学习日记——从多线程到生产者消费者模式

关键字:多线程,Thread,Runnable,run(),start(),Callable,wait(),notify(),线程的状态,sleep(),interrupt(),join(),yield(),sychronized关键字

最简单的多线程实现

实现多线程最简单的方法就是继承Thread父类,重写其中的run()方法来实现。

package dataStructure;

class MyThread extends Thread{
	private String name ;
	public MyThread(String name){
		this.name = name ;
	}
	@Override
	public void run(){
		for(int i = 0 ; i < 10 ; i++){
			System.out.println("线程: "+ this.name +"执行:i = " + i);
		}
	}
}

public class ThreadDemo {
	public static void main(String[] args){
		MyThread mt5 = new MyThread("五号") ;
		MyThread mt4 = new MyThread("四号") ;
		MyThread mt3 = new MyThread("三号") ;
		
		mt5.run();
		mt4.run();
		mt3.run();
	}
}
线程: 五号执行:i = 0
线程: 五号执行:i = 1
线程: 五号执行:i = 2
线程: 五号执行:i = 3
线程: 五号执行:i = 4
线程: 五号执行:i = 5
线程: 五号执行:i = 6
线程: 五号执行:i = 7
线程: 五号执行:i = 8
线程: 五号执行:i = 9
线程: 四号执行:i = 0
线程: 四号执行:i = 1
线程: 四号执行:i = 2
线程: 四号执行:i = 3
线程: 四号执行:i = 4
线程: 四号执行:i = 5
线程: 四号执行:i = 6
线程: 四号执行:i = 7
线程: 四号执行:i = 8
线程: 四号执行:i = 9
线程: 三号执行:i = 0
线程: 三号执行:i = 1
线程: 三号执行:i = 2
线程: 三号执行:i = 3
线程: 三号执行:i = 4
线程: 三号执行:i = 5
线程: 三号执行:i = 6
线程: 三号执行:i = 7
线程: 三号执行:i = 8
线程: 三号执行:i = 9

但是这样子并不算实现多线程,因为这其实就是一个简单Java类,把一个操作执行完成之后开始下一个。所以我们可以引入Thread类中的start()方法 。

public class ThreadDemo {
	public static void main(String[] args){
		MyThread mt5 = new MyThread("五号") ;
		MyThread mt4 = new MyThread("四号") ;
		MyThread mt3 = new MyThread("三号") ;
		
		mt5.start();
		mt4.start();
		mt3.start();
	}

线程: 五号执行:i = 0
线程: 五号执行:i = 1
线程: 四号执行:i = 0
线程: 三号执行:i = 0
线程: 三号执行:i = 1
线程: 三号执行:i = 2
线程: 三号执行:i = 3
线程: 三号执行:i = 4
线程: 三号执行:i = 5
线程: 三号执行:i = 6
线程: 三号执行:i = 7
线程: 三号执行:i = 8
线程: 三号执行:i = 9
线程: 四号执行:i = 1
线程: 五号执行:i = 2
线程: 四号执行:i = 2
线程: 五号执行:i = 3

在使用了 Thread类中的start()方法之后,三个操作开始交替的执行起来。
但是这么做也有一个问题,我们的线程实现类继承父类只能实现单继承,要是多继承就只能选择接口,也就是Runnable接口。但Runnable接口中没有提供start()方法怎么办呢?
我们观察一下Thread的构造方法
https://docs.oracle.com/javase/8/docs/api/
我们可以看到Thread构造方法中是可以传入一个Runnable对象作为参数的。所以我们可以这样做。

class MyThread implements Runnable{
	//略
}
public static void main(String[] args){
		new Thread(new MyThread("三号")).start();
		new Thread(new MyThread("四号")).start();
		new Thread(new MyThread("五号")).start();
	}
线程: 四号执行:i = 0
线程: 五号执行:i = 0
线程: 五号执行:i = 1
线程: 五号执行:i = 2
线程: 五号执行:i = 3
线程: 五号执行:i = 4
线程: 五号执行:i = 5
线程: 三号执行:i = 0
线程: 五号执行:i = 6

在上面的代码中我们的线程实现类实现了Runnable接口,但是在使用的时候确实将其实例化对象传入到Thread的匿名对象当中。再调用匿名对象的start()方法。
https://edu.aliyun.com/lesson_1012_8947?spm=5176.8764728.0.0.338dd290NGFGYY#_8947
我们观察Java中源码可以知道,Runnable接口提供的是一个run()方法的标准,Thread类做代理实现了绝大多数需要使用的方法,而我们自己只需要在线程实现类中写出来核心业务,重写run()方法就好了。运行的时候,让客户端调用Thread类中的start()方法
这样我们可以说知道了“如何”使用多线程,但是什么时候使用多线程?为什么要用多线程呢?这就是下一个问题了。

为什么使用多线程?

多线程操作本质上是对资源的抢占,当我们的一个实例化线程对象调用资源一定时间之后,系统需要将资源调配给其他 对象使用一段时间。下面我们来看一个数据同步的例子。

class MyThread implements Runnable{
	private int ticket ;
	public MyThread(int ticket){
		this.ticket = ticket ;
	}
	@Override
	public void run(){
		for(; this.ticket > 0 ;){
				System.out.println(this.toString() + "卖出一张票," + "票还剩:" + this.ticket--);
		}
	}
}
public class ThreadDemo {
	public static void main(String[] args){
		MyThread shop = new MyThread(5) ;//总共一万张票
		Thread shopA = new Thread(shop) ;
		Thread shopB = new Thread(shop) ;
		shopA.start();
		shopB.start();
	}
}
dataStructure.MyThread@558fdafd卖出一张票,票还剩:5
dataStructure.MyThread@558fdafd卖出一张票,票还剩:4
dataStructure.MyThread@558fdafd卖出一张票,票还剩:3
dataStructure.MyThread@558fdafd卖出一张票,票还剩:2
dataStructure.MyThread@558fdafd卖出一张票,票还剩:1

在上面的例子当中,我们的ticket总数是固定的,但是却使用了两个线程将其售卖出去。
到目前为止我们知道了多线程是如何运作的,但是现在摆在我们面前的一个问题就是,Runnable接口并没有返回值。好在Java为我们提供了具有返回值的方法。
https://docs.oracle.com/javase/8/docs/api/
在这个接口之中Java提供了一个带有泛型返回值的call()方法。
在这里插入图片描述
用法大概是这样,有兴趣的朋友可以自行去Oracle网站查阅api,我这里直接给出几个类之间的关系。
那么到了这里一切就都明白了,我们既然想要一个有返回值的方法,又想要Thread类中的start()方法,那么我们可以使用线程实现类实现Callacle接口,将自己的核心业务写入到call方法中,将线程实现类作为参数传入到FutureTask的实例化对象之中
在这里插入图片描述
这个类实现了Runnable接口,这就意味着我们的FutureTask实例化对象同样是一个Runnable对象,是可以作为参数传入到Thread类中的!
那么到这里我们总算捋清楚了这几个类的逻辑,来写一个代码测试一下吧!

class MyThread implements Callable<String>{
	@Override
	public String call() throws Exception {
		for(int i = 0 ; i < 10 ; i++){
			System.out.println("执行:" + i);
		}
		return "数据已获取";
	}
	
}
public class ThreadDemo {
	public static void main(String[] args) throws InterruptedException, ExecutionException{
		FutureTask<String> task = new FutureTask<String>(new MyThread()) ;
		new Thread(task).start() ;
		System.out.println(task.get());
	}
}
执行:0
执行:1
执行:2
执行:3
执行:4
执行:5
执行:6
执行:7
执行:8
执行:9
数据已获取

在上面这个案例之中我们可以看到,我们既实现了有返回值的call()方法,同样调用了Thread类。
到此为止我们的线程实现先告一段落,我们来看一下在JVM中线程到底是如何调用的。
在这里插入图片描述
我们创建的多个线程对象首先要调用start()方法让对象处于就绪状态,随后系统会对就绪状态中的对象进行调度。当所有对象处理完毕之后终止这个过程。

多线程到底能帮助我们做什么?

说了这么多,我们到现在还不知道为什么我们要在程序中设计一个多线程的操作。让我们用一个简单的代码看一下吧。

System.out.println("1.执行任务一");
		System.out.println("2.执行任务二");
		int temp = 0 ;
		for(int i = 0 ; i < Integer.MAX_VALUE ; i++)
			temp += i ;
		System.out.println("3.执行任务三");
		System.out.println("n.执行任务N");

这个代码不同人运行起来效果可能不一样,但有一点是肯定的,就是在程序输出“执行任务2”之后一定会停顿一下。虽然这个时间很短,但这代表了我们程序中会出现非常耗时的任务,如果我们任由其占着主线程处理,那么可能会耽误我们进行后续的操作。所以我们可以将其封装到一个线程之中。

System.out.println("1.执行任务一");
		System.out.println("2.执行任务二");
		new Thread(() -> {
			int temp = 0 ;
			for(int i = 0 ; i < Integer.MAX_VALUE ; i++){
				temp += i ;
			}
		}).start(); ;
		System.out.println("3.执行任务三");
		System.out.println("n.执行任务N");

这个时候大家再去运行代码,是不是四句话瞬间就输出了呢?到此为止,我们看到了我们多线程的第一个用途:主线程负责整体流程,子线程负责耗时操作。

线程的休眠

接下来我们讨论写一个话题,一个任务执行的时间过长想先暂停一下怎么操作呢?这个时候我们就可以使用我们Thread类中的sleep()方法。只需要将我们希望其休眠的时间当做参数传入进去,线程在执行到这一步的时候就会休眠。我们来看一段代码。https://docs.oracle.com/javase/8/docs/api/

new Thread(() -> {
			for(int i = 0 ; i < 10 ; i++){
				System.out.println(Thread.currentThread().getName() + "执行:" + i);
				try {
					Thread.sleep(1000) ;
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
		} , "线程对象").start();

在上面的代码中,我们每执行一次start()方法,线程就要休眠一秒。要记得添加trycatch语句捕获异常。

如何中断异常

我们观察休眠的api文档会发现,这个方法中有可能出一个“InterruptedException”异常,这就是说我们的线程也可以被打断。我们用一个代码来说明一下。

Thread thread = new Thread(() -> {
			System.out.println("准备睡觉");
			try {
				Thread.sleep(10000);
				System.out.println("休息够了");
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
				System.out.println("没休息够,被打断了");
			}
		}) ;
		thread.start();
		Thread.sleep(1000);
		if(!thread.isInterrupted())
			thread.interrupt();

在上面的代码中我们可以看到,正常情况下第一个线程会休眠十秒钟的时间,但是却在执行了一秒钟的时间后被打断了。

强制执行线程

那么如果有重要的线程需要我们优先调度资源去处理呢?Java中同样给我们提供了方法可以让调用这个方法的线程一直占用线程直到处理完毕。我们来看一段代码在这里插入图片描述

Thread mainThread = Thread.currentThread() ;
		Thread thread = new Thread(() -> {
			for(int i = 0 ; i < 100 ; i++){
				if(i == 3)
					try {
						mainThread.join();
					} catch (InterruptedException e1) {
						e1.printStackTrace();
					}
				try {
					Thread.sleep(100);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				System.out.println(Thread.currentThread().getName() + "执行:" + i);
			}
		} , "玩耍的线程") ;
		thread.start();
		for(int i = 0 ; i < 100 ; i++){
			Thread.sleep(100);
			System.out.println("【优先】执行的线程: " + i);
		}

在上面的代码中我们设置了正常的子线程,当子线程执行到第三个步骤的时候,我们通过Thread.currentThread()获取到的主线程对象开始调用join()方法,强制占用资源到处理完毕,然后开始处理子线程。

如何礼让线程

在程序运行的过程中可能出现某一个线程临时更重要一些,这个时候我们可以执行线程的礼让操作。让调用礼让方法的线程优先执行。看一段代码。在这里插入图片描述

Thread mainThread = Thread.currentThread() ;
		Thread thread = new Thread(() -> {
			for(int i = 0 ; i < 100 ; i++){
				if(i % 3 == 0){
					System.out.println("##开始礼让##");
					Thread.yield();
				}
				try {
					Thread.sleep(100);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				System.out.println(Thread.currentThread().getName() + "执行:" + i);
			}
		} , "玩耍的线程") ;
		thread.start();
		for(int i = 0 ; i < 100 ; i++){
			Thread.sleep(100);
			System.out.println("【优先】执行的线程: " + i);
		}

在上面的代码中每一次执行到计数器是3的倍数的时候,就会先执行【优先】进程。

同步问题

多线程程序面临的一个重要的问题就是同步问题,假设我们现在要卖出去一部分门票,总共三个票贩子往出卖,没卖一张票,我们的私有属性ticket就减一,当ticket小于0时停止程序,这个时候会发生什么呢?我们来看一个程序。

class MyThread implements Runnable{
	private int ticket = 10 ;
	@Override
	public void run(){
		while(ticket > 0){
			try {
				Thread.sleep(100);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			System.out.println(Thread.currentThread().getName() + "卖出一张门票 , 剩余:" + this.ticket--);
		}
	}
}
public class ThreadDemo{
	public static void main(String[] args) {
		MyThread mt = new MyThread() ;
		new Thread(mt , "票贩子A").start() ;
		new Thread(mt , "票贩子B").start() ;
		new Thread(mt , "票贩子C").start() ;
	}
}
票贩子A卖出一张门票 , 剩余:10
票贩子B卖出一张门票 , 剩余:10
票贩子C卖出一张门票 , 剩余:9
票贩子C卖出一张门票 , 剩余:8
票贩子A卖出一张门票 , 剩余:8
票贩子B卖出一张门票 , 剩余:8
票贩子C卖出一张门票 , 剩余:7
票贩子B卖出一张门票 , 剩余:6
票贩子A卖出一张门票 , 剩余:7
票贩子A卖出一张门票 , 剩余:5
票贩子B卖出一张门票 , 剩余:4
票贩子C卖出一张门票 , 剩余:5
票贩子C卖出一张门票 , 剩余:2
票贩子A卖出一张门票 , 剩余:3
票贩子B卖出一张门票 , 剩余:3
票贩子B卖出一张门票 , 剩余:1
票贩子A卖出一张门票 , 剩余:0
票贩子C卖出一张门票 , 剩余:-1

在上面的程序中我们定义了一个int类型的局部变量ticket表示我们的门票数,总共有10张,定义三个线程对象来卖这十张票,使用一个100ms的线程休眠来模拟网络延迟。这个时候我们观察执行结果会发现一个非常有意思的情况出现了。有很多张门票被好几个票贩子卖出,总共10张门票被卖了18次,最终甚至出现了门票数为-1的情况。什么原因导致的这种结果呢?原来是多个线程争着进行处理造成的,我们拿最终出现门票剩余0 的情况来分析。当第一个线程进行处理(休眠100ms)的时候,我们剩余的线程也开始处理,第一个线程判断此时的票数为1,开始卖票,但还没来的及将ticket减一,第二三个线程也开始处理,造成了错误。那么要想解决这个问题,就需要引入我们的synchronized关键字。

synchronized关键字

java提供了synchronized关键字供一个方法和一个线程可以独占一个资源一段时间,这段时间之内不允许其余线程进入处理。我们看一段代码

class MyThread implements Runnable{
	private int ticket = 10 ;
	public synchronized boolean isSale(){
		try {
			Thread.sleep(100);//模拟网络延迟
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		if(this.ticket > 0){
			System.out.println(Thread.currentThread().getName() + "售票 ,剩余:" + this.ticket--);
			return true ;
		}
		else
			return false ;
	}
	@Override
	public void run() {
		while(isSale()){;}
		}
	}
public class ThreadDemo {
	public static void main(String[] args){
		MyThread mt = new MyThread() ;
		new Thread(mt , "票贩子A").start();
		new Thread(mt , "票贩子B").start();
		new Thread(mt , "票贩子C").start();
	}
}
票贩子A售票 ,剩余:10
票贩子A售票 ,剩余:9
票贩子A售票 ,剩余:8
票贩子A售票 ,剩余:7
票贩子A售票 ,剩余:6
票贩子A售票 ,剩余:5
票贩子A售票 ,剩余:4
票贩子A售票 ,剩余:3
票贩子A售票 ,剩余:2
票贩子A售票 ,剩余:1

在上面的代码中我们使用了synchronized关键字,顺利完成了多线程售票任务。

生产者消费者模型

接下来说一说生产者消费者模型,这个模型就是利用两个线程,一个生产消息,生产出来之后另一个线程获取并消费,消费完成之后生产者再次生产。我们直接看一个代码。

public class ThreadDemo{
	public static void main(String[] args) {
		Message msg = new Message() ;
		new Thread(new Producer(msg)).start() ;
		new Thread(new Consumer(msg)).start() ;
	}
}
class Message{
	private String title ;
	private String content ;
	public void setTitle(String title){
		this.title = title ;
	}
	public void setContent(String content){
		this.content = content ;
	}
	public String getTitle(){
		return this.title ;
	}
	public String getContent(){
		return this.content ;
	}	
}
class Producer implements Runnable{
	private Message msg ;
	public Producer(Message msg){
		this.msg = msg ;
	}
	@Override
	public void run() {
		for(int i = 0 ; i < 100 ; i++){
			if(i % 2 == 0){
				this.msg.setTitle("张三");
				try {
					Thread.sleep(100);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
				this.msg.setContent("好人");
			}else{
				this.msg.setTitle("李四");
				try {
					Thread.sleep(100);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				this.msg.setContent("坏人");
			}
		}	
	}
}
class Consumer implements Runnable{
	private Message msg ;
	public Consumer(Message msg) {
		this.msg = msg ;
	}
	@Override
	public void run() {
		for(int i = 0 ; i < 100 ; i++){
			try {
				Thread.sleep(10);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println(this.msg.getTitle() + "-" + this.msg.getContent());
		}
	}
}

在这个代码中我创建了一个Message类,这个类定义了消息的标准,设置了属性和Getter/Setter方法。Producer线程利用Getter/Setter创建两种不同的线程,Consumer线程将获取到的消息输出。理论上来说创建的结果应该是“张三-好人”和“李四-坏人”。但是当我们加上一个100ms的线程休眠模拟延迟之后,问题就出现了。我们看一下输出结果:

张三-null
张三-null
张三-null
张三-null
张三-null
李四-好人
李四-好人
李四-好人
张三-坏人
张三-坏人
张三-坏人
张三-坏人

实验结果太多我就粘贴一部分有代表性的,可以看到,由于延迟的出现,生产者生产出的东西到了消费者那里就出现了严重的错误。
我们首先解决一下不同步的问题,我们可以将两个属性Getter/Setter方法合并为一个,并且加上synchronized 关键字限制线程争夺资源:

public synchronized void setMessage(String title , String content){
		this.title = title ;
		try {
			Thread.sleep(100);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		this.content = content ;	
	}
	public synchronized String getMessage(){
		try {
			Thread.sleep(10);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		return this.title + "-" + this.content ;
	}
张三-好人
李四-坏人
张三-好人
李四-坏人
李四-坏人
张三-好人
张三-好人
张三-好人
李四-坏人
李四-坏人
李四-坏人

运行代码我们可以看出同步的问题算是顺利解决了,每一个title都能匹配到正确的content,但是运行重复的问题还是没有解决。
这个时候就需要每次执行线程的时候增加一个判断,如果消费者尚未消费,那么生产者就等待。生产者没有生产,消费者就等待。我们来看一段代码。

public class ThreadDemo{
	public static void main(String[] args) {
		Message msg = new Message() ;
		new Thread(new Producer(msg)).start() ;
		new Thread(new Consumer(msg)).start() ;
	}
}
class Message{
	private String title ;
	private String content ;
	private boolean flag = true ;
	//如果flag==true,可以生产不能消费。如果flag==false,不能生产可以消费
	public synchronized void setMessage(String title , String content) throws InterruptedException{
		if(!this.flag){
			super.wait();
		}
		this.title = title ;
		this.content = content ;
		this.flag = false ;
		super.notify();//唤醒可能在等待的线程,不存在也没关系
	}
	public synchronized String getMessage() throws InterruptedException{
		if(this.flag){
			super.wait();
		}
		Thread.sleep(10);
		this.flag = true ;
		super.notify();
		return this.title + "-" + this.content ;
	}
}
class Producer implements Runnable{
	private Message msg ;
	public Producer(Message msg){
		this.msg = msg ;
	}
	@Override
	public void run() {
		for(int i = 0 ; i < 100 ; i++){
			if(i % 2 == 0){
				try {
					this.msg.setMessage("张三", "好人");
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}else{
				try {
					this.msg.setMessage("李四", "坏人");
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}	
	}
}
class Consumer implements Runnable{
	private Message msg ;
	public Consumer(Message msg) {
		this.msg = msg ;
	}
	@Override
	public void run() {
		for(int i = 0 ; i < 100 ; i++){
			try {
				System.out.println(this.msg.getMessage());
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}
}
张三-好人
李四-坏人
张三-好人
李四-坏人
张三-好人
李四-坏人
张三-好人
李四-坏人
张三-好人
李四-坏人
张三-好人
李四-坏人
张三-好人

上面这一段就是我们生产者-消费者的最终模式啦!在这段代码中,我们为Message类添加了一个布尔类型的flag变量,判断当前是否可以生产或是否可以消费。在我们的set/get方法中判断当前的状态,如果不符合执行线程的条件,就先让线程等待,否则执行操作,然后将flag值翻转,并唤醒等待线程。

生产着消费者练习

我们学习了生产者消费者,知道了如何在多线程之中实现同步和规避重复操作,下面我们来看一个例题:
例一:设计四个线程对象,两个负责加操作,两个负责减操作

public class AddSubOperation {
	public static void main(String[] args) {
		Resource res = new Resource() ;
		SubThread st = new SubThread(res) ;
		AddThread at = new AddThread(res) ;
		
		new Thread(at , "加法操作A").start();
		new Thread(at , "加法操作B").start();
		new Thread(st , "减法操作X").start();
		new Thread(st , "减法操作Y").start();
		
	}
}

class Resource{
	private int num = 0 ;
	private boolean flag = true ;//flag = true , 可加不可减。flag = false , 可减不可加
	
	public synchronized void add() throws InterruptedException{
		while(!this.flag)
			super.wait();
		Thread.sleep(100);
		System.out.println("【加法操作:" + Thread.currentThread().getName() + "】num = " + this.num++);
		this.flag = false ;
		super.notifyAll();
	}
	public synchronized void sub() throws InterruptedException{
		while(this.flag)
			super.wait();
		Thread.sleep(200) ;
		System.out.println("【减法操作:" + Thread.currentThread().getName() + "】num = " + this.num--);
		this.flag = true ;
		super.notifyAll();
	}
}

class AddThread implements Runnable{
	private Resource res ;
	public AddThread(Resource res){
		this.res = res ;
	}
	@Override
	public void run() {
		for(int i = 0 ; i < 100 ; i++){
			try {
				this.res.add();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}	
}

class SubThread implements Runnable{
	private Resource res ;
	public SubThread(Resource res){
		this.res = res ;
	}
	@Override
	public void run() {
		for(int i = 0 ; i < 100 ; i++){
			try {
				this.res.sub();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
}

这个代码比较简单,就是简单的生产者-消费者模式,利用布尔值控制线程的等待—唤醒操作。但是有一个 地方值得一说。可以看到我在Resource类中将判断布尔值的语句从if变成了while,这是因为如果使用if语句判断,有可能出现错误(我也不知道为什么,求路过的大神指点)。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值