『JavaWeb』线程

本篇博客主要介绍Java中线程的相关概念。

什么是线程?


线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中是进程中的实际运作单位。一个线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程每条线程并行执行不同的任务。所以线程可以理解为进程中的一个执行流进程可以理解为线程组

  • 进程是资源分配的最小单位
  • 线程是CPU调度的最小单位

同一进程中的不同线程将共享该进程中的全部资源,如虚拟地址空间,文件描述符表和信号处理等但同一进程中的多个线程有各自的调用栈(call stack),自己的寄存器环境等

下面,我们通过代码来感受一下什么是线程

public class ThreadTest {
    private static class MyThread extends Thread {
        @Override
        public void run() {
            while (true) {
                System.out.println(this.getName() + " is running!");
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        MyThread t1 = new MyThread();
        t1.start();

        MyThread t2 = new MyThread();
        t2.start();

        MyThread t3 = new MyThread();
        t3.start();

        while (true) {
            System.out.println(Thread.currentThread().getName() + " is running!");
            Thread.sleep(3000);
        }
    }
}

在这里插入图片描述
我们可以使用jconsole工具来观察一个Java程序的线程情况,jconsole工具在JDK安装目录下的bin文件中,由于我们在配置Java环境的时候,已经将这个路径添加到环境变量,所以我们可以直接使用win + r进行操作,然后输入jconsole即可
在这里插入图片描述
在这里插入图片描述
可以看到除了main、Thread-0、Thread-1、Thread-2和Thread-3这些线程之外还有很多的其他线程,这些都是JVM启动的一些守护线程

多线程的优势


我们来看一个代码,观察一下多线程在某些场合的优势

public class ThreadTest {
    private static final long COUNT = 10_0000_0000L;

    public static void main(String[] args) throws InterruptedException {
        concurrency();

        serial();
    }

    /**
     * 并发
     * @throws InterruptedException
     */
    private static void concurrency() throws InterruptedException {
        long begin = System.currentTimeMillis();

        // 利用一个线程计算a的值
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                int a = 0;
                for (long i = 0; i < COUNT; ++i) {
                    --a;
                }
            }
        });
        thread.start();

        // 主线程内计算b的值
        int b = 0;
        for (long i = 0; i < COUNT; ++i) {
            --b;
        }
        // 等待thread线程运行结束
        thread.join();

        long end = System.currentTimeMillis();

        System.out.printf("并发: %d毫秒%n", end - begin);
    }

    /**
     * 串行
     */
    private static void serial() {
        long begin = System.currentTimeMillis();

        int a = 0;
        for (long i = 0; i < COUNT; ++i) {
            --a;
        }

        int b = 0;
        for (long i = 0; i < COUNT; ++i) {
            --b;
        }

        long end = System.currentTimeMillis();

        System.out.printf("串行: %d毫秒%n", end - begin);
    }
}

在这里插入图片描述
可以看出并发还是比串行要快一些的

线程的创建


继承Thread类


可以通过继承Thread类来创建一个线程类,该方法的好处是this代表的就是当前线程,不需要通过Thread.currentThread()来获取当前线程的引用。
我们来看一下代码

public class ThreadCreate {
    public static void main(String[] args) {
        MyThread thread = new MyThread("我的线程");
        thread.start();
    }
}

class MyThread extends Thread {
    public MyThread(String name) {
        super(name);
    }

    @Override
    public void run() {
        while (true) {
            System.out.println(this.getName() + " is running!");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

在这里插入图片描述
我们还可以以匿名类的方式来实现

public class ThreadCreate {
    public static void main(String[] args) {
        Thread thread = new Thread("我的线程") {
            @Override
            public void run() {
                while (true) {
                    try {
                        System.out.println(this.getName() + " is running!");
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        thread.start();
    }
}

在这里插入图片描述

实现Runnable接口


通过实现Runnable接口,并且调用Thread的构造方法时将Runnable对象作为target参数传入来创建线程对象。该方法的好处是可以规避类的单继承的限制;但需要通过Thread.currentThread()来获取当前线程的引用
下面看一下代码

public class ThreadCreate {
    public static void main(String[] args) {
        Thread thread = new Thread(new MyRunnable(), "我的线程");
        thread.start();
    }
}

class MyRunnable implements Runnable {
    @Override
    public void run() {
        while (true) {
            try {
                System.out.println(Thread.currentThread().getName() + " is running!");
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

在这里插入图片描述
除了上述实现一个MyRunnable类创建实例之外,我们还可以使用匿名类的方式来实现,下面看代码实现:

public class ThreadCreate {
    public static void main(String[] args) {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    try {
                        System.out.println(Thread.currentThread().getName() + " is running!");
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }, "我的线程");
        thread.start();
    }
}

在这里插入图片描述
还可以使用Lambda表达式来创建Runnable子类对象

public class ThreadCreate {
    public static void main(String[] args) {
        Thread thread = new Thread(()->{
            while (true) {
                try {
                    System.out.println(Thread.currentThread().getName() + " is running!");
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "我的线程");
        thread.start();
    }
}

在这里插入图片描述

Thread类常用方法


Thread类是JVM用来管理线程的一个类,换句话说,每个线程都有一个唯一的Thread对象与之关联
每个执行流,也需要一个对象来描述,而Thread类的对象就是用来描述一个线程执行流的,JVM会将这些Thread对象组织起来,用于线程调度,线程管理

Thread的常见构造方法


方法说明
Thread()创建线程对象
Thread(Runnable target)使用Runnable对象创建线程对象
Thread(String name)创建线程对象,并命名
Thread(Runnable target, String name)使用Runnable对象创建线程对象,并命名
Thread(ThreadGroup group, Runnable target)线程可以被用来分组管理,分号的组即线程组

Thread的几个常见的属性


属性获取方法
IDgetID()
名称getName()
状态getState()
优先级getPriority()
是否后台线程isDaemon()
是否存活isAlive()
是否被中断isInterrupted()
  • 关于后台线程,需要记住一点:JVM会在一个进程的所有非后台线程结束后,才会结束运行
  • 是否存活:可以简单的理解为run方法是否运行结束了

启动一个线程


我们可以通过覆写run方法来创建一个线程对象,但线程对象被创建出来并不意味着线程就开始运行了

  • 覆写run()方法是提供给线程要做的事情
  • 调用start()方法,线程才真正独立去执行了

一定要区分run和start方法的区别,下面我们通过代码来演示一下

public class ThreadDemo {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread("我的线程") {
            @Override
            public void run() {
                while (true) {
                    try {
                        System.out.println("我的线程");
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        thread.run();

        while (true) {
            System.out.println("main");
            Thread.sleep(1000);
        }
    }
}

在这里插入图片描述
我们用jconsole来观察一下
在这里插入图片描述
可以看到只有一个main线程,我们创建的线程名为“我的线程”的线程并没有被启动。thread.run();就是在调用thread对象的方法run();并不是在启动线程

下面,我们来看一下真正的启动线程

public class ThreadDemo {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread("我的线程") {
            @Override
            public void run() {
                while (true) {
                    try {
                        System.out.println("我的线程");
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        thread.start();

        while (true) {
            System.out.println("main");
            Thread.sleep(1000);
        }
    }
}

在这里插入图片描述
同样的,我们使用jconsole来观察一下
在这里插入图片描述
可以看到,我的线程和main线程都运行起来了

中断一个线程


中断一个线程有两种方式

  • 通过共享的标记来进行沟通
  • 通过调用interrupt()方法来通知

我们先来看一下通过共享的标记进行沟通的方法

public class ThreadDemo {
    private static boolean flag= false;

    public static void main(String[] args) throws InterruptedException {
        new Thread("我的线程") {
            @Override
            public void run() {
                while (!flag) {
                    try {
                        System.out.println(Thread.currentThread().getName() + " is running!");
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }.start();

        Thread.sleep(5000);
        flag= true;
    }
}

在这里插入图片描述
但是这种方式有一个小问题,那就是如果目标线程正在sleep(),那么目标线程不会被立即中断,而是等sleep()结束后才会结束中断

通过调用实例方法或者静态方法来中断一个线程
我们来看一下三个方法:

方法说明
public void interrupt()中断对象关联的线程,如果线程处于阻塞状态,则以异常的方式通知,否则设置标志位
public static boolean interrupted()判断当前线程的中断标志位是否被设置,调用后清除标志位
public boolean isInterrupted()判断当前对象关联的线程的标志位是否被设置,调用后不清除标志位

interrupt()方法比较好理解,就是中断对象关联的线程
下面,我们来看一下interrupted()和isInterrupted()的区别
在这里插入图片描述
在这里插入图片描述

  • 可以看到interrupted()方法内部调用的就是isInterrupted()方法,但是interrupted()会清除标志位,isInterrupted()不会清除标志位
  • interrupted()是静态方法,isInterrupted()是实例方法

对于这三个方法,我们可以理解为底层也是通过一个共享的标志位flag来实现的isInterrupted()方法只是返回这个flag的值,而interrupted()不仅返回这个flag的值,还会将这个flag的值置为false

我们再来看一下interrupt()方法

  • 通过thread对象调用interrupt()方法通知该线程停止运行
  • thread收到通知的方式有两种
    ① 如果线程调用了wait/join/sleep等方法而阻塞挂起,则以InterruptedException异常的形式通知,清除中断标志
    否则,只是内部的一个中断标志被设置,thread可以通过Thread.interrupted()方法或者thread.isInterrupt()方法来判断当前线程的中断标志是否被设置。其中前者在调用之后会清除中断标志

下面,我们来看一个代码

public class ThreadDemo {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread() {
            @Override
            public void run() {
                while (!Thread.interrupted()) {
                    try {
                        System.out.println(Thread.currentThread().getName() + " is running!");
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        Thread t2 = new Thread() {
            @Override
            public void run() {
                try {
                    while (!Thread.interrupted()) {
                        System.out.println(Thread.currentThread().getName() + " is running!");
                        Thread.sleep(1000);
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };

        t1.start();
        t2.start();

        Thread.sleep(3000);

        t1.interrupt();
        t2.interrupt();
    }
}

在这里插入图片描述
可以看到try…catch和while的位置不同,两个线程表现出来的结果是不同的

线程等待


有时候,我们需要等待一个线程完成它的工作后,才能进行自己下一步工作。这时候我们就需要一个方法等待线程的结束。
常用的线程等待的方法如下

方法说明
public void join()等待线程结束
public void join(long millis)等待线程结束,最多等millis毫秒
public void join(long millis, int nanos)等待线程结束,最多等millis毫秒nanos纳秒

我们来看一个代码,该代码可以实现threads数组中的线程挨个顺序执行

public class ThreadDemo {
    public static void main(String[] args) throws InterruptedException {
        Thread[] threads = new Thread[5];
        for (int i = 0; i < threads.length; ++i) {
            threads[i] = new Thread() {
                @Override
                public void run() {
                    for (int i = 0; i < 2; ++i) {
                        System.out.println(Thread.currentThread().getName());
                    }
                }
            };
        }

        for (int i = 0; i < threads.length; ++i) {
            threads[i].start();
            threads[i].join();
        }
    }
}

在这里插入图片描述
如果我们把threads[i].join()这句代码去掉会怎么样,我们来看一下:

public class ThreadDemo {
    public static void main(String[] args) throws InterruptedException {
        Thread[] threads = new Thread[5];
        for (int i = 0; i < threads.length; ++i) {
            threads[i] = new Thread() {
                @Override
                public void run() {
                    for (int i = 0; i < 2; ++i) {
                        System.out.println(Thread.currentThread().getName());
                    }
                }
            };
        }

        for (int i = 0; i < threads.length; ++i) {
            threads[i].start();
        }
    }
}

在这里插入图片描述
可以看到去掉join()顺序就乱了

线程的状态


线程的状态是一个枚举类型Thread.State,我们来看一下:

public class ThreadDemo {
    public static void main(String[] args) {
        for (Thread.State state : Thread.State.values()) {
            System.out.println(state);
        }
    }
}

在这里插入图片描述
在这里插入图片描述

  • 初始状态(NEW)只是安排了工作,还未开始行动
  • 运行中(RUNNABLE)CPU正在调度该线程
  • 就绪(READY)等待被系统调度
  • 等待(WAITING)、超时等待(TIMED_WAITING)、阻塞(BLOCKED)排队等着其他事情
  • 终止(TERMINATED)工作全部完成

线程的超时等待、阻塞和等待都对应操作系统中进程的阻塞状态。只不过Java中对阻塞状态进行了细分。划分出了三种

线程安全


我们来看一个例子来体会一下什么是线程不安全
我们启动20个线程来对同一个变量进行自增操作,每个线程自增一万次

public class ThreadDemo {
    private static int num = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread[] threads = new Thread[20];
        for (int i = 0; i < threads.length; ++i) {
            threads[i] = new Thread() {
                @Override
                public void run() {
                    for (int i = 0; i < 10000; ++i) {
                        ++num;
                    }
                }
            };
        }

        for (int i = 0; i < threads.length; ++i) {
            threads[i].start();
        }

        while (Thread.activeCount() > 1) {
            Thread.yield();
        }

        System.out.println("num = " + num);
    }
}

在这里插入图片描述
我们预期的结果是20万,但是这里只有17万多。这种现象我们可以称之为线程不安全,为什么会出现线程不安全的问题呢?

线程不安全的原因


原子性


我们把一段代码想像成一个房间,每个线程就是要进入这个房间的人。如果没有任何机制保证,A进入房间之后,还没有出来;B是不是也可以进入房间,打断A在房间里的隐私。这个就是不具备原子性的。
如何解决这个问题呢?我们可以给房间加一把锁,A进去把门锁上,其他人就进不来了,这样我们就保证了这段代码的原子性了
有时也把这个现象叫做同步互斥,表示操作是互相排斥的

一条Java语句不一定是原子的,也不一定只是一条指令。
比如刚才我们看到的++num,其实是由三步操作组成的

  1. 从内存中把数据读到CPU
  2. 进行数据更新
  3. 把数据写回到内存中

不保证原子性会给多线程带来什么问题
如果一个线程正在对一个变量进行操作,中途其他线程插入进来了,如果这个操作被打断了,结果就可能是错误的

可见性


JVM将内存组织为主内存和工作内存两部分

  • 主内存包括本地方法区和堆
  • 工作内存每个线程都有一个工作内存,工作内存中主要包括两个部分,一个是属于该线程私有的栈和对主存部分变量拷贝的寄存器

所有的变量都存储在主内存中,对于所有线程都是共享的;线程之间无法直接访问对方的工作内存中的变量,线程间变量的传递均需要通过主内存来完成
在这里插入图片描述
JVM执行过程中,共享变量在多线程之间不能及时看到改变,这个就是可见性问题

有序性


一段代码逻辑如下

  1. 去前台取U盘;
  2. 去教室写10分钟作业;
  3. 去前台取下快递。

如果是在单线程情况下,JVM、CPU指令集会对其进行优化,比如,按照1->3->2的方式执行,也是没问题的,可以少跑一次前台。这种叫做指令重排序
但是如果在多线程场景下就有问题了,可能快递是在你写作业的10分钟内被另一个线程放过来的,或者被人变过了,如果指令重排序,代码就会是错误的。

有序性:如果在线程内观察,所有操作都是有序的;如果在一个线程中观察另一个线程,所有操作都是无序的。前半句指:线程内表现为串行语义,后半句是指:指令重排现象和工作内存与主内存同步延迟现象。

synchronized关键字


对于前面的线程不安全的问题,我们可以使用synchronized关键字来实现线程安全
synchronized的用法

  • synchronized修饰普通方法,此时锁的是当前实例的对象
  • synchronized修饰静态方法,此时锁的是类的class对象
  • synchronized修饰代码块,此时锁的是括号内的对象

我们分别使用synchronized关键字来解决一下前面的线程不安全问题
静态同步方法

public class ThreadDemo {
    private static int num = 0;

    public static synchronized void increment() {
        ++num;
    }

    public static void main(String[] args) {
        Thread[] threads = new Thread[20];
        for (int i = 0; i < threads.length; ++i) {
            threads[i] = new Thread() {
                @Override
                public void run() {
                    for (int i = 0; i < 10000; ++i) {
                        increment();
                    }
                }
            };
        }

        for (int i = 0; i < threads.length; ++i) {
            threads[i].start();
        }

        while (Thread.activeCount() > 1) {
            Thread.yield();
        }

        System.out.println("num = " + num);
    }
}

在这里插入图片描述
同步代码块

public class ThreadDemo {
    private static int num = 0;

    public static void main(String[] args) {
        Object o = new Object();

        Thread[] threads = new Thread[20];
        for (int i = 0; i < threads.length; ++i) {
            threads[i] = new Thread() {
                @Override
                public void run() {
                    for (int i = 0; i < 10000; ++i) {
                        synchronized (o) {
                            ++num;
                        }
                    }
                }
            };
        }

        for (int i = 0; i < threads.length; ++i) {
            threads[i].start();
        }

        while (Thread.activeCount() > 1) {
            Thread.yield();
        }

        System.out.println("num = " + num);
    }
}

在这里插入图片描述
注意:

  • 想要使多个线程保持同步,需要保证多个线程锁的是同一个对象
  • 使用同步代码块时,不建议去锁一个Integer或String对象,因为它们有时候不在常量池,而在堆中,就不是唯一的了,有可能多个线程锁的是不同的对象,就无法达到同步的效果。

同步方法、静态同步方法可以和同步代码块之间相互转换
同步方法和同步代码块

// 同步方法
public synchronized void method() {}

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

静态同步方法和同步代码块

// 静态同步方法
public static synchronized void method() {}

// 同步代码块
public static void method() {
	synchronized (ThreadDemo.class) {}
}

synchronized能够保证原子性、可见性和有序性
synchronized不能锁null,因为synchronized锁在对象头上。null是没有对象头的

线程间通信


我们主要来看三个方法wait()notify()notifyAll()

wait方法


wait()方法就是使线程停止运行

  • 方法wait()的作用是使当前执行的线程进行等待,wait()方法是Object类的方法,该方法是用来将当前线程置入“等待队列”并且在wait()所在的代码处停止执行,直到接到通知或被中断为止
  • wait()方法只能在同步方法中或同步块中调用。如果调用wait()时,没有持有适当的锁,会抛出异常
  • wait()方法执行后,当前线程释放锁,其他线程竞争获取锁

我们来看一段代码

package Thread;

public class WaitTest {
    public static void main(String[] args) throws InterruptedException {
        Object o = new Object();
        synchronized (o) {
            System.out.println("等待中...");
            o.wait();
            System.out.println("等待结束!");
        }
        System.out.println("main方法结束!");
    }
}

在这里插入图片描述
这段代码在执行到o.wait()的时候会一直等待下去。除非被中断或唤醒

notify方法


notify()方法就是使停止的线程继续运行

  • 方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象锁的其它线程,对其发出通知,使其可以重新竞争锁如果有多个线程等待,则有线程规划器随机挑选出一个处于等待队列的线程进行唤醒
  • notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程执行完同步代码块中的代码之后才会释放对象锁虽然此时已经有线程被唤醒,但是执行notify()方法的线程还持有锁,所以被唤醒的线程依旧会等待在锁上

我们来看一下代码

package Thread;

public class NotifyTest {
    public static void main(String[] args) {
        Thread t1 = new Thread() {
            @Override
            public void run() {
                synchronized (NotifyTest.class) {
                    try {
                        System.out.println("t1线程正在运行!");
                        System.out.println("t1线程正在等待!");
                        NotifyTest.class.wait();
                        System.out.println("t1线程被唤醒!");
                        System.out.println("t1线程即将退出!");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        t1.start();

        Thread t2 = new Thread() {
            @Override
            public void run() {
                synchronized (NotifyTest.class) {
                    System.out.println("t2线程正在运行!");
                    NotifyTest.class.notify();
                    System.out.println("t2线程即将退出!");
                }
            }
        };
        t2.start();
    }
}

在这里插入图片描述

notifyAll方法


上面的notify()方法只能唤醒某一个等待线程,那么如果有多个线程都在等待中怎么办呢,这个时候就可以使用notifyAll方法可以一次唤醒所有的等待线程,我们直接来看代码

package Thread;

import com.sun.xml.internal.bind.annotation.OverrideAnnotationOf;

public class NotifyAllTest {
    public static void main(String[] args) {
        Thread t1 = new Thread() {
            @Override
            public void run() {
                synchronized (NotifyAllTest.class) {
                    try {
                        System.out.println("t1线程正在运行!");
                        System.out.println("t1线程正在等待!");
                        NotifyAllTest.class.wait();
                        System.out.println("t1线程被唤醒!");
                        System.out.println("t1线程即将退出!");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        t1.start();

        Thread t2 = new Thread() {
            @Override
            public void run() {
                synchronized (NotifyAllTest.class) {
                    try {
                        System.out.println("t2线程正在运行!");
                        System.out.println("t2线程正在等待!");
                        NotifyAllTest.class.wait();
                        System.out.println("t2线程被唤醒!");
                        System.out.println("t2线程即将退出!");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        t2.start();

        Thread t3 = new Thread() {
            @Override
            public void run() {
                synchronized (NotifyAllTest.class) {
                    System.out.println("t3线程正在运行!");
                    NotifyAllTest.class.notifyAll();
                    System.out.println("t3线程即将退出!");
                }
            }
        };
        t3.start();
    }
}

在这里插入图片描述
线程间通信总结

  • wait()、notify()、notifyAll()三个方法的执行都必须在synchronized代码块中
  • 执行这三个方法必有持有相应的锁对象
  • wait、notify、notifyAll都是java.lang.Object类的方法,而不是Thread固有的方法。换句话说,wait、notify和notifyAll这三个方法与其说是针对线程的操作,倒不如说是针对实例的等待队列的操作。由于所有实例都有等待队列,所以wait、notify和notifyAll也就成为了Object类的方法。

wait和sleep的区别


  • wait之前需要请求锁,而wait执行时会先释放锁,等被唤醒时再重新请求锁。这个锁是wait对象上的monitor lock;
  • sleep是无视锁的存在的,即之前请求的锁不会释放,没有锁也不会请求
  • wait方法是Object的方法
  • sleep方法是Thread类的静态方法
  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值