java之多线程02

文章介绍了Java中线程同步的几种方法,包括Thread的join()方法实现线程合并,CountDownLatch实现线程间的顺序执行,以及如何处理InterruptedException异常。此外,还讨论了线程的退出方式和线程状态,以及synchronized关键字在多线程同步中的应用。
摘要由CSDN通过智能技术生成

1.线程合并join

在这里插入图片描述
执行join方法后:
在这里插入图片描述

在Java中,可以使用Thread类的join()方法来实现线程合并(也称为线程等待)。线程合并指的是主线程等待某个线程执行完毕后再继续执行。

join()方法用于等待调用它的线程执行完毕。具体地,当一个线程调用另一个线程的join()方法时,它将会等待被调用线程执行完毕后再继续执行。

下面是一个简单的示例代码,演示了如何在主线程中合并其他线程:

public class ThreadJoinExample {
    public static void main(String[] args) {
        Thread myThread1 = new MyThread();
        Thread myThread2 = new MyThread();

        myThread1.start();
        myThread2.start();

        try {
            myThread1.join(); // 主线程等待myThread1执行完毕
            myThread2.join(); // 主线程等待myThread2执行完毕
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("所有线程执行完毕");
    }
}

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " 开始执行");
        // 线程的业务逻辑
        try {
            Thread.sleep(2000); // 模拟执行耗时操作
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " 执行完毕");
    }
}

在上面的代码中,myThread1和myThread2是两个并行执行的线程。通过调用它们各自的join()方法,主线程会等待这两个线程执行完毕后再继续执行。在主线程中,join()方法处于try-catch块中,以处理可能抛出的InterruptedException异常。

当线程执行时间较长时,使用join()方法可以保证在主线程中等待其他线程执行完毕,从而协调线程间的顺序和结果。

2.CountDownLatch(倒计时门栓)

CountDownLatch(倒计时门栓)是Java中的一个同步辅助类,它可以用于控制线程的执行顺序以及等待其他线程执行完毕。

CountDownLatch内部维护了一个计数器,当我们在创建CountDownLatch对象时,需要指定计数器的初始值。每当一个线程完成了自己的任务后,可以调用CountDownLatch的countDown()方法,将计数器的值减1。当计数器的值减到0时,所有在等待的线程都会被唤醒

下面是一个使用CountDownLatch的简单示例:

import java.util.concurrent.CountDownLatch;

public class CountDownLatchExample {
    public static void main(String[] args) {
        int threadCount = 3;
        CountDownLatch latch = new CountDownLatch(threadCount);

        for (int i = 0; i < threadCount; i++) {
            Thread thread = new WorkerThread(latch);
            thread.start();
        }

        try {
            latch.await(); // 主线程等待所有工作线程执行完毕
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("所有工作线程执行完毕");
    }
}

class WorkerThread extends Thread {
    private CountDownLatch latch;

    public WorkerThread(CountDownLatch latch) {
        this.latch = latch;
    }

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " 开始执行");
        // 模拟执行耗时操作
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " 执行完毕");
        latch.countDown(); // 每个工作线程执行完毕后,计数器减1
    }
}

在上面的示例中,创建了三个工作线程,每个线程执行自己的任务后,调用latch.countDown()方法将计数器减1。主线程使用latch.await()方法等待计数器为0才可以执行后面的程序。当计数器的值减到0时,主线程被唤醒,继续执行后续代码。

CountDownLatch常用于一个或多个线程等待其他线程的场景,例如主线程等待所有子线程执行完毕后再进行后续操作

3. InterruptedException

InterruptedException是Java中的一个异常类,它表示线程在等待、休眠或阻塞的过程中被中断了(接收到中断信号)。当线程被中断(接收到中断信号)时,如果线程处于等待状态(如调用了Object.wait()、Thread.sleep()、Thread.join()等方法),或者进入了被阻塞状态(如调用了I/O操作、同步块)时,会抛出InterruptedException。

当一个线程收到InterruptedException异常时,它的中断状态会被清除,即通过Thread.interrupted()方法会返回false。此时,可以根据具体情况采取相应的处理措施,例如终止线程的执行、释放资源等。

如何处理InterruptedException?
一般来说,有三种做法:

(1)不做处理,直接抛出

直接抛出并不会影响线程的状态,被中 断的线程还是会提前结束中断状态(如果在异常在上层得到处理),继续执行。如果异常没有被适当地处理或者没有合适的catch块来捕获该异常,那么异常将会传播到调用栈上层,并最终导致程序终止执行。

在这里插入图片描述

(2)捕获异常,再次调用interrupt方法,将中断状态重新设置为true;Thread.currentThread().interrupt();
在这里插入图片描述
加一个判断语句判断这个中断信号是否为true,if语句里应该再加个break也就是接收到这个信号为true的情况就跳出循环结束线程,因为出现异常之后他会自动清除这个中断信号也就是改为false,我们在catch里面重新将中断信号改为true返回用于下面if的判断

这样就保留了线程原有的状态,让线程继续等待下去

(3)捕获异常,不处理;(不推荐)

通常,在捕获到InterruptedException异常后,我们会在catch块中进行相应的处理,例如打印日志、恢复中断状态、中止线程等。

下面是一个简单的示例代码,演示了如何处理InterruptedException异常:

public class InterruptedExceptionExample {
    public static void main(String[] args) {
        Thread thread = new MyThread();
        thread.start();

        try {
            Thread.sleep(3000); // 主线程休眠3秒
            thread.interrupt(); // 在主线程休眠结束后中断子线程
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

class MyThread extends Thread {
    @Override
    public void run() {
        try {
            System.out.println(Thread.currentThread().getName() + " 开始执行");
            Thread.sleep(10000); // 子线程休眠10秒,模拟耗时操作
            System.out.println(Thread.currentThread().getName() + " 执行完毕");
        } catch (InterruptedException e) {
            System.out.println(Thread.currentThread().getName() + " 被中断了");
            // 恢复中断状态
            Thread.currentThread().interrupt();
        }
    }
}

在上面的示例中,主线程创建并启动了一个子线程。主线程休眠3秒后,调用thread.interrupt()方法中断子线程执行。在子线程中,通过捕获InterruptedException异常来处理中断事件。在catch块中,我们打印了一条被中断的消息,并通过Thread.currentThread().interrupt()方法恢复了中断状态。

请注意,处理InterruptedException异常时应根据具体情况采取适当的措施,例如尽快退出线程、释放资源等。

4.线程退出

stop

不推荐,线程退出方式粗暴,不管线程正在执行的任务,直接退出,可能丢失数据请添加图片描述

public class Test1 {

    public static void main(String[] args) {
        Thread t1 = new Thread(new MyTask());
        t1.start();

        Scanner in = new Scanner(System.in);
        System.out.println("输入1/0:0表示退出");
        int i = in.nextInt(); ///主线程进入IO阻塞

        if (i == 0) {
            t1.stop();
        }

        System.out.println("main over");
    }


    static class MyTask implements Runnable {

        @Override
        public void run() {
            while (true) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }

}


interrupt请添加图片描述

  • interrupt():发送中断信号(true)
    • 如果线程在阻塞状态,比如sleep(),join(),wait(),这时接收到中断信号会抛出一个异常InterruptException,同时中断信号清除(false)
    • 只是发送信号,不会对线程产生影响
  • static interrupted():得到中断信号(true),然后把中断信号设置成false
  • isInterrupted():得到中断信号,不会清除中断信号

5. 线程原理

线程的调度与时间片

由于 CPU 的计算频率非常高,每秒计算数十亿次,于是,可以将 CPU 的时间从毫秒的维度进行分段,每一小段叫做一个 CPU 时间片。不同的操作系统、不同的处理器,线程的 CPU 时间片长度都不同。假定操作系统的线程一个时间片的时间长度为 20 毫秒(比如 Windows XP),在一个 2GHz 的 CPU 上,那么一个时间片可以进行计算的次数是: 20 亿/(1000/20) =4 千万次,也就是说,一个时间片内的计算量是非常巨大的。 目前操作系统中主流的线程调度方式大都是:基于 CPU 时间片方式进行线程调度。线程只有得到 CPU 时间片,才能执行指令,处于执行状态;没有得到时间片的线程,处于就绪状态,等待系统分配下一个 CPU 时间片。由于时间片非常短,在各个线程之间快速地切换,表现出来特征是很多个线程在“同时执行”或者“并发执行”。线程的调度模型,目前主要分为两种调度模型:分时调度模型、抢占式调度模型。
(1)分时调度模型——系统平均分配 CPU 的时间片,所有线程轮流占用 CPU。分时调度模型在时间片调度的分配上,所有线程人人平等。
下图就是一个分时调度的简单例子:三个线程,轮流得到 CPU 时间片;一个线程执行时,另外两个线程处于就绪状态

在这里插入图片描述

(2)抢占式调度模型——系统按照线程优先级分配 CPU 时间片。优先级高的线程,优先分配 CPU 时间片;如果所有的就绪线程的优先级相同,那么会随机选择一个;优先级高的线程获取的 CPU 时间片相对多一些。 由于目前大部分操作系统都是使用抢占式调度模型进行线程调度。 Java 的线程管理和调度是委托给了操作系统完成的,与之相对应, Java 的线程调度也是使用抢占式调度模型。

线程

操作系统,线程的状态请添加图片描述

java的线程状态
得到java的线程状态

public Thread.State getState(); //返回当前线程的执行状态,一个枚举类型值

Thread.State 是一个内部枚举类,定义了 6 个枚举常量,分别代表 Java 线程的 6 种状态,具体如下

public static enum State {
   	
   	 NEW, //新建
    
     RUNNABLE, //可执行:包含操作系统的就绪、运行两种状态
    
     BLOCKED, //阻塞  -> 操作系统线程中的阻塞
    
     WAITING, //等待  -> 操作系统线程中的阻塞
    
     TIMED_WAITING, //计时等待 -> 操作系统线程中的阻塞
    
     TERMINATED; //终止
 }

在Thread.State 定义的 6 种状态中,有四种是比较常见的状态,它们是: NEW 状态、RUNNABLE状态、 TERMINATED 状态、 TIMED_WAITING 状态。

接下来,将线程的 6 种状态以及各种状态的进入条件,做一个总结。
1. NEW 状态 通过 new Thread(…)已经创建线程,但尚未调用 start()启动线程,该线程处于 NEW(新建)状态。虽然前面介绍了4种方式创建线程,但是其中的其他三种方式,本质上都是通过new Thread( )创建的线程,仅仅是创建了不同的 target 执行目标实例(如 Runnable 实例)。
2. RUNNABLE 状态 Java 把就绪(Ready)和执行(Running)两种状态合并为一种状态:可执行(RUNNABLE)状态(或者可运行状态)。调用了线程的 start()实例方法后,线程就处于就绪状态;此线程获取到 CPU 时间片后,开始执行 run( )方法中的业务代码,线程处于执行状态。

请添加图片描述

(1)就绪状态就绪状态仅仅表示线程具备运行资格,如果没有被操作系统的调度程序挑选中,线程就永远是就绪状态;当前线程进入就绪状态的条件,大致包括以下几种:

  • 调用线程的 start()方法,此线程进入就绪状态。
  • 当前线程的执行时间片用完。
  • 线程睡眠(sleep)操作结束。
  • 对其他线程合入(join)操作结束。
  • 等待用户输入结束。
  • 线程争抢到对象锁(Object Monitor)。
  • 当前线程调用了 yield 方法出让 CPU 执行权限。
    (2)执行状态
    线程调度程序从就绪状态的线程中选择一个线程,作为当前线程时线程所处的状态。这也是线程进入执行状态的唯一方式。
    3. BLOCKED 状态 处于阻塞(BLOCKED)状态的线程并不会占用 CPU 资源,以下情况会让线程进入阻塞状态:
    (1)线程等待获取锁 等待获取一个锁,而该锁被其他线程持有,则该线程进入阻塞状态。当其他线程释放了该锁,并且线程调度器允许该线程持有该锁时,该线程退出阻塞状态。(2) IO 阻塞 线程发起了一个阻塞式 IO 操作后,如果不具备 IO 操作的条件,线程会进入阻塞状态。 IO 包括磁盘 IO、 网络 IO 等。 IO 阻塞的一个简单例子:线程等待用户输入内容后继续执行。
    4. WAITING 状态
    处于 WAITING(无限期等待)状态的线程不会被分配 CPU 时间片,需要被其他线程显式地唤醒,才会进入就绪状态。线程调用以下 3 种方法,会让自己进入无限等待状态:
  • Object.wait() 方法,对应的唤醒方式为: Object.notify() / Object.notifyAll()。
  • Thread.join() 方法,对应的唤醒方式为:被合入的线程执行完毕。
  • LockSupport.park() 方法,对应的唤醒方式为: LockSupport.unpark(Thread)。
    5. TIMED_WAITING 状态 处于 TIMED_WAITING(限时等待)状态的线程不会被分配 CPU 时间片,如果指定时间之内没有被唤醒,限时等待的线程会被系统自动唤醒,进入就绪状态。以下 3 个方法会让线程进入限时等待状态:
  • Thread.sleep(time) 方法,对应的唤醒方式为: sleep 睡眠时间结束。
  • Object.wait(time) 方 法 , 对 应 的 唤 醒 方 式 为 : 调 用 Object.notify() /Object.notifyAll()去主动唤醒,或者限时结束。
  • LockSupport.parkNanos(time)/parkUntil(time) 方法,对应的唤醒方式为:线程调用配套的 LockSupport.unpark(Thread)方法结束,或者线程停止(park)时限结束。
    进入 BLOCKED 状态、 WAITING 状态、 TIMED_WAITING 状态的线程都会让出 CPU 的使用权;另外,等待或者阻塞状态的线程被唤醒后,进入 Ready 状态,需要重新获取时间片才能接着运行。
    6. TERMINATED 状态 线程结束任务之后,将会正常进入 TERMINATED(死亡)状态;或者说在线程执行过程中发生了异常(而没有被处理),也会导致线程进入死亡状态。

ps:如果在线程内部捕获了异常但没有进行处理,线程的运行状态不会受到直接的影响,线程仍然可以继续执行后续任务。这是因为捕获异常只是将异常对象暂时保存起来,而不对线程本身造成终止。

然而,这种情况下需要格外小心。未处理的异常可能会导致程序的意外行为或潜在的问题。如果异常没有得到适当的处理,它可能会被传播到更高级别的异常处理器或被默认的异常处理机制捕获,最终可能导致整个应用程序的崩溃。

因此,在捕获异常时,最好要进行适当的处理操作,比如记录日志、报告错误、回滚操作等,以确保程序的稳定性和健壮性。

6.synchronized的用法

synchronized 是Java中用于实现互斥锁(Mutex Lock)的关键字,用于控制多个线程对共享资源的安全访问。可以将synchronized 用于方法级别或代码块级别

方法级别的synchronized :将 synchronized 关键字应用于方法上,可以确保同一时间只有一个线程能够执行该方法。方法级别的synchronized 锁定的是对象实例。示例如下:


public synchronized void method() {
    // 同步的代码块
}

代码块级别的synchronized :将 synchronized 关键字应用于代码块上,通过指定锁的对象来实现对共享资源的同步访问。代码块级别的synchronized 可以更加灵活地选择锁的范围。示例如下:


public void method() {
    synchronized (lockObject) {
        // 同步的代码块
    }
}

在方法级别和代码块级别的 synchronized 中,锁定的对象可以是实例对象(通过synchronized 关键字修饰的实例方法)或者类对象(通过 synchronized 关键字修饰的静态方法)。当不同线程想要进入被 synchronized 修饰的方法或代码块时,它们会尝试获取锁。如果锁已被其他线程持有,则等待直到锁被释放后再执行。

这种方式的synchronized 是隐式锁,Java会自动管理锁的获取和释放。当线程离开同步代码块或方法时,会自动释放锁。

需要注意的是,synchronized 锁定的是对象实例本身,不同实例之间的锁是相互独立的。如果多个线程操作的是不同的实例,它们可以同时执行被 synchronized 修饰的代码块或方法。

entrylist 和owner 与waitset

在多线程编程中,“entry list”、"owner"和"wait set"分别指的是以下概念:

1.Entry List(入口列表):Entry List是用于保存等待获取某个锁的线程的一个数据结构。当一个线程请求获取一个被其他线程占用的锁时,它会被放置在该锁的Entry List中。当锁被释放时,Entry List中的线程可以竞争获取锁。

2.Owner(所有者):Owner指的是当前拥有某个锁的线程。每个锁只能被一个线程所拥有,该线程即为锁的Owner。只有锁的Owner能够对锁进行操作,包括获取锁、释放锁等。当一个线程成功获取锁时,它会成为该锁的Owner。

3.Wait Set(等待集合):Wait Set是用于保存那些等待某个条件满足的线程的集合。当一个线程调用了锁对象的wait()方法后,它会释放锁并进入该锁的Wait Set等待条件满足。只有当条件满足时,该线程才会被唤醒并重新尝试获取锁。

这些概念通常与锁(如互斥锁)或条件变量相关。当多线程程序中的线程需要对共享资源进行同步访问时,可以使用锁和条件变量等机制来实现线程间的同步和通信。Entry List用于保存等待某个锁的线程,Owner用于标识当前拥有锁的线程,而Wait Set用于保存等待某个条件满足的线程。

需要注意的是,这些概念可能在不同的多线程编程模型或语言中有所差异,上述的解释是基于一般的多线程编程概念进行的解释。
请添加图片描述

这个图可以解释线程在抢锁之后的动作,

1.如果执行结束这个抢到锁的线程任务之后,释放锁,线程死亡
2.在执行这个线程任务的过程中释放锁,进入等待状态,线程在等待状态中,不参与CPU的竞争,直到被其他线程调用 notify() 方法唤醒

7.多线程自增i++案例解析

需求:
4个线程自增一个堆(共享的)里的对象的值

我们先来看一下i++的反编译结果:

javap -c Test.class

请添加图片描述
请添加图片描述
在这里插入图片描述

在这里插入图片描述
好了,那么我们来思考一下,这样四个线程同时进行i++,会出现什么问题?

可能会出现的问题就是
如果前面一个线程取到 i 的值为100还没来的及返回值,就被后面的线程也取到这个100的值,那么都返回之后就会出现少了一个i++对吧。那么我们就要通过加锁来保证不会出现问题

下面就是代码:

自增类

public class Plus {

    private int amount = 0;

    public void selfPlus() {
           synchronized (this){amount ++;} 
    }

    public int getAmount() {
        return amount;
    }

}

自增任务类

public class PlusTask implements Runnable {

    private Plus plus;

    public PlusTask() {
    }

    public PlusTask(Plus plus) {
        this.plus = plus;
    }

    @Override
    public void run() {
        for (int i = 0; i < 100000000; i++) {
            plus.selfPlus();
        }
    }

}

测试

    public static void main(String[] args) throws InterruptedException {
        Plus plus = new Plus();
        PlusTask plusTask = new PlusTask(plus);

        Thread t1 = new Thread(plusTask);
        Thread t2 = new Thread(plusTask);
        Thread t3 = new Thread(plusTask);
        Thread t4 = new Thread(plusTask);

        t1.start();
        t2.start();
        t3.start();
        t4.start();

        t1.join();
        t2.join();
        t3.join();
        t4.join();

        System.out.println("实际的值 = " + plus.getAmount());

    }

这里我要提一点就是如果不加锁的情况下,四个线程都可以对堆中存储的amount进行自增,如果一个线程join()之后,那么其他三个线程不是一直不动的,这一点要清楚

那么加锁是什么呢?

这里我们用的就是互斥锁里面的synchronized对代码块进行加锁保持只有一个线程能访问公共资源也就是堆中的amount

锁是什么呢?

锁是多线程编程中的一种同步机制,用于控制多个线程对共享资源的访问。在Java中,可以使用关键字 synchronized 和 Lock 接口来实现锁。

对于锁的不同类型,有以下三种常见的锁:

  • 1.互斥锁(Mutex Lock):也称为独占锁,是最基本的锁类型。在同一时刻,只允许一个线程持有该锁,并对共享资源进行访问。其他线程必须等待锁的释放才能访问。在Java中,可以使用 synchronized 关键字或 ReentrantLock 类来实现互斥锁。

  • 2.读写锁(ReadWrite Lock):也称为共享-独占锁。读写锁允许多个线程同时持有读锁并进行读操作,但只允许一个线程持有写锁并进行写操作。读锁可以同时被多个线程持有,以提高读取操作的并发性能。在Java中,可以使用 ReentrantReadWriteLock 类来实现读写锁。

  • 3.条件锁(Condition Lock):条件锁是在互斥锁的基础上引入了条件等待和通知的机制。条件锁允许线程在某个条件满足时等待,直到其他线程通过通知的方式唤醒等待线程。在Java中,可以使用 Condition 接口与互斥锁结合使用,例如 ReentrantLock 的 newCondition() 方法创建一个条件对象。

这些锁机制可以帮助控制多线程对共享资源的安全访问,避免数据竞争和不一致性。具体选择哪种锁取决于应用程序的需求和线程之间的交互模式。

8.多窗口买票 案例

需求:

比如模拟4个线程,也就是4个窗口同时买票

分析

理想状态
在这里插入图片描述

极端状态:
在这里插入图片描述

可能会出现把票卖成负数的情况,这是一个需要注意的点,下面代码我用if语句来判断是否为0,尽量不让票数成为负数。

还有就是共享资源多线程操作时,为了保证不出现问题,加锁是一个不错的选择!

下面用到sleep就是为了避免一个窗口把票卖完的情况,但用到sleep如果不用到 synchronized锁就很容易出现超卖现象。

在这里插入图片描述

分析之后我们就来用代码来实现把
注意我这个分析中的方法名字和代码中的方法名字并不一样
窗口类

package Thred.BuyTickets;

import java.util.concurrent.CountDownLatch;

public class MyThred2 extends Thread{
    private Tickets tickets;
    private CountDownLatch latch;

    @Override
    public void run() {
        while (true){
            synchronized (tickets){
                int i1 = 0;
                if ((i1 =tickets.checkTick()) ==0){
                    break;

                }
                try {
                    Thread.sleep(20);
                } catch (InterruptedException e) {

                    e.printStackTrace();
                }
                System.out.println(this.getName()+"\t"+tickets.sellTick());

            }
            latch.countDown();

        }

    }

    public MyThred2(String name, Tickets tickets) {
        super(name);
        this.tickets = tickets;
    }
    public MyThred2(String name, Tickets tickets,CountDownLatch latch) {
        super(name);
        this.tickets = tickets;
        this.latch = latch;
    }

}

票库类

package Thred.BuyTickets;

public class Tickets {
    private int tick;

    public Tickets(int tick) {
        this.tick = tick;
    }
    public int checkTick(){
        if (tick == 0){
            System.out.println("票售空了");
            return 0;
        }else {

            System.out.println("还剩"+tick+"张票");
            return 1;
    }
    }
public  int sellTick(){
        if (tick == 0){
            System.out.println("不好意思,票卖完了");
            return 0;
        }else {
            System.out.println("购票成功!");
            tick--;
            return 1;
        }

}}


测试类

package Thred.BuyTickets;

import java.util.concurrent.CountDownLatch;

public class Test11 {


    public static void main(String[] args) {
        int threadCount =100;
        CountDownLatch latch =new CountDownLatch(threadCount);
        Tickets tickets = new Tickets(100);
        MyThred2 a = new MyThred2("a", tickets,latch);
        MyThred2 b = new MyThred2("b", tickets,latch);
        MyThred2 c = new MyThred2("c", tickets,latch);
        MyThred2 d = new MyThred2("d", tickets,latch);
        a.start();
        b.start();
        c.start();
        d.start();
        try {
            latch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }


        System.out.println("关门喽");
    }
}

这里我想用 CountDownLatch来阻塞主线程等待票卖完之后再运行,但是呢,如果这里把计数器写成4,就不行,因为我这个代码中买了一张票你就让他计数器减一,那么买了四张票计数器不就为0,然后主线程不就直接执行了,所以这里这个计数器要写为和票数一样

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值