javaSE-多线程

一.基本概念

1.程序,进程,线程

  • 程序:就是一段静态代码段
  • 进程:正在运行的程序,有生命周期-创建,就绪,执行,阻塞,销毁,进程是操作系统分配资源的最小单位
  • 线程:进程可进一步细分成多个线程,是程序内部的一条执行路径,它是调度和执行的基本单位,每个线程拥有独立的运行栈和程序计数器,他们共同享有堆内存,这使得线程之间的通信变得简单
  • 一个java程序,至少有三个线程,main()主线程,gc()垃圾回收线程,异常处理线程,如果发生异常会影响主线程

2.单核和多核CPU

  • 单核cpu实现的多线程是一种假的多线程,因为同一时间内,一个cpu只能执行一个任务,只不过cpu执行速度很快,每个任务都轮询执行,所以感觉是多线程执行
  • 如果是多核的话才能真正的实现多线程的效果

3.并行与并发

并行:多个任务同时执行

并发:同时交付给cpu多个任务,例如秒杀,双11上亿用户同时访问淘宝

二.线程的创建方法

1.方式一:继承Thread类

例子:

线程类

package test;

public class TestThread extends Thread{
	
	public TestThread(String threadName) {
		super(threadName);
	}

	@Override
	public void run() {
		for(int i = 0;i<5;i++){
			System.out.println(Thread.currentThread().getName()+"--->"+i);
			try {
				Thread.sleep(2000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		
	}
	
}

测试类

package test;

public class TestMain {
	public static void main(String[] args) {
		TestThread t1 = new TestThread("t1");
		TestThread t2 = new TestThread("t2");
		t1.start();
		t2.start();
	}
}

运行结果:

t1—>0
t2—>0
t2—>1
t1—>1
t2—>2
t1—>2
t2—>3
t1—>3
t2—>4
t1—>4

2.实现Runnable接口

例子:

线程类:

package test;

public class TestRunnable implements Runnable{
	@Override
	public void run() {
		for(int i = 0;i<5;i++){
			System.out.println(Thread.currentThread().getName()+"--->"+i);
			try {
				Thread.sleep(2000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}		
	}

}

测试类:

package test;

public class TestMain {
	public static void main(String[] args) {
		TestRunnable runnable = new TestRunnable();
		Thread t1 = new Thread(runnable, "t1");
		Thread t2 = new Thread(runnable, "t1");
		t1.start();
		t2.start();
	}
}

运行结果:

t1—>0
t1—>0
t1—>1
t1—>1
t1—>2
t1—>2
t1—>3
t1—>3
t1—>4
t1—>4

3.继承Thread和实现Runnable接口的区别

区别:

  • 继承Thread:线程代码存放在Thread子类的run方法中
  • 实现Runable接口:线程代码存放在接口子类的run方法中

使用Runable接口的好处:

多个线程可以共享同一个接口实现类的对象,非常适合相同线程处理同一份资源

三.多线程的使用

1.线程的调度

在cpu中分配任务的时候,是按照是时间片分配的,分得了时间片,那么就可以执行这一段代码

java的线程的调度方法:

  • 同优先级的线程,是先进先出队列(先到先服务),使用时间片策略
  • 对于高优先级,使用优先调度的抢占式策略

2.线程的优先级

java线程优先级:

  • MAX_PRIORITY:10 最高优先级
  • MIN_PRIOTITY:1 最低优先级
  • NORM_PRIOTITY:5 正常优先级

涉及到的方法:

getPriotity() 获取优先级

setPriotity(int newPriotity): 改变线程优先级

注意:

  • 线程创建时继承父线程的优先级
  • 优先级低只是获取调度的概率低了,并不一定在高优先级之后执行

3.线程的生命周期

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

一个线程的完整生命周期包含:

创建:当线程对象的创建的时候就处于创建状态

就绪:在线程创建后,调用了start()方法,该线程进入线程队列等待分配CPU时间片,

运行:当线程获得了时间片,并被调度的时候就进入了运行状态,执行run方法

阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中止自己的执行,进入阻塞状态

销毁:线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束

4.线程控制

方法功能
isAlive()判断线程是否还活着
getPriotity()获取线程的优先级
setPriotity(int ptiotity)设置线程的优先级(1-10)
Thread.sleep(int millis)将当前线程睡眠多少毫秒
join()将当前线程与该线程"合并",即等待该线程技术,再恢复当前线程的运行.
yield()让出CPU,当前线程进入就绪队列等待调度
wait()当前线程进入对象的wait pool
notify()唤醒对象的wait pool中的一个线程
notifyAll()唤醒对象的wait pool中的所有等待线程

5.线程的同步概念

多线程的安全问题:

多个线程执行的不确定性引起执行结果的不稳定

多个线程对账本的共享,会造成操作的不完整性,会破坏数据

例子:

模拟火车站买票,有三个售票员同时卖票

线程实现类:

package test;

public class TestThread01 implements Runnable{
	//设置一个公共的变量,这样多个线程就可以共享了
	private int ticket = 100;//一共100张票
	@Override
	public void run() {
		//有票的话就开始卖
		while(ticket>0){
			System.out.println("售票员"+Thread.currentThread().getName()+"售出票--->"+ticket);
			ticket--;
		}
		System.out.println("售票员"+Thread.currentThread().getName()+"这里没票了");
	}

}

测试类:

package test;

public class TestMain01 {
	public static void main(String[] args) {
		//创建三个售票员
		TestThread01 runnable = new TestThread01();
		Thread t1 = new Thread(runnable,"t1");
		Thread t2 = new Thread(runnable,"t2");
		Thread t3 = new Thread(runnable,"t3");
		
		t1.start();
		t2.start();
		t3.start();
	}
}

运行结果:

售票员t2售出票--->10
售票员t2售出票--->9
售票员t2售出票--->8
售票员t3售出票--->10
售票员t1售出票--->10
售票员t1售出票--->5
售票员t1售出票--->4
售票员t1售出票--->3
售票员t1售出票--->2
售票员t3售出票--->6
售票员t2售出票--->7
售票员t2这里没票了
售票员t3这里没票了
售票员t1售出票--->1
售票员t1这里没票了

很明显上面出现了错误,出售了相同的票,原因就是多个线程同时访问了相同的数据,并对它进行了操作

如何解决上面的问题呢?

接下来就引入了线程同步的概念

**线程同步:**在一个线程正在访问临界区资源(共享资源)时,其他线程不能访问该资源,相当于是单线程

三.线程同步

1.同步方法

java对于多线程的安全问题提供了专业的解决方案,那就是同步代码块

加锁某一部分代码

synchronized(对象){
    //需要被同步的代码
}

加锁某个方法

public synchronized void show(String name){}

2.同步锁机制

对于并发,要防止多个线程同时访问相同的资源,为了防止这种现象,就在这个资源上加锁,也就是该资源在同一时间只能被一个线程访问

synchronized的锁是什么

任意对象都可以作为同步锁。所有对象都自动含有单一的锁(监视器)

同步方法的锁:静态方法(类名.class)、非静态方法(this)

同步代码块:自己指定,很多时候也是指定为this或类名.class

注意:

  • 必须确保使用同一个资源的多个线程共用一把锁,这个非常重要,否则就无法保证共享资源的安全
  • 一个线程类中的所有静态方法共用同一把锁(类名.class),所有非静态方 法共用同一把锁(this),同步代码块(指定需谨慎)

3.释放锁

释放锁的操作:

  • 当前线程的同步方法、同步代码块执行结束
  • 当前线程在同步代码块、同步方法中遇到break、return终止了该代码块、 该方法的继续执行。
  • 当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致异常结束。
  • l 当前线程在同步代码块、同步方法中执行了线程对象的wait()方法,当前线 程暂停,并释放锁。

不会释放锁的操作:

  • 线程执行同步代码块或同步方法时,程序调Thread.sleep()、
    Thread.yield()方法暂停当前线程的执行
  • 线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放锁(同步监视器)

**注意:**应尽量避免使用suspend()和resume()来控制线程

4.Lock(锁)

概述:

  • 从JDK 5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。
  • java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的 工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象 加锁,线程开始访问共享资源之前应先获得Lock对象。
  • ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和 内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以 显式加锁、释放锁。

synchronized与Lock的对比:

  1. Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是 隐式锁,出了作用域自动释放

  2. Lock只有代码块锁,synchronized有代码块锁和方法锁

  3. 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)

优先使用的顺序:

Lock 同步代码块(已经进入了方法体,分配了相应资源) à 同步方法(在方法体之外)

5.使用线程同步解决上面的售票问题

5.1使用synchroinzed代码块

线程代码:

package test;

public class TestThread01 implements Runnable{
	//设置一个公共的变量,这样多个线程就可以共享了
	private int ticket = 10;//一共10张票
	//创建一个锁
	private Object objLock = new Object();
	@Override
	public void run() {
		
		//有票的话就开始卖
		while(ticket>0){
            //对一下代码加锁,同一时间只能一个线程访问一下代码
			synchronized (objLock) {
			System.out.println("售票员"+Thread.currentThread().getName()+"售出票--->"+ticket);
			ticket--;
		}
		System.out.println("售票员"+Thread.currentThread().getName()+"这里没票了");
		}
	}

}

测试代码:

package test;

public class TestMain01 {
	public static void main(String[] args) {
		//创建三个售票员
		TestThread01 runnable = new TestThread01();
		Thread t1 = new Thread(runnable,"t1");
		Thread t2 = new Thread(runnable,"t2");
		Thread t3 = new Thread(runnable,"t3");
		
		t1.start();
		t2.start();
		t3.start();
	}
}

运行结果:

售票员t1售出票--->10
售票员t1售出票--->9
售票员t1售出票--->8
售票员t1售出票--->7
售票员t1售出票--->6
售票员t1售出票--->5
售票员t1售出票--->4
售票员t1售出票--->3
售票员t1售出票--->2
售票员t1售出票--->1
售票员t3售出票--->0
售票员t1这里没票了
售票员t2售出票--->-1
售票员t2这里没票了
售票员t3这里没票了

上面结果表明,已经没有卖出重复的票了,但是最后还出售了一个0和-1的票,说明还是有问题 的

原因猜测是在t3和t2进入循环的时候,还是有票的(没票肯定进不了循环),但是进去之后t1线程正好将票数减少了1,然后t3将0卖了出去后,t2又进入这段代码,卖出了-1这个票

如何解决呢?

看如下代码:

package test;

import java.util.Random;

public class TestThread01 implements Runnable {
	// 设置一个公共的变量,这样多个线程就可以共享了
	private int ticket = 10;// 一共100张票
	// 创建一个锁
	private Object objLock = new Object();

	@Override
	public void run() {

		// 有票的话就开始卖
		while (ticket > 0) {
			synchronized (objLock) {
				if (ticket <= 0) {
					break;
				}
				System.out.println("售票员" + Thread.currentThread().getName()
						+ "售出票--->" + ticket);
				ticket--;
			}
		}
		System.out.println("售票员" + Thread.currentThread().getName() + "这里没票了");
	}

}

在进入同步代码段之后再加判断

//防止卖出0和-1的票
if (ticket <= 0) {
					break;
				}

运行结果:

售票员t1售出票--->10
售票员t1售出票--->9
售票员t1售出票--->8
售票员t1售出票--->7
售票员t1售出票--->6
售票员t1售出票--->5
售票员t1售出票--->4
售票员t1售出票--->3
售票员t3售出票--->2
售票员t3售出票--->1
售票员t3这里没票了
售票员t2这里没票了
售票员t1这里没票了

5.2使用Lock锁

线程代码:

package test;

import java.util.Random;
import java.util.concurrent.locks.ReentrantLock;

public class TestThread01 implements Runnable {
	// 设置一个公共的变量,这样多个线程就可以共享了
	private int ticket = 10;// 一共100张票
	// 创建一个显示锁
	private final ReentrantLock lock = new ReentrantLock();

	@Override
	public void run() {

		// 有票的话就开始卖
		while (ticket > 0) {
			// 加锁
			lock.lock();
			if (ticket <= 0) {
				break;
			}
			System.out.println("售票员" + Thread.currentThread().getName()
					+ "售出票--->" + ticket);
			ticket--;
			// 必须手动解锁
			lock.unlock();
		}

		System.out.println("售票员" + Thread.currentThread().getName() + "这里没票了");
	}

}

测试代码:

package test;

public class TestMain01 {
	public static void main(String[] args) {
		//创建三个售票员
		TestThread01 runnable = new TestThread01();
		Thread t1 = new Thread(runnable,"t1");
		Thread t2 = new Thread(runnable,"t2");
		Thread t3 = new Thread(runnable,"t3");
		
		t1.start();
		t2.start();
		t3.start();
	}
}

运行结果:

售票员t1售出票--->10
售票员t1售出票--->9
售票员t1售出票--->8
售票员t1售出票--->7
售票员t1售出票--->6
售票员t1售出票--->5
售票员t1售出票--->4
售票员t1售出票--->3
售票员t1售出票--->2
售票员t1售出票--->1
售票员t1这里没票了
售票员t3这里没票了

问题:

为什么少了一个打印没票的呢?运行了多次总是有一个没打印,而且程序还一直在运行着,没有正常关闭

原因:

在if判断的的时候,直接break了,没有解锁导致了其他线程一直卡在那里

所以一般在使用lock锁的时候一般都用这种格式

lock.lock();
try{
    //加锁的代码
}finally{
    lock.unlock();//解锁
}

四.死锁

1.概念

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

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

2.代码实现

package test;

public class TestDeadLock {
	public static void main(String[] args) {
		DeadLock deadLock = new DeadLock();
		//创建两个线程
		Thread t1 = new Thread(deadLock,"t1");
		Thread t2 = new Thread(deadLock,"t2");
		//启动线程
		t1.start();
		t2.start();
		System.out.println("主线程结束了");
	}
	public static class DeadLock implements Runnable{
		//创建两个资源
		Object obj1 = new Object();
		Object obj2 = new Object();

		@Override
		public void run() {
			//获取线程的名称,通过线程的名称表示线程
			String tName = Thread.currentThread().getName();
			//如果是t1的话就让它先锁住obj1然后再去锁obj2
			if(tName.equals("t1")){
				synchronized (obj1) {
					System.out.println("t1获取了资源obj1");
					//加一个延时,让t2有足够的时间获取资源obj2,执行休眠不会释放锁
					try {
						Thread.sleep(2000);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					//获取到obj1后再去获取obj2
					synchronized (obj2) {
						System.out.println("t1获取了资源obj2");
					}
				}
			}else if(tName.equals("t2")){
				synchronized (obj2) {
					System.out.println("t2获取了资源obj2");
					//加一个延时,让t1有足够的时间获取资源obj1,执行休眠不会释放锁
					try {
						Thread.sleep(2000);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					//获取到obj2后再去获取obj1
					synchronized (obj1) {
						System.out.println("t1获取了资源obj2");
					}
				}
			}
		}
	} 
}

执行结果:

主线程结束了
t1获取了资源obj1
t2获取了资源obj2

以上就形成了死锁,获取到自己的资源后,还要获取对方的资源,然后就卡住了.

五.线程通信

1.线程通信使用的方法

方法作用
wait()令当前线程挂起并放弃CPU、同步资源并等待,使别的线程可访问并修改共享资源,而当 前线程排队等候其他线程调用notify()或notifyAll()方法唤醒,唤醒后等待重新获得对监视器的所有 权后才能继续执行。
notify()唤醒正在排队等待同步资源的线程中优先级最高者结束等待
notifyAll()唤醒正在排队等待资源的所有线程结束等待

注意:

这三个方法只有在synchronized方法或synchronized代码块中才能使用,否则会报java.lang.IllegalMonitorStateException异常
因为这三个方法必须由锁对象调用,而任意对象都可以作为synchronized的同步锁, 因此这三个方法只能在Object类中声明

2.线程通信的案例:消费者与生产者

代码实现:

主类:

package homework;
/**
 * @author Antg
 * @date 2021年7月2日
 * @Description
 */
public class HomeWork {
	public static void main(String[] args) {
		/*******完成线程通信(生产者消费者)******/
		TestProducerAndConsumer.test();
	}
}

TestProducerAndConsumer类

package homework;


import producer_consumer.Consumer;
import producer_consumer.NoodlesPool;
import producer_consumer.Producer;

public class TestProducerAndConsumer {
	//测试生产者消费者
	public static void test(){
		int pNum = 3;//生产者数量
		int cNum = 4;//消费者数量
		int productivity = 10;//生产者生产力
		int allNum = pNum*productivity;//总共可以生产的数量
		int poolSize = 5;//资源池最大值
		
		//创建一个面馆
		NoodlesPool pool = new NoodlesPool(poolSize,allNum);
		
		Consumer consumer = new Consumer(pool);
		for(int i=1;i<cNum;i++){
			Thread ct = new Thread(consumer,"顾客-"+i);
			//顾客要吃面
			ct.start();
		}
		
		Producer p = new Producer(pool,productivity);
		for(int i = 1;i<=pNum;i++){
			Thread pt = new Thread(p,"师傅-"+i);
			//削面师傅开始做饭
			pt.start();
		}
	}
}

生产者类:

package producer_consumer;

/**
 * 
 * @author Antg
 * @date 2021年7月2日
 * @Description 生产者(削面师傅) 默认每个削面师傅每天削面10碗
 */
public class Producer implements Runnable {
	private NoodlesPool pool;// 先给削面师傅一个放削面的柜台
	private int productivity = 10;// 削面师傅的生产力(默认每天10碗面)

	public Producer(NoodlesPool pool) {
		this(pool, 10);// 生产力默认是10
	}

	public Producer(NoodlesPool pool, int productivity) {
		this.pool = pool;
		this.productivity = productivity;
	}

	@Override
	public void run() {

		// 让削面师傅削面10碗
		for (int i = 0; i < productivity; i++) {
			// 同一时间,只能一个线程访问pool
			synchronized (pool) {

				// 判断前台是否有位置放面
				while (!pool.ifHavingSpace()) {
					System.out.println("前台放满");
						// 如果没有位置,就让师傅等一会
						try {
							pool.wait();
						} catch (InterruptedException e) {
							e.printStackTrace();
						}
				}
				// 当前台有位置的时候就让师傅削面
				pool.notifyAll();
				// 获取订单编号
				int orderNum = pool.getOrderNum();
				// 削面
				Noodles noodles = new Noodles(orderNum, "刀削面-" + orderNum, 9.5);
				// 放在前台
				pool.add(noodles);
				// 打印日志
				System.out.println(Thread.currentThread().getName() + "做了削面:"
						+ noodles.getName());
			}
				// 做完一碗面后休息0~2秒
				double randomTime = Math.random()*2;
				try {
					Thread.sleep((long)(1000*randomTime));
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			
		}

	}

}

消费者类:

package producer_consumer;
/**
 * 
 * @author Antg
 * @date 2021年7月2日
 * @Description 消费者
 */
public class Consumer implements Runnable {
	private NoodlesPool pool;

	public Consumer(NoodlesPool pool) {
		this.pool = pool;
	}

	@Override
	public void run() {
		// 让消费者不断消费(吃一碗面大概3~4秒钟)
		while (true) {
			synchronized (pool) {
				//如果今天的面卖完了就结束线程
				if(pool.ifClose()){
					System.out.println("关店了");
					System.exit(0);
				}
				// 如果没有面就等待一下
				while (!pool.ifHavingNoodles()) {
					System.out.println("暂时没有面,等待中.");
						try {
							pool.wait();
						} catch (InterruptedException e) {
							// TODO Auto-generated catch block
							e.printStackTrace();
						}
				}
				// 有面了唤醒所有顾客
				pool.notifyAll();
				//消费面
				Noodles noodles = pool.poll();
				System.out.println(Thread.currentThread().getName()+" 吃了 "+noodles.getName());
			}
				//吃一碗面需要0~3秒
				double randomTime = Math.random()*3;
				try {
					Thread.sleep((long)(1000*randomTime));
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
				
			
		}
	}

}

资源池类:

package producer_consumer;

import java.awt.List;
import java.util.LinkedList;

/**
 * 
 * @author Antg
 * @date 2021年7月2日
 * @Description 刀削面池
 */
public class NoodlesPool {
	public LinkedList<Noodles> noodlesList = new LinkedList<Noodles>();// 存储面的柜台
	public int poolSize = 10;// 最多可以存放多少碗面(默认为10)
	public int curruntNum = 0;// 当前存储了多少碗面
	public int orderNumber = 1;//订单编号
	public int sellNumber = 0;//售出数量	
	public int allNum;//面馆最多可以做多少面条
	
	public NoodlesPool(int allNum){
		this.allNum = allNum;
	}
	
	public NoodlesPool(int poolSize,int allNum) {
		this.poolSize = poolSize;
		this.allNum = allNum;
	}

	// 生产刀削面的方法
	public void add(Noodles noodles) {
		noodlesList.add(noodles);
	}

	// 消费刀削面的方法
	public Noodles poll() {
		Noodles noodles = noodlesList.poll();
		this.sellNumber++;
		return noodles;
	}
	
	//判断柜台是否空闲
	public boolean ifHavingSpace(){
		return noodlesList.size()<poolSize;
	}
	
	//判断柜台上是否还有面
	public boolean ifHavingNoodles(){
		return noodlesList.size()>0;
	}
	
	//获取订单编号
	public int getOrderNum(){
		return orderNumber++;
	}
	
	//判断该面馆是否打样
	public boolean ifClose() {
		return sellNumber>=allNum;
	}
}

资源实体类:

package producer_consumer;
/**
 * 
 * @author Antg
 * @date 2021年7月2日
 * @Description
 * 刀削面
 */
public class Noodles {
	private int id;
	private String name;
	private double price;
	
	public Noodles(int id, String name, double price) {
		super();
		this.id = id;
		this.name = name;
		this.price = price;
	}
	
	public int getId() {
		return id;
	}
	public void setId(int id) {
		this.id = id;
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public double getPrice() {
		return price;
	}
	public void setPrice(double price) {
		this.price = price;
	}
	@Override
	public String toString() {
		return "Noodles [id=" + id + ", name=" + name + ", price=" + price
				+ "]";
	}	
}

总结:

生产者消费者的关键代码:

生产者: 创建资源类对象并放入资源池中,在生产的过程中同一时间只能有一个生产者去访问资源池

消费者: 从资源池获取对象(删除链表中的元素).在消费的过程中同一时间只能有一个消费者去访问资源池

资源池:就是一个链表,这个资源池要加锁

资源实体类: 可以是任意实体

六.线程池

1.概述

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

**思路:**提前创建好多个线程,放入线程池中,使用时直接获取,使用完 放回池中。可以避免频繁创建销毁、实现重复利用。

优点:

  • 提高响应速度(减少了创建新线程的时间)
  • 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
  • 便于线程管理

2.线程池 API

JDK 5.0起提供了线程池相关API:ExecutorService 和 Executors

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Antgeek

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值