Java基础——多线程

目录

一、线程相关概念:

二、线程基本使用

2.1 创建线程的方式

2.1.1 继承Thread类

 2.1.2 实现 Runnable 接口

 2.1.3 继承 Thread vs 实现 Runnable 的区别

三、线程终止

3.1 线程常用方法

 3.2 用户线程和守护线程

四、线程的生命周期

 五、线程同步


一、线程相关概念:

  1. 程序:为完成特定任务、用某种语言编写的一组指令的集合。(简单而言:就是我们写的代码)
  2. 进程:运行中的程序。比如我们使用QQ,就启动了一个进展,操作系统会为该进程分配内存空间。进程为程序的一次执行过程,或正在运行的一个程序。是动态过程:有它自身的产生、存在和消灭的过程。
  3. 线程:由进程创建,为进程的一个实体。一个进程可以有多个线程
  4. 单线程:同一个时刻,只允许执行一个线程
  5. 多线程:同一个时刻,可以执行多个线程。比如一个迅雷进程,可以同时下载多个文件
  6. 并发:同一个时刻,多个任务交替执行,造成“貌似同时”的错觉。单核CPU完成的多任务就是并发
  7. 并行:同一个时刻,多个任务同时进行。多核CPU可以实现并行

二、线程基本使用

2.1 创建线程的方式

2.1.1 继承Thread类

public class Test{
    public static void main(String[] args) throws InterruptedException {
        Cat cat = new Cat();
        cat.run();
        cat.start();
        System.out.println("主线程继续执行" + Thread.currentThread().getName());//名字 main
        for(int i = 0; i < 60; i++) {
            System.out.println("主线程 i=" + i);
//让主线程休眠
            Thread.sleep(1000);
        }
    }
}


class Cat extends Thread{
    int times=0;
    @Override
    public void  run(){
        while (true){
            //该线程每隔 1 秒。在控制台输出 “喵喵, 我是小猫咪”
            System.out.println("喵喵, 我是小猫咪" + (++times) + " 线程名=" + Thread.currentThread().getName());
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if(times==10){
                break;
            }
        }
    }
}

喵喵, 我是小猫咪1 线程名=main
喵喵, 我是小猫咪2 线程名=main
喵喵, 我是小猫咪3 线程名=main
喵喵, 我是小猫咪4 线程名=main
喵喵, 我是小猫咪5 线程名=main
喵喵, 我是小猫咪6 线程名=main
喵喵, 我是小猫咪7 线程名=main
喵喵, 我是小猫咪8 线程名=main
喵喵, 我是小猫咪9 线程名=main
喵喵, 我是小猫咪10 线程名=main
主线程继续执行main
主线程 i=0
喵喵, 我是小猫咪11 线程名=Thread-0
主线程 i=1
喵喵, 我是小猫咪12 线程名=Thread-0
主线程 i=2
喵喵, 我是小猫咪13 线程名=Thread-0
主线程 i=3
喵喵, 我是小猫咪14 线程名=Thread-0
主线程 i=4
喵喵, 我是小猫咪15 线程名=Thread-0
主线程 i=5
喵喵, 我是小猫咪16 线程名=Thread-0
主线程 i=6
喵喵, 我是小猫咪17 线程名=Thread-0
主线程 i=7
喵喵, 我是小猫咪18 线程名=Thread-0
主线程 i=8
喵喵, 我是小猫咪19 线程名=Thread-0
主线程 i=9
喵喵, 我是小猫咪20 线程名=Thread-0
主线程 i=10
喵喵, 我是小猫咪21 线程名=Thread-0
主线程 i=11
喵喵, 我是小猫咪22 线程名=Thread-0
主线程 i=12
喵喵, 我是小猫咪23 线程名=Thread-0
主线程 i=13
喵喵, 我是小猫咪24 线程名=Thread-0
主线程 i=14
喵喵, 我是小猫咪25 线程名=Thread-0
主线程 i=15
喵喵, 我是小猫咪26 线程名=Thread-0

  1. run 方法就是一个普通的方法, 没有真正的启动一个线程,就会把 run 方法执行完毕,才向下执行
  2. 当 main 线程启动一个子线程 Thread-0, 主线程不会阻塞, 会继续执行,主线程和子线程是交替执行
  3. 当一个类继承了 Thread 类, 该类就可以当做线程使用
  4. 我们会重写 run 方法,写上自己的业务代码
  5.  run Thread 类 实现了 Runnable 接口的 run 方法

 2.1.2 实现 Runnable 接口

Java为单继承,在某种情况下一个类可能已经继承了别的父类,这时就无法继承Thread。Java设计者提供了另外一个方式创建线程,就是通过实现Runnable接口来创建线程

public class Test{
    public static void main(String[] args) throws InterruptedException {
        Dog dog = new Dog();
        //dog.start(); 这里不能调用 start
        //创建了 Thread 对象,把 dog 对象(实现 Runnable),放入 Thread
        Thread thread = new Thread(dog);
        thread.start();
    }
}


class Dog implements Runnable { //通过实现 Runnable 接口,开发线程
    int count = 0;

    @Override
    public void run() { //普通方法
        while (true) {
            System.out.println("小狗汪汪叫..hi" + (++count) + Thread.currentThread().getName());
//休眠 1 秒
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if (count == 10) {
                break;
            }
        }
    }
}

小狗汪汪叫..hi1Thread-0
小狗汪汪叫..hi2Thread-0
小狗汪汪叫..hi3Thread-0
小狗汪汪叫..hi4Thread-0
小狗汪汪叫..hi5Thread-0
小狗汪汪叫..hi6Thread-0
小狗汪汪叫..hi7Thread-0
小狗汪汪叫..hi8Thread-0
小狗汪汪叫..hi9Thread-0
小狗汪汪叫..hi10Thread-0

 2.1.3 继承 Thread vs 实现 Runnable 的区别

继承Thread与实现Runnable接口本质上没有区别。Thread本身就是实现了Runnable接口。但是实现Runnable接口更加适合多个线程共享一个资源的情况,并且避免了单继承的限制,建议使用Runnable

两种方法模拟三个售票窗口售票100,编程如下:

public class SellTicket {
    public static void main(String[] args) {
        //测试
        // SellTicket01 sellTicket01 = new SellTicket01();
        // SellTicket01 sellTicket02 = new SellTicket01();
        // SellTicket01 sellTicket03 = new SellTicket01();
        //
        // //这里我们会出现超卖.. // sellTicket01.start();//启动售票线程
        // sellTicket02.start();//启动售票线程
        // sellTicket03.start();//启动售票线程
        System.out.println("===使用实现接口方式来售票=====");
        SellTicket02 sellTicket02 = new SellTicket02();
        new Thread(sellTicket02).start();//第 1 个线程-窗口
        new Thread(sellTicket02).start();//第 2 个线程-窗口
        new Thread(sellTicket02).start();//第 3 个线程-窗口
    }
}

//使用 Thread 方式
class SellTicket01 extends Thread {
    private static int ticketNum = 100;//让多个线程共享 ticketNum

    @Override
    public void run() {
        while (true) {
            if (ticketNum <= 0) {
                System.out.println("售票结束...");
                break;
            }
            //休眠 50 毫秒, 模拟
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("窗口 " + Thread.currentThread().getName() + " 售出一张票" + " 剩余票数=" + (--ticketNum));
        }
    }
}

//实现接口方式        
class SellTicket02 implements Runnable {
    private int ticketNum = 100;//让多个线程共享 ticketNum

    @Override
    public void run() {
        while (true) {
            if (ticketNum <= 0) {
                System.out.println("售票结束...");
                break;
            }
            //休眠 50 毫秒, 模拟
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("窗口 " + Thread.currentThread().getName() + " 售出一张票" + " 剩余票数=" + (--ticketNum));//1 - 0 - -1 - -2
        }
    }
}

窗口 Thread-2 售出一张票 剩余票数=12
窗口 Thread-1 售出一张票 剩余票数=11
窗口 Thread-2 售出一张票 剩余票数=10
窗口 Thread-0 售出一张票 剩余票数=11
窗口 Thread-2 售出一张票 剩余票数=9
窗口 Thread-0 售出一张票 剩余票数=7
窗口 Thread-1 售出一张票 剩余票数=8
窗口 Thread-1 售出一张票 剩余票数=5
窗口 Thread-2 售出一张票 剩余票数=6
窗口 Thread-0 售出一张票 剩余票数=6
窗口 Thread-1 售出一张票 剩余票数=4
窗口 Thread-0 售出一张票 剩余票数=4
窗口 Thread-2 售出一张票 剩余票数=4
窗口 Thread-1 售出一张票 剩余票数=3
窗口 Thread-0 售出一张票 剩余票数=2
窗口 Thread-2 售出一张票 剩余票数=1
窗口 Thread-1 售出一张票 剩余票数=0
窗口 Thread-2 售出一张票 剩余票数=0
售票结束...
窗口 Thread-0 售出一张票 剩余票数=0
售票结束...
售票结束...

 出现了超卖的情况

三、线程终止

当线程完成任务后,会自动退出

还可以通过使用变量来控制run方法退出的方式通知线程,即通知方式

3.1 线程常用方法

class ThreadDemo1 extends Thread{
    @Override
    public void run(){
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName()+i);
        }
        System.out.println(Thread.currentThread().getName()+"休眠中");
        try {
            Thread.sleep(20000);
        } catch (InterruptedException e) {
            System.out.println(Thread.currentThread().getName()+"被interrupt了");
        }
    }
}
public class SellTicket {
    @SneakyThrows
    public static void main(String[] args) {
        ThreadDemo1 threadDemo1 = new ThreadDemo1();
        threadDemo1.setName("yrh");
        threadDemo1.setPriority(Thread.MAX_PRIORITY);
        threadDemo1.start();
        System.out.println("默认优先级="+Thread.currentThread().getPriority());
        Thread.sleep(3000);
        threadDemo1.interrupt();
    }

默认优先级=5
yrh0
yrh1
yrh2
yrh3
yrh4
yrh5
yrh6
yrh7
yrh8
yrh9
yrh休眠中
yrh被interrupt了

InterruptedException出现一般都是因为在线程执行的时候被打断(interrupt),线程(A)不是自己打断自己,一般都是在另外一个线程(B)中执行中断方法(objA.interrupt())。 

 

public class SellTicket {
    public static void main(String[] args) throws InterruptedException {
        T t1 = new T();
        t1.start();
        t1.join();
        for (int i = 0; i <=10; i++) {
            Thread.sleep(50);
            System.out.println("main"+i);
        }
    }
}
class T extends  Thread{
    @Override
    public void run(){
        for (int i = 1; i <= 10; i++) {
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("JoinThread----"+i);
        }
    }
}

JoinThread----1
JoinThread----2
JoinThread----3
JoinThread----4
JoinThread----5
JoinThread----6
JoinThread----7
JoinThread----8
JoinThread----9
JoinThread----10
main0
main1
main2
main3
main4
main5
main6
main7
main8
main9
main10

 3.2 用户线程和守护线程

  1. 用户线程:也叫工作线程,当线程的任务执行完或通知方式结束
  2. 守护线程:一般是为工作线程服务,当所有用户线程结束,守护线程自动结束

线程名.setDaemon(true)即设置为守护线程

四、线程的生命周期

Java线程状态包括以下几种:

1. NEW:线程被创建但还没有启动。

2. RUNNABLE:线程正在运行或者准备运行,但是可能正在等待其他资源,比如CPU时间片或者I/O操作。

3. BLOCKED:线程被阻塞,因为它正在等待获取一个锁或者其他的监视器资源。

4. WAITING:线程正在等待其他线程执行特定的操作,比如等待另一个线程调用notify()或者notifyAll()方法。

5. TIMED_WAITING:线程正在等待其他线程执行特定的操作,但是等待时间有限制,比如等待一段时间后自动唤醒。

6. TERMINATED:线程已经执行完毕,已经退出。

public class ThreadState_ {
    public static void main(String[] args) throws InterruptedException {
        T t = new T();
        System.out.println(t.getName() + " 状态 " + t.getState());
        t.start();
        while (Thread.State.TERMINATED != t.getState()) {
            System.out.println(t.getName() + " 状态 " + t.getState());
            Thread.sleep(200);
        }
        System.out.println(t.getName() + " 状态 " + t.getState());
    }
}
class T extends Thread {
    @Override
    public void run() {
        while (true) {
            for (int i = 0; i < 10; i++) {
                System.out.println("hi " + i);
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            break;
        }
    }
}

Thread-0 状态 NEW
Thread-0 状态 RUNNABLE
hi 0
hi 1
Thread-0 状态 TIMED_WAITING
hi 2
hi 3
Thread-0 状态 TIMED_WAITING
hi 4
hi 5
Thread-0 状态 TIMED_WAITING
hi 6
hi 7
Thread-0 状态 TIMED_WAITING
hi 8
hi 9
Thread-0 状态 TIMED_WAITING
Thread-0 状态 TERMINATED

 五、线程同步

在多线程编程,一些敏感数据不允许多个线程同时访问,此时就使用同步访问技术,保证数据在任何同一时刻,最多一个线程访问。

5.1 同步具体方法-Synchronized

synchronized关键字最主要有以下3种应用方式,下面分别介绍

  1. 修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁
  2. 修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁
  3. 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。

 

 

public class SellTicket {
    public static void main(String[] args) {
        System.out.println("===使用实现接口方式来售票=====");
        SellTicket02 sellTicket02 = new SellTicket02();
        new Thread(sellTicket02).start();//第 1 个线程-窗口
        new Thread(sellTicket02).start();//第 2 个线程-窗口
        new Thread(sellTicket02).start();//第 3 个线程-窗口
    }
}

class SellTicket02 implements Runnable {
    private int ticketNum = 100;//让多个线程共享 ticketNum

    private boolean loop=true;

    public synchronized void sell(){
        if (ticketNum <= 0) {
            System.out.println("售票结束...");
            loop=false;
            return;
        }
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("窗口 " + Thread.currentThread().getName() + " 售出一张票" + " 剩余票数=" + (--ticketNum));
    }

    @Override
    public  void run() {
        while (loop) {
            sell();
        }
    }
}

5.1.1 修饰实例方法:

public class AccountingSync implements Runnable{
    //共享资源(临界资源)
    static int i=0;

    /**
     * synchronized 修饰实例方法
     */
    public synchronized void increase(){
        i++;
    }
    @Override
    public void run() {
        for(int j=0;j<1000000;j++){
            increase();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        AccountingSync instance=new AccountingSync();
        Thread t1=new Thread(instance);
        Thread t2=new Thread(instance);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(i);
    }

}

2000000

 如果不加t1.join()以及t2.join(),输出会为0,因为新起线程调用run(). 主线程不等待直接往下执行。输出时i还是0

上述代码中,我们开启两个线程操作同一个共享资源即变量i,由于i++;操作并不具备原子性,该操作是先读取值,然后写回一个新值,相当于原来的值加上1,分两步完成,如果第二个线程在第一个线程读取旧值和写回新值期间读取i的域值,那么第二个线程就会与第一个线程一起看到同一个值,并执行相同值的加1操作,这也就造成了线程安全失败,因此对于increase方法必须使用synchronized修饰,以便保证线程安全。此时我们应该注意到synchronized修饰的是实例方法increase,在这样的情况下,当前线程的锁便是实例对象instance,注意Java中的线程同步锁可以是任意对象。从代码执行结果来看确实是正确的,倘若我们没有使用synchronized关键字,其最终输出结果就很可能小于2000000,这便是synchronized关键字的作用。这里我们还需要意识到,当一个线程正在访问一个对象的 synchronized 实例方法,那么其他线程不能访问该对象的其他 synchronized 方法,毕竟一个对象只有一把锁,当一个线程获取了该对象的锁之后,其他线程无法获取该对象的锁,所以无法访问该对象的其他synchronized实例方法,但是其他线程还是可以访问该实例对象的其他非synchronized方法,当然如果是一个线程 A 需要访问实例对象 obj1 的 synchronized 方法 f1(当前对象锁是obj1),另一个线程 B 需要访问实例对象 obj2 的 synchronized 方法 f2(当前对象锁是obj2),这样是允许的,因为两个实例对象锁并不同相同,此时如果两个线程操作数据并非共享的,线程安全是有保障的,遗憾的是如果两个线程操作的是共享数据,那么线程安全就有可能无法保证了,如下代码将演示出该现象

public class AccountingSyncBad implements Runnable{
    static int i=0;
    public synchronized void increase(){
        i++;
    }
    @Override
    public void run() {
        for(int j=0;j<1000000;j++){
            increase();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        //new新实例
        Thread t1=new Thread(new AccountingSyncBad());
        //new新实例
        Thread t2=new Thread(new AccountingSyncBad());
        t1.start();
        t2.start();
        //join含义:当前线程A等待thread线程终止之后才能从thread.join()返回
        t1.join();
        t2.join();
        System.out.println(i);
    }
}

上述代码与前面不同的是我们同时创建了两个新实例AccountingSyncBad,然后启动两个不同的线程对共享变量i进行操作,但很遗憾操作结果是1066316而不是期望结果2000000,因为上述代码犯了严重的错误,虽然我们使用synchronized修饰了increase方法,但却new了两个不同的实例对象,这也就意味着存在着两个不同的实例对象锁,因此t1和t2都会进入各自的对象锁,也就是说t1和t2线程使用的是不同的锁,因此线程安全是无法保证的。解决这种困境的的方式是将synchronized作用于静态的increase方法,这样的话,对象锁就当前类对象,由于无论创建多少个实例对象,但对于的类对象拥有只有一个,所有在这样的情况下对象锁就是唯一的。下面我们看看如何使用将synchronized作用于静态的increase方法。

5.1.2 synchronized作用于静态方法

public class AccountingSyncClass implements Runnable{
    static int i=0;

    /**
     * 作用于静态方法,锁是当前class对象,也就是
     * AccountingSyncClass类对应的class对象
     */
    public static synchronized void increase(){
        i++;
    }

    /**
     * 非静态,访问时锁不一样不会发生互斥
     */
    public synchronized void increase4Obj(){
        i++;
    }

    @Override
    public void run() {
        for(int j=0;j<1000000;j++){
            increase();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        //new新实例
        Thread t1=new Thread(new AccountingSyncClass());
        //new心事了
        Thread t2=new Thread(new AccountingSyncClass());
        //启动线程
        t1.start();t2.start();

        t1.join();t2.join();
        System.out.println(i);
    }
}

 当synchronized作用于静态方法时,其锁就是当前类的class对象锁。由于静态成员不专属于任何一个实例对象,是类成员,因此通过class对象锁可以控制静态 成员的并发操作。需要注意的是如果一个线程A调用一个实例对象的非static synchronized方法,而线程B需要调用这个实例对象所属类的静态 synchronized方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的class对象,而访问非静态 synchronized 方法占用的锁是当前实例对象锁
 

5.1.3 synchronized同步代码块

public class AccountingSync implements Runnable{
    static AccountingSync instance=new AccountingSync();
    static int i=0;
    @Override
    public void run() {
        //省略其他耗时操作....
        //使用同步代码块对变量i进行同步操作,锁对象为instance
        synchronized(instance){
            for(int j=0;j<1000000;j++){
                    i++;
              }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(instance);
        Thread t2=new Thread(instance);
        t1.start();t2.start();
        t1.join();t2.join();
        System.out.println(i);
    }
}

除了使用关键字修饰实例方法和静态方法外,还可以使用同步代码块,在某些情况下,我们编写的方法体可能比较大,同时存在一些比较耗时的操作,而需要同步的代码又只有一小部分,如果直接对整个方法进行同步操作,可能会得不偿失,此时我们可以使用同步代码块的方式对需要同步的代码进行包裹,这样就无需对整个方法进行同步操作了

将synchronized作用于一个给定的实例对象instance,即当前实例对象就是锁对象,每次当线程进入synchronized包裹的代码块时就会要求当前线程持有instance实例对象锁,如果当前有其他线程正持有该对象锁,那么新到的线程就必须等待,这样也就保证了每次只有一个线程执行i++;操作。当然除了instance作为对象外,我们还可以使用this对象(代表当前实例)或者当前类的class对象作为锁

//this,当前实例对象锁
synchronized(this){
    for(int j=0;j<1000000;j++){
        i++;
    }
}

//class对象锁
synchronized(AccountingSync.class){
    for(int j=0;j<1000000;j++){
        i++;
    }
}

5.2 互斥锁 

 5.3 线程的死锁

当两个或两个以上的线程在执行过程中,因为争夺资源而造成的一种相互等待的状态,由于存在一种环路的锁依赖关系而永远地等待下去,如果没有外部干涉,他们将永远等待下去,此时的这个状态称之为死锁。

5.3.1 死锁的四个条件:

  1. 互斥条件:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用完释放。
  2. 请求和保持条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。
  3. 不剥夺条件:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。
  4. 环路等待条件:指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{A,B,C,···,Z} 中的A正在等待一个B占用的资源;B正在等待C占用的资源,……,Z正在等待已被A占用的资源。
    public class DeadLock implements Runnable {
        public int flag = 1;
        //静态对象是类的所有对象共享的
        private static Object o1 = new Object(), o2 = new Object();
    
        @Override
        public void run() {
            System.out.println("flag:{}"+flag);
            if (flag == 1) { //先锁o1,再对o2加锁,环路等待条件
                synchronized (o1) {
                    try {
                        Thread.sleep(500);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
    
                    synchronized (o2) {
                        System.out.println("1");
                    }
                }
            }
            if (flag == 0) {//先锁o2,在锁01
                synchronized (o2) {
                    try {
                        Thread.sleep(500);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    synchronized (o1) {
                        System.out.println("0");
                    }
                }
            }
        }
    
        public static void main(String[] args) {
            DeadLock td1 = new DeadLock();
            DeadLock td2 = new DeadLock();
            td1.flag = 1;
            td2.flag = 0;
            //td1,td2都处于可执行状态,但JVM线程调度先执行哪个线程是不确定的。
            //td2的run()可能在td1的run()之前运行
            new Thread(td1).start();
            new Thread(td2).start();
        }
    }
    

    1、当DeadLock 类的对象flag=1时(td1),先锁定o1,睡眠500毫秒
    2、而td1在睡眠的时候另一个flag==0的对象(td2)线程启动,先锁定o2,睡眠500毫秒
    3、td1睡眠结束后需要锁定o2才能继续执行,而此时o2已被td2锁定;
    4、td2睡眠结束后需要锁定o1才能继续执行,而此时o1已被td1锁定;
    5、td1、td2相互等待,都需要得到对方锁定的资源才能继续执行,从而死锁
     

5.4 释放锁的情况

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值