java 多线程 简书_Java 多线程

要想了解多线程,必须先了解线程,而要想了解线程,必须先了解进程,因为线程是依赖于进程而存在。

进程

正在执行的应用程序

进程是系统进行资源分配和调用的独立单位。每一个进程都有它自己的内存空间和系统资源。

线程

进程的执行单位,执行路径

在同一个进程内又可以执行多个任务,而这每一个任务我就可以看出是一个线程。

是程序的执行单元,执行路径。是程序使用CPU的最基本单位。

单线程

一个应用程序只有一条执行路径

多线程

一个应用程序有多条执行路径

多进程的意义

提高CPU的使用率

单进程的计算机只能做一件事情,而我们现在的计算机都可以做多件事情。

单CPU在某一个时间点上只能做一件事情。而我们在玩游戏,或者听音乐的时候,是CPU在做着程序间的高效切换让我们觉得是同时进行的。

多线程的意义

多线程的存在,不是提高程序的执行速度。其实是为了提高应用程序的使用率。

程序的执行其实都是在抢CPU的资源,CPU的执行权。

多个进程是在抢这个资源,而其中的某一个进程如果执行路径比较多,就会有更高的几率抢到CPU的执行权。

我们是不敢保证哪一个线程能够在哪个时刻抢到,所以线程的执行有随机性。

Java程序的运行原理及JVM启动是多线程的么

1.Java命令去启动JVM,JVM会启动一个进程,该进程会启动一个主线程。主线程去调用某个类的 main方法 。

2.是多线程的,至少有两个线程启动了,主线程和垃圾回收机制

多线程的实现

1.继承Thread类

public class MyThread extends Thread {

@Override

public void run() {

for (int i = 0; i < 10; i++) {

System.out.println(Thread.currentThread().getName()+"--->"+i);

}

}

}

public class MyThreadDemo {

public static void main(String[] args) {

MyThread myThread = new MyThread();

myThread.setName("Threa第一个");

myThread.start();

}

}

2.实现Runnable接口

public class MyRunnable implements Runnable {

@Override

public void run() {

for (int i = 0; i < 10; i++) {

System.out.println(Thread.currentThread().getName()+"--->"+i);

}

}

}

public class MyRunnableDemo {

public static void main(String[] args) {

MyRunnable myRunnable = new MyRunnable();

Thread thread = new Thread(myRunnable);

thread.setName("Runnable第一个");

thread.start();

}

}

问题

1.为什么要重写run()方法

run()方法里面封装的是被线程执行的代码

2.启动线程使用的是那个方法

start()

3.线程能不能多次启动

不能

4.run()和start()方法的区别

如直接调用run()方法只是普通的方法调用

start()是先启动线程,再由JVM调用run()方法

1.调度

分时调度:

所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间片

抢占式调度(Java采用这种方式):

优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的 CPU 时间片相对多一些。

2.调度优先级

public final int getPriority()

public final void setPriority(int newPriority)

优先级默认是5,范围是1-10

线程控制

1.休眠线程

public class ThreadSleep extends Thread {

@Override

public void run() {

for (int x = 0; x < 100; x++) {

System.out.println(getName() + ":" + x + ",日期:" + new Date());

// 睡眠

// 困了,我稍微休息1秒钟

try {

Thread.sleep(1000);

} catch (InterruptedException e) {

e.printStackTrace();

}

}

}

}

2.加入线程

/*

* public final void join():等待该线程终止。

*/

public class ThreadJoinDemo {

public static void main(String[] args) {

ThreadJoin tj1 = new ThreadJoin();

ThreadJoin tj2 = new ThreadJoin();

ThreadJoin tj3 = new ThreadJoin();

tj1.setName("李渊");

tj2.setName("李世民");

tj3.setName("李元霸");

tj1.start();

try {

tj1.join();

} catch (InterruptedException e) {

e.printStackTrace();

}

tj2.start();

tj3.start();

}

}

3.礼让线程

public class ThreadYield extends Thread {

@Override

public void run() {

for (int x = 0; x < 100; x++) {

System.out.println(getName() + ":" + x);

Thread.yield();

}

}

}

/*

* public static void yield():暂停当前正在执行的线程对象,并执行其他线程。

* 让多个线程的执行更和谐,但是不能靠它保证一人一次。

*/

public class ThreadYieldDemo {

public static void main(String[] args) {

ThreadYield ty1 = new ThreadYield();

ThreadYield ty2 = new ThreadYield();

ty1.setName("林青霞");

ty2.setName("刘意");

ty1.start();

ty2.start();

}

}

4.后台线程

/*

* public final void setDaemon(boolean on):将该线程标记为守护线程或用户线程。

* 当正在运行的线程都是守护线程时,Java 虚拟机退出。 该方法必须在启动线程前调用。

*/

public class ThreadDaemonDemo {

public static void main(String[] args) {

ThreadDaemon td1 = new ThreadDaemon();

ThreadDaemon td2 = new ThreadDaemon();

td1.setName("关羽");

td2.setName("张飞");

// 设置守护线程

td1.setDaemon(true);

td2.setDaemon(true);

td1.start();

td2.start();

Thread.currentThread().setName("刘备");

for (int x = 0; x < 5; x++) {

System.out.println(Thread.currentThread().getName() + ":" + x);

}

}

}

5.终止线程(掌握)

public class ThreadStop extends Thread {

@Override

public void run() {

System.out.println("开始执行:" + new Date());

// 我要休息10秒钟,亲,不要打扰我哦

try {

Thread.sleep(10000);

} catch (InterruptedException e) {

// e.printStackTrace();

System.out.println("线程被终止了");

}

System.out.println("结束执行:" + new Date());

}

}

/*

* public final void stop():让线程停止,过时了,但是还可以使用。

* public void interrupt():中断线程。 把线程的状态终止,并抛出一个InterruptedException。

*/

public class ThreadStopDemo {

public static void main(String[] args) {

ThreadStop ts = new ThreadStop();

ts.start();

// 你超过三秒不醒过来,我就干死你

try {

Thread.sleep(3000);

// ts.stop();

ts.interrupt();

} catch (InterruptedException e) {

e.printStackTrace();

}

}

}

线程的生命周期

6546eabe072d

多线程安全问题的原因

判断一个程序是否有线程安全问题的依据

1.是否有多线程环境

2.有共享数据

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

同步解决线程安全问题

把多个语句操作共享数据的代码给锁起来,让任意时刻只能有一个线程执行即可。

同步代码块

public class SellTicket implements Runnable {

// 定义100张票

private int tickets = 100;

//创建锁对象

private Object obj = new Object();

@Override

public void run() {

while (true) {

synchronized (obj) {

if (tickets > 0) {

try {

Thread.sleep(100);

} catch (InterruptedException e) {

e.printStackTrace();

}

System.out.println(Thread.currentThread().getName()

+ "正在出售第" + (tickets--) + "张票");

}

}

}

}

}

同步方法

//这个锁对象是this

private synchronized void sellTicket()

if (tickets > 0) {

try {

Thread.sleep(100);

} catch (InterruptedException e) {

e.printStackTrace();

}

System.out.println(Thread.currentThread().getName()

+ "正在出售第" + (tickets--) + "张票 ");

}

}

静态同步方法

//这里的锁对象是当前类的字节码文件对象

private static synchronized void sellTicket() {

if (tickets > 0) {

try {

Thread.sleep(100);

} catch (InterruptedException e) {

e.printStackTrace();

}

System.out.println(Thread.currentThread().getName()

+ "正在出售第" + (tickets--) + "张票 ");

}

}

}

同步的特点

同步的前提

多个线程

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

同步的好处

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

同步的弊端

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

线程安全的类

StringBuffer

Vector

Hashtable

如何把一个线程不安全的集合类变成一个线程安全的集合类用Collections工具类的方法即可。

Lock锁

虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁,为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock

Lock

void lock()

void unlock()

ReentrantLock

public class SellTicket implements Runnable {

// 定义票

private int tickets = 100;

// 定义锁对象

private Lock lock = new ReentrantLock();

@Override

public void run() {

while (true) {

try {

// 加锁

lock.lock();

if (tickets > 0) {

try {

Thread.sleep(100);

} catch (InterruptedException e) {

e.printStackTrace();

}

System.out.println(Thread.currentThread().getName()

+ "正在出售第" + (tickets--) + "张票");

}

} finally {

// 释放锁

lock.unlock();

}

}

}

}

死锁问题

同步弊端

效率低

如果出现了同步嵌套,就容易产生死锁问题

死锁问题及其代码

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

同步代码块的嵌套案例

public class MyLock {

// 创建两把锁对象

public static final Object objA = new Object();

public static final Object objB = new Object();

}

public class DieLock extends Thread {

private boolean flag;

public DieLock(boolean flag) {

this.flag = flag;

}

@Override

public void run() {

if (flag) {

synchronized (MyLock.objA) {

System.out.println("if objA");

synchronized (MyLock.objB) {

System.out.println("if objB");

}

}

} else {

synchronized (MyLock.objB) {

System.out.println("else objB");

synchronized (MyLock.objA) {

System.out.println("else objA");

}

}

}

}

}

/*

* 举例:

* 中国人,美国人吃饭案例。

* 正常情况:

* 中国人:筷子两支

* 美国人:刀和叉

* 现在:

* 中国人:筷子1支,刀一把

* 美国人:筷子1支,叉一把

*/

public class DieLockDemo {

public static void main(String[] args) {

DieLock dl1 = new DieLock(true);

DieLock dl2 = new DieLock(false);

dl1.start();

dl2.start();

}

}

线程间通信

生产者和消费者多线程体现(线程间通信问题)

以学生作为资源来实现的

资源类:Student

设置数据类:SetThread(生产者)

获取数据类:GetThread(消费者)

测试类:StudentDemo

public class Student {

private String name;

private int age;

private boolean flag; // 默认情况是没有数据,如果是true,说明有数据

public synchronized void set(String name, int age) {

// 如果有数据,就等待

if (this.flag) {

try {

this.wait();

} catch (InterruptedException e) {

e.printStackTrace();

}

}

// 设置数据

this.name = name;

this.age = age;

// 修改标记

this.flag = true;

this.notify();

}

public synchronized void get() {

// 如果没有数据,就等待

if (!this.flag) {

try {

this.wait();

} catch (InterruptedException e) {

e.printStackTrace();

}

}

// 获取数据

System.out.println(this.name + "---" + this.age);

// 修改标记

this.flag = false;

this.notify();

}

}

public class SetThread implements Runnable {

private Student s;

private int x = 0;

public SetThread(Student s) {

this.s = s;

}

@Override

public void run() {

while (true) {

if (x % 2 == 0) {

s.set("林青霞", 27);

} else {

s.set("刘意", 30);

}

x++;

}

}

}

public class GetThread implements Runnable {

private Student s;

public GetThread(Student s) {

this.s = s;

}

@Override

public void run() {

while (true) {

s.get();

}

}

}

*

* 分析:

* 资源类:Student

* 设置学生数据:SetThread(生产者)

* 获取学生数据:GetThread(消费者)

* 测试类:StudentDemo

*

* 问题1:按照思路写代码,发现数据每次都是:null---0

* 原因:我们在每个线程中都创建了新的资源,而我们要求的时候设置和获取线程的资源应该是同一个

* 如何实现呢?

* 在外界把这个数据创建出来,通过构造方法传递给其他的类。

*

* 问题2:为了数据的效果好一些,我加入了循环和判断,给出不同的值,这个时候产生了新的问题

* A:同一个数据出现多次

* B:姓名和年龄不匹配

* 原因:

* A:同一个数据出现多次

* CPU的一点点时间片的执行权,就足够你执行很多次。

* B:姓名和年龄不匹配

* 线程运行的随机性

* 线程安全问题:

* A:是否是多线程环境 是

* B:是否有共享数据 是

* C:是否有多条语句操作共享数据 是

* 解决方案:

* 加锁。

* 注意:

* A:不同种类的线程都要加锁。

* B:不同种类的线程加的锁必须是同一把。

*

* 问题3:虽然数据安全了,但是呢,一次一大片不好看,我就想依次的一次一个输出。

* 如何实现呢?

* 通过Java提供的等待唤醒机制解决。

*

* 等待唤醒:

* Object类中提供了三个方法:

* wait():等待

* notify():唤醒单个线程

* notifyAll():唤醒所有线程

* 为什么这些方法不定义在Thread类中呢?

* 这些方法的调用必须通过锁对象调用,而我们刚才使用的锁对象是任意锁对象。

* 所以,这些方法必须定义在Object类中。

*

* 最终版代码中:

* 把Student的成员变量给私有的了。

* 把设置和获取的操作给封装成了功能,并加了同步。

* 设置或者获取的线程里面只需要调用方法即可。

*/

public class StudentDemo {

public static void main(String[] args) {

//创建资源

Student s = new Student();

//设置和获取的类

SetThread st = new SetThread(s);

GetThread gt = new GetThread(s);

//线程类

Thread t1 = new Thread(st);

Thread t2 = new Thread(gt);

//启动线程

t1.start();

t2.start();

}

}

等待唤醒机制

Object类中提供了三个方法:

wait():等待

notify():唤醒单个线程

notifyAll():唤醒所有线程

为什么这些方法不定义在Thread类中呢?

这些方法的调用必须通过锁对象调用,而我们刚才使用的锁对象是任意锁对象。所以,这些方法必须定义在Object类中。

线程状态转换图

6546eabe072d

线程组

Java中使用ThreadGroup来表示线程组,它可以对一批线程进行分类管理,

Java允许 程序直接对线程组进行控制。

默认情况下,所有的线程都属于主线程组。

public final ThreadGroup getThreadGroup()

我们也可以给线程设置分组

Thread(ThreadGroup group, Runnable target, String name)

/*

* 线程组: 把多个线程组合到一起。

* 它可以对一批线程进行分类管理,Java允许程序直接对线程组进行控制。

*/

public class ThreadGroupDemo {

public static void main(String[] args) {

// method1();

// 我们如何修改线程所在的组呢?

// 创建一个线程组

// 创建其他线程的时候,把其他线程的组指定为我们自己新建线程组

method2();

// t1.start();

// t2.start();

}

private static void method2() {

// ThreadGroup(String name)

ThreadGroup tg = new ThreadGroup("这是一个新的组");

MyRunnable my = new MyRunnable();

// Thread(ThreadGroup group, Runnable target, String name)

Thread t1 = new Thread(tg, my, "林青霞");

Thread t2 = new Thread(tg, my, "刘意");

System.out.println(t1.getThreadGroup().getName());

System.out.println(t2.getThreadGroup().getName());

//通过组名称设置后台线程,表示该组的线程都是后台线程

tg.setDaemon(true);

}

private static void method1() {

MyRunnable my = new MyRunnable();

Thread t1 = new Thread(my, "林青霞");

Thread t2 = new Thread(my, "刘意");

// 我不知道他们属于那个线程组,我想知道,怎么办

// 线程类里面的方法:public final ThreadGroup getThreadGroup()

ThreadGroup tg1 = t1.getThreadGroup();

ThreadGroup tg2 = t2.getThreadGroup();

// 线程组里面的方法:public final String getName()

String name1 = tg1.getName();

String name2 = tg2.getName();

System.out.println(name1);

System.out.println(name2);

// 通过结果我们知道了:线程默认情况下属于main线程组

// 通过下面的测试,你应该能够看到,默任情况下,所有的线程都属于同一个组

System.out.println(Thread.currentThread().getThreadGroup().getName());

}

}

线程池

程序启动一个新线程成本是比较高的,因为它涉及到要与操作系统进行交互。而使用线程池可以很好的提高性能,尤其当程序中要创建大量生存期很短的线程时,更应该考虑使用线程池

1.线程池里的每一个线程代码结束后,并不会死亡,而是再次回到线程池中成为空闲状态,等待下一个对象来使用

2.在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实例

关闭线程池

public class MyRunnable implements Runnable {

@Override

public void run() {

for (int x = 0; x < 100; x++) {

System.out.println(Thread.currentThread().getName() + ":" + x);

}

}

}

/*

* 线程池的好处:线程池里的每一个线程代码结束后,并不会死亡,而是再次回到线程池中成为空闲状态,等待下一个对象来使用。

*

* 如何实现线程的代码呢?

* A:创建一个线程池对象,控制要创建几个线程对象。

* public static ExecutorService newFixedThreadPool(int nThreads)

* B:这种线程池的线程可以执行:

* 可以执行Runnable对象或者Callable对象代表的线程

* 做一个类实现Runnable接口。

* C:调用如下方法即可

* Future> submit(Runnable task)

* Future submit(Callable task)

* D:我就要结束,可以吗?

* 可以。

*/

public class ExecutorsDemo {

public static void main(String[] args) {

// 创建一个线程池对象,控制要创建几个线程对象。

// public static ExecutorService newFixedThreadPool(int nThreads)

ExecutorService pool = Executors.newFixedThreadPool(2);

// 可以执行Runnable对象或者Callable对象代表的线程

pool.submit(new MyRunnable());

pool.submit(new MyRunnable());

//结束线程池

pool.shutdown();

}

}

多线程实现方案三

实现Callable接口

步骤和刚才演示线程池执行Runnable对象的差不多。

但是还可以更好玩一些,求和案例演示

好处:

可以有返回值

可以抛出异常

弊端:

代码比较复杂,所以一般不用

不带泛型

mport java.util.concurrent.Callable;

//Callable:是带泛型的接口。

//这里指定的泛型其实是call()方法的返回值类型。

public class MyCallable implements Callable {

@Override

public Object call() throws Exception {

for (int x = 0; x < 100; x++) {

System.out.println(Thread.currentThread().getName() + ":" + x);

}

return null;

}

}

/*

* 多线程实现的方式3:

* A:创建一个线程池对象,控制要创建几个线程对象。

* public static ExecutorService newFixedThreadPool(int nThreads)

* B:这种线程池的线程可以执行:

* 可以执行Runnable对象或者Callable对象代表的线程

* 做一个类实现Runnable接口。

* C:调用如下方法即可

* Future> submit(Runnable task)

* Future submit(Callable task)

* D:我就要结束,可以吗?

* 可以。

*/

public class CallableDemo {

public static void main(String[] args) {

//创建线程池对象

ExecutorService pool = Executors.newFixedThreadPool(2);

//可以执行Runnable对象或者Callable对象代表的线程

pool.submit(new MyCallable());

pool.submit(new MyCallable());

//结束

pool.shutdown();

}

}

带泛型

/*

* 线程求和案例

*/

public class MyCallable implements Callable {

private int number;

public MyCallable(int number) {

this.number = number;

}

@Override

public Integer call() throws Exception {

int sum = 0;

for (int x = 1; x <= number; x++) {

sum += x;

}

return sum;

}

}

/*

* 多线程实现的方式3:

* A:创建一个线程池对象,控制要创建几个线程对象。

* public static ExecutorService newFixedThreadPool(int nThreads)

* B:这种线程池的线程可以执行:

* 可以执行Runnable对象或者Callable对象代表的线程

* 做一个类实现Runnable接口。

* C:调用如下方法即可

* Future> submit(Runnable task)

* Future submit(Callable task)

* D:我就要结束,可以吗?

* 可以。

*/

public class CallableDemo {

public static void main(String[] args) throws InterruptedException, ExecutionException {

// 创建线程池对象

ExecutorService pool = Executors.newFixedThreadPool(2);

// 可以执行Runnable对象或者Callable对象代表的线程

Future f1 = pool.submit(new MyCallable(100));

Future f2 = pool.submit(new MyCallable(200));

// V get()

Integer i1 = f1.get();

Integer i2 = f2.get();

System.out.println(i1);

System.out.println(i2);

// 结束

pool.shutdown();

}

}

匿名内部类实现多线程

/*

* 匿名内部类的格式:

* new 类名或者接口名() {

* 重写方法;

* };

* 本质:是该类或者接口的子类对象。

*/

public class ThreadDemo {

public static void main(String[] args) {

// 继承Thread类来实现多线程

new Thread() {

public void run() {

for (int x = 0; x < 100; x++) {

System.out.println(Thread.currentThread().getName() + ":"

+ x);

}

}

}.start();

// 实现Runnable接口来实现多线程

new Thread(new Runnable() {

@Override

public void run() {

for (int x = 0; x < 100; x++) {

System.out.println(Thread.currentThread().getName() + ":"

+ x);

}

}

}) {

}.start();

// 更有难度的

new Thread(new Runnable() {

@Override

public void run() {

for (int x = 0; x < 100; x++) {

System.out.println("hello" + ":" + x);

}

}

}) {

public void run() {

for (int x = 0; x < 100; x++) {

System.out.println("world" + ":" + x);

}

}

}.start();

}

}

定时器

定时器是一个应用十分广泛的线程工具,可用于调度多个定时任务以后台线程的方式执行。在Java中,可以通过Timer和TimerTask类来实现定义调度的功能

Timer

public Timer()

public void schedule(TimerTask task, long delay)

public void schedule(TimerTask task,long delay,long period)

TimerTask

public abstract void run()

public boolean cancel()

/*

* 定时器:可以让我们在指定的时间做某件事情,还可以重复的做某件事情。

* 依赖Timer和TimerTask这两个类:

* Timer:定时

* public Timer()

* public void schedule(TimerTask task,long delay)

* public void schedule(TimerTask task,long delay,long period)

* public void cancel()

* TimerTask:任务

*/

public class TimerDemo {

public static void main(String[] args) {

// 创建定时器对象

Timer t = new Timer();

// 3秒后执行爆炸任务

// t.schedule(new MyTask(), 3000);

//结束任务

t.schedule(new MyTask(t), 3000);

}

}

// 做一个任务

class MyTask extends TimerTask {

private Timer t;

public MyTask(){}

public MyTask(Timer t){

this.t = t;

}

@Override

public void run() {

System.out.println("beng,爆炸了");

t.cancel();

}

}

/*

* 定时器:可以让我们在指定的时间做某件事情,还可以重复的做某件事情。

* 依赖Timer和TimerTask这两个类:

* Timer:定时

* public Timer()

* public void schedule(TimerTask task,long delay)

* public void schedule(TimerTask task,long delay,long period)

* public void cancel()

* TimerTask:任务

*/

public class TimerDemo2 {

public static void main(String[] args) {

// 创建定时器对象

Timer t = new Timer();

// 3秒后执行爆炸任务第一次,每隔2秒再继续炸

t.schedule(new MyTask2(), 3000, 2000);

}

}

// 做一个任务

class MyTask2 extends TimerTask {

@Override

public void run() {

System.out.println("beng,爆炸了");

}

}

/*

* 需求:在指定的时间删除我们的指定目录(你可以指定c盘,但是我不建议,我使用项目路径下的demo)

*/

class DeleteFolder extends TimerTask {

@Override

public void run() {

File srcFolder = new File("demo");

deleteFolder(srcFolder);

}

// 递归删除目录

public void deleteFolder(File srcFolder) {

File[] fileArray = srcFolder.listFiles();

if (fileArray != null) {

for (File file : fileArray) {

if (file.isDirectory()) {

deleteFolder(file);

} else {

System.out.println(file.getName() + ":" + file.delete());

}

}

System.out.println(srcFolder.getName() + ":" + srcFolder.delete());

}

}

}

public class TimerTest {

public static void main(String[] args) throws ParseException {

Timer t = new Timer();

String s = "2014-11-27 15:45:00";

SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

Date d = sdf.parse(s);

t.schedule(new DeleteFolder(), d);

}

}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值