java基础-chapter14(多线程)

线程

线程是操作系统能够进行运算调度的最小单位。他被包含在进程之中,是进程中的实际运作单位

进程

进程是程序的基本执行实体

多线程的作用:提高效率

并发和并行

并发:在同一时刻,有多个指令在单个CPU上交替执行

并行:在同一时刻,有多个指令在单个CPU上同时执行

多线程的实现方式

1.继承Thread类的方式进行实现

2.实现Runnable接口的方式进行实现

3.利用Callable接口和Future接口方式实现

多线程的启动方式

第一种启动方式

1.自己定义一个类继承Thread

2.重写run方法

public class MyThread extends Thread {
    @Override
    public void run() {
        //编写线程要执行的代码
        for (int i = 0; i < 100; i++) {
            System.out.println(getName()+":你好");
        }
    }
}

3.创建子类的对象,并启动线程

public class ThreadDemo1 {
    public static void main(String[] args) {
        /*
        多线程的第一种启动方式
       1.自己定义一个类继承Thread
       2.重写run方法
       3.创建子类的对象,并启动线程
         */
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        //给线程起名字
        t1.setName("t1线程");
        t2.setName("t2线程");
        //开启线程
        t1.start();
        t2.start();
    }
}

第二种启动方式

1.自定义一个类实现Runnable接口

2.重写里面的run方法

public class MyRun implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            //获取当前线程的对象
            Thread t = Thread.currentThread();
            System.out.println(t.getName()+"Hello");
        }
    }
}

3.创建自定义类的对象

4.创建一个Thread类的对象,并开启线程

public class ThreadDemo2 {
    public static void main(String[] args) {
        /*
        多线程的第二种启动方式
        1.自定义一个类实现Runnable接口
        2.重写里面的run方法
        3.创建自定义类的对象
        4.创建一个Thread类的对象,并开启线程
         */

        //创建MyRun的对象
        MyRun mr = new MyRun();

        //创建线程对象
        Thread t1 = new Thread(mr);
        Thread t2 = new Thread(mr);

        //设置线程名字
        t1.setName("线程1");
        t2.setName("线程2");

        //开启线程
        t1.start();
        t2.start();
    }
}

第三种启动方式

特点:可以获取到多线程的运行结果

1.创建一个类MyCallable实现Callable接口

2.重写call (有返回值,表示多线程运行的结果)

public class MyCallable implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        //求1-100的和
        int sum = 0;
        for (int i = 1; i <= 100; i++) {
            sum += i;
        }
        return sum;
    }
}

3.创建MyCallable的对象(表示多线程要执行的任务)

4.创建FutureTask的对象(作用:管理多线程运行的结果)

5.创建Thread类的对象,并启动(表示线程)

public class ThreadDemo3 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        /*
        1.创建一个类MyCallable实现Callable接口
        2.重写call (有返回值,表示多线程运行的结果)
        3.创建MyCallable的对象(表示多线程要执行的任务)
        4.创建FutureTask的对象(作用:管理多线程运行的结果)
        5.创建Thread类的对象,并启动(表示线程)
         */

        //创建MyCallable的对象(表示多线程要执行的任务)
        MyCallable mc = new MyCallable();
        //创建FutureTask的对象(作用:管理多线程运行的结果)
        FutureTask<Integer> fk = new FutureTask<>(mc);
        //创建Thread类的对象,并启动(表示线程)
        Thread t1 = new Thread(fk);
        t1.start();
        //获取线程运行的结果
        Integer number = fk.get();
        System.out.println(number);
    }
}

常见的成员方法

1. String getName() 返回此线程的名称

2. void setName(String name) 设置线程的名字

3. static Thread currentThread()  获取当前线程的对象

4. static void sleep(long time) 让线程休眠指定的时间,单位为毫秒

5. setPriority(int newPriority) 设置线程优先级

6. final int getPriority() 获取线程的优先级

7. final void setDaemon(boolean on) 设置为守护线程

8. public static void yield() 出让线程/礼让线程

9. public static void join() 插入线程/插队线程

public class ThreadExample extends Thread {

    public static void main(String[] args) {
        // 创建线程对象
        ThreadExample thread = new ThreadExample();

        // 1. 获取线程的名称
        String threadName = thread.getName();
        System.out.println("Thread name: " + threadName);

        // 2. 设置线程的名字
        thread.setName("MyThread");

        // 3. 获取当前线程的对象
        Thread currentThread = Thread.currentThread();
        System.out.println("Current thread: " + currentThread.getName());

        // 4. 让线程休眠指定的时间
        try {
            Thread.sleep(1000); // 休眠1秒
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 5. 设置线程优先级
        thread.setPriority(Thread.MAX_PRIORITY); // 设置为最高优先级

        // 6. 获取线程的优先级
        int priority = thread.getPriority();
        System.out.println("Thread priority: " + priority);

        // 7. 设置为守护线程
        thread.setDaemon(true);

        // 8. 出让线程/礼让线程
        Thread.yield();

        // 9. 插入线程/插队线程
        try {
            Thread otherThread = new Thread();
            otherThread.start();
            otherThread.join(); // 当前线程会插队执行otherThread,直到otherThread执行完成
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void run() {
        // 线程运行时的逻辑
    }
}

线程的生命周期

Java线程的生命周期主要包括以下几个状态:

  1. 新建状态(New):使用new关键字创建一个线程后,该线程就处于新建状态。此时,它和其他Java对象一样,仅仅由JVM为其分配了内存,并初始化了其成员变量的值。此时的线程对象没有表现出任何线程的动态特征,程序也不会自动执行线程体(run()方法)。
  2. 就绪状态(Runnable):当线程对象调用了start()方法后,该线程就进入了就绪状态。就绪状态的线程处于线程队列中,等待CPU调度执行。
  3. 运行状态(Running):当处于就绪状态的线程被CPU调度执行时,该线程就进入了运行状态。运行状态表示线程正在执行run()方法中的代码。
  4. 阻塞状态(Blocked):阻塞状态表示线程因为某种原因暂时不能继续执行,例如等待I/O操作完成,或者等待获取某个锁。当线程处于阻塞状态时,它不执行任何操作。
  5. 死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程的生命周期就结束了,进入死亡状态。死亡状态是线程生命周期中的最后一个阶段,进入这个阶段意味着线程的彻底终止。

需要注意的是,虽然线程的生命周期中有多个状态,但是在Java虚拟机中,线程的状态只有新建、运行、阻塞和死亡四种。就绪状态和运行状态在虚拟机中并没有明确的区分,它们都可以看作是运行状态。

另外,Java线程的状态转换并不是任意的,只能从一种状态转换到另一种状态,比如线程不能从阻塞状态直接转换到运行状态,必须先转到就绪状态。线程状态的转换需要遵循一定的规则。

总的来说,Java线程的生命周期是由线程的创建、就绪、运行、阻塞和死亡五个阶段组成的。这些状态反映了线程从创建到结束的动态过程。

9e9910c7af7949ed8486810cdc21697b.png

 

同步代码块

在Java中,同步代码块是一种用于控制多线程对共享资源的访问的机制,以防止数据不一致和其他并发问题。通过同步代码块,可以确保任何时候只有一个线程能够执行特定区域的代码,从而避免多个线程同时访问和修改共享数据。

同步代码块使用synchronized关键字来定义,并需要指定一个对象作为锁。这个锁对象用于同步代码块的访问。当一个线程进入同步代码块并获得锁时,其他尝试进入该同步代码块的线程将被阻塞,直到第一个线程退出同步代码块并释放锁。

下面是一个同步代码块的示例:

public class Buy extends Thread {
    static int tickets = 0; //将属性设置为静态 三个线程共享一个属性
    // Buy.class 表示本类的字节码文件 在同一个文件夹中只能有一个 Buy.class 所以他一定是唯一的
    // 锁对象 一定要是唯一的
    @Override
    public void run() {
            while(true){ //同步代码块不能写在循环外
                synchronized (Buy.class){ // () 填写锁对象
                if (tickets < 100) {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    tickets++;
                    System.out.println(getName()+"正在卖第" + tickets + "张票");
                }else {
                    System.out.println("已售罄");
                    break;
                }
            }
        }
    }
}

测试类:

public class BuyTest {
    public static void main(String[] args) {
        Buy b1 = new Buy();
        Buy b2 = new Buy();
        Buy b3 = new Buy();

        b1.setName("窗口1");
        b2.setName("窗口2");
        b3.setName("窗口3");

        b1.start();
        b2.start();
        b3.start();
    }
}

同步方法

格式: 修饰符 synchronized 返回值类型 方法名(方法参数){ .... }

特点1:同步方法是锁住方法里面所有的代码

特点2:锁对象不能自己指定  

非静态:this

静态:当前类的字节码文件对象

快捷键

选中代码块 ctrl + alt + m  会快捷生成一个方法

c93b007368724a4db35a979aeaebf184.png

示例代码:

public class Buy2 implements Runnable {
    int tickets = 0;
    @Override
    public void run() {
        /*
        1.循环
        2.同步代码块(同步方法)
        3.判断共享数据是否到了末尾 如果到了末尾
        4.判断共享数据是否到了末尾 如果没到末尾
         */
        while (true){
            if (extracted()) break;
        }
    }

    private synchronized  boolean extracted() {
        synchronized (Buy2.class){
            if (tickets == 100){
                return true;
            }else {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                tickets ++;
                System.out.println(Thread.currentThread().getName() + "正在卖第" +tickets+"张票");
            }
        }
        return false;
    }
}

测试代码:

public class Buy2Test {
    public static void main(String[] args) {
        Buy2 b2 = new Buy2();
        Thread t1 = new Thread(b2);
        Thread t2 = new Thread(b2);
        Thread t3 = new Thread(b2);

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

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

lock和unlock

 

lock(): 获取锁。如果锁被另一个线程持有,则当前线程将被禁用,直至发生以下两种情况之一:锁由当前线程获取;或者线程被中断。

unlock(): 释放锁。通常,在 finally 块中调用此方法,以确保锁一定会被释放,防止发生死锁。

 static Lock lock = new ReentrantLock(); //将锁对象设置为静态 让所有线程对象共享同一个锁

    public void run() {
            while (true){
                lock.lock(); //获取锁
                try {
                    if (tickets == 100){
                        break;
                    }else {
                        Thread.sleep(100);
                        tickets++;
                        System.out.println(Thread.currentThread().getName()+"正在卖第"+tickets+"张票");
                    }
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                } finally {
                    lock.unlock();
                }
            }
        }

注意事项

一定要确保在 finally 块中释放锁,以防止因异常而导致的锁未释放。

使用 Lock 比使用 synchronized 提供了更多的灵活性,例如可以尝试获取锁(tryLock()),可以被中断的获取锁(lockInterruptibly())等。

ReentrantLock 是 Lock 接口的一个常用实现,它允许同一个线程多次获取同一个锁,而不会造成自己阻塞自己。

 

死锁

在Java中,死锁(Deadlock)是指两个或更多的线程无限期地等待一个资源,该资源正在被另一个线程持有,而后者也在等待另一个被第一个线程持有的资源。这种情况会导致一个僵局,即所有的线程都在等待,无法继续执行,因为没有一个线程能够获得它所需要的全部资源。

死锁通常发生在以下四个条件同时满足时,这四个条件也被称为死锁的四个必要条件(Coffman条件):

  1. 互斥条件:至少有一个资源必须处于非共享模式,即一次只有一个线程能使用。如果其他线程请求该资源,请求者只能等待,直到资源被释放。

  2. 持有和等待条件:一个线程至少持有一个资源,并且正在等待获取其他线程持有的额外资源。

  3. 非抢占条件:资源不能被强制从一个线程中夺走,线程必须主动释放资源。

  4. 循环等待条件:存在一个等待循环,其中每个线程都在等待下一个线程所持有的资源。

当这四个条件都成立时,就可能发生死锁。

 

生产者和消费者

常用方法

void wait() 当前线程等待,直到被其他线程唤醒

void notify() 随机唤醒单个线程

void notifyAll() 唤醒所有线程

生产者(厨师)

public class Cook extends Thread{
    @Override
    public void run() {
        while(true){
            synchronized (Desk.lock){
                if (Desk.count == 0){
                    break;
                }else {
                    //判断桌子上是否有食物
                    //如果有则等待
                    if (Desk.foodFlag == 1) {
                        try {
                            Desk.lock.wait();
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    }else{
                        //没有就制作食物
                        System.out.println("厨师做了一碗面条");
                        //修改桌子上的食物状态
                        Desk.foodFlag = 1;
                        //叫醒等待的消费者吃
                        Desk.lock.notifyAll();
                    }
                }
            }
        }
    }
}

消费者(吃货)

public class Foodie extends Thread{
    @Override
    public void run() {
        /*
        1.循环
        2.同步代码块
        3.判断代码是否到了末尾,到了末尾
        4.判断代码是否到了末尾,没到末尾
         */
        //1.循环
        while(true) {
            //2.同步代码块
            synchronized (Desk.lock) {
                //3.判断代码是否到了末尾,到了末尾
                if (Desk.count == 0) {
                    break;
                }
                //4.判断代码是否到了末尾,没到末尾
                else {
                    //先判断桌子上是否有面条
                    if (Desk.foodFlag == 0) {
                        try {
                            //如果没有就等待
                            Desk.lock.wait(); //让当前线程跟锁进行绑定
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    } else{

                        //如果有就开吃
                        System.out.println("吃货正在吃第"+Desk.count+"碗面条");
                        //吃完之后唤醒厨师继续做
                        Desk.lock.notifyAll();
                        //把吃的总数 -1
                        Desk.count--;
                        //修改桌子的状态
                        Desk.foodFlag = 0;
                        }
                    }
                }
            }
        }
    }

桌子

public class Desk {
    //控制消费者和生产者的执行

    //是否有面条 有面条:1   无面条:0
    public static int foodFlag = 0;

    //总个数
    public static int count = 10;

    //锁对象
    public static Object lock = new Object();

}

测试类

public class Test {
    public static void main(String[] args) {
        //创建线程对象
        Cook c = new Cook();
        Foodie f = new Foodie();
        //给线程设置名字
        c.setName("吃货");
        f.setName("厨师");
        //开启线程
        c.start();
        f.start();
    }
}

 

等待唤醒机制(阻塞队列实现)

阻塞队列(BlockingQueue)是一种特殊的队列,当队列为空时,从队列中获取元素的操作会被阻塞,直到队列中有新的元素添加进来;当队列已满时,尝试向队列中添加新元素的操作也会被阻塞,直到队列有空余空间。这种特性使得阻塞队列非常适合用于实现等待唤醒机制。

在Java中,java.util.concurrent包已经提供了BlockingQueue接口以及几种实现,如ArrayBlockingQueueLinkedBlockingQueuePriorityBlockingQueue等。下面是一个使用BlockingQueue来实现等待唤醒机制的简单例子:

import java.util.concurrent.BlockingQueue;  
import java.util.concurrent.LinkedBlockingQueue;  
  
public class ProducerConsumerExample {  
  
    public static void main(String[] args) {  
        BlockingQueue<Integer> queue = new LinkedBlockingQueue<>(10); // 容量为10的阻塞队列  
  
        // 创建生产者线程  
        Thread producer = new Thread(() -> {  
            try {  
                for (int i = 0; i < 20; i++) {  
                    System.out.println("生产者生产了 " + i);  
                    queue.put(i); // 当队列满时会阻塞  
                    Thread.sleep((long) (Math.random() * 1000)); // 模拟生产时间  
                }  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
        });  
  
        // 创建消费者线程  
        Thread consumer = new Thread(() -> {  
            try {  
                while (true) {  
                    Integer item = queue.take(); // 当队列空时会阻塞  
                    System.out.println("消费者消费了 " + item);  
                    Thread.sleep((long) (Math.random() * 1000)); // 模拟消费时间  
                    if (item == 19) {  
                        break; // 当消费到最后一个元素时停止消费者线程  
                    }  
                }  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
        });  
  
        // 启动生产者和消费者线程  
        producer.start();  
        consumer.start();  
    }  
}

在这个例子中,创建了一个容量为10的LinkedBlockingQueue。生产者线程会向队列中放入整数,当队列满时,put方法会阻塞直到队列中有空间。消费者线程会从队列中取出整数,当队列为空时,take方法会阻塞直到队列中有元素可取。这样,生产者和消费者之间就通过阻塞队列实现了等待唤醒机制。生产者生产了元素后会唤醒等待的消费者,而消费者消费了元素后会为生产者腾出空间,从而唤醒等待的生产者。

 

线程的状态

e21ed456cc1b464ca856a17dfc2a8c26.png

 

线程池

线程池(Thread Pool)是一种并发编程中常用的技术,用于管理和重用线程。它由线程池管理器、工作队列和线程池线程组成。在应用程序启动时,会创建一定数量的线程,并将它们保存在线程池中。当需要执行任务时,线程池管理器会从线程池中获取一个空闲的线程,并将任务分配给该线程执行。任务执行完毕后,线程会返回到线程池中,等待下一个任务的分配。

线程池的主要优点包括:

  1. 提高性能:通过重用已创建的线程,避免了频繁地创建和销毁线程所带来的开销,从而提高了系统的性能。
  2. 控制并发数:线程池能够控制同时运行的线程数量,防止过多的线程同时执行导致系统资源耗尽。
  3. 提高资源利用率:当线程处于空闲状态时,可以被其他任务复用,从而提高了资源的利用率。

线程池的实现方式有多种,例如在Java中,Executors类提供了几种不同的线程池创建配置,包括固定大小的线程池(newFixedThreadPool)、可缓存的线程池(newCachedThreadPool)、单线程的线程池(newSingleThreadExecutor)等。这些线程池具有不同的特点和使用场景,可以根据实际需求进行选择。

线程池的使用场景广泛,特别适用于请求量大且任务执行时间短的业务,如Web服务器、数据库连接池等。通过合理地设置线程池的大小和任务队列的长度,可以有效地提高系统的吞吐量和响应时间。

总的来说,线程池是一种高效且灵活的并发编程技术,能够帮助开发者更好地管理和利用线程资源,提高系统的性能和稳定性。

线程池的核心原理

1.创建一个池子,池子中是空的

2.提交任务时,池子会创建新的线程对象,任务执行完毕,线程归还给池子,

下次再提交任务时,不需要创建新的线程,直接复用已用的线程即可

3.但是如果提交任务时,池子中没有空闲线程,也无法创建新的线程,任务就会排队等待

import java.util.concurrent.BlockingQueue; // 导入BlockingQueue接口,用于线程安全的队列操作  
import java.util.concurrent.LinkedBlockingQueue; // 导入LinkedBlockingQueue类,它是BlockingQueue接口的一个实现  
  
/**  
 * 简单的线程池实现类  
 */  
public class SimpleThreadPool {  
    private final BlockingQueue<Runnable> taskQueue; // 任务队列,用于存放待执行的任务  
    private final ThreadPoolWorker[] workers; // 工作线程数组  
    private final int numWorkers; // 工作线程的数量  
  
    /**  
     * 构造函数,初始化线程池  
     *  
     * @param numWorkers 线程池中工作线程的数量  
     */  
    public SimpleThreadPool(int numWorkers) {  
        this.numWorkers = numWorkers;  
        this.taskQueue = new LinkedBlockingQueue<>(); // 创建一个新的任务队列  
        this.workers = new ThreadPoolWorker[numWorkers]; // 初始化工作线程数组  
  
        // 创建并启动指定数量的工作线程  
        for (int i = 0; i < numWorkers; i++) {  
            workers[i] = new ThreadPoolWorker(taskQueue);  
            workers[i].start();  
        }  
    }  
  
    /**  
     * 提交一个新任务到任务队列中  
     *  
     * @param task 要执行的任务  
     */  
    public void execute(Runnable task) {  
        try {  
            taskQueue.put(task); // 将任务放入队列,如果队列已满则阻塞等待  
        } catch (InterruptedException e) {  
            e.printStackTrace(); // 打印异常堆栈信息  
        }  
    }  
  
    /**  
     * 关闭线程池,停止所有工作线程  
     */  
    public void shutdown() {  
        for (ThreadPoolWorker worker : workers) {  
            worker.shutdown(); // 调用每个工作线程的shutdown方法,停止线程  
        }  
    }  
  
    /**  
     * 内部类,代表线程池中的一个工作线程  
     */  
    class ThreadPoolWorker extends Thread {  
        private final BlockingQueue<Runnable> taskQueue; // 该工作线程关联的任务队列  
        private volatile boolean running = true; // 线程运行状态标志  
  
        /**  
         * 构造函数,初始化工作线程  
         *  
         * @param taskQueue 该线程要处理的任务队列  
         */  
        ThreadPoolWorker(BlockingQueue<Runnable> taskQueue) {  
            this.taskQueue = taskQueue;  
        }  
  
        /**  
         * 线程的run方法,线程启动后会自动执行此方法  
         */  
        @Override  
        public void run() {  
            while (running) {  
                try {  
                    Runnable task = taskQueue.take(); // 从队列中取出任务,如果队列为空则阻塞等待  
                    task.run(); // 执行任务  
                } catch (InterruptedException e) {  
                    // 处理中断异常,通常是因为线程在等待时被中断  
                    e.printStackTrace(); // 打印异常堆栈信息  
                }  
            }  
        }  
  
        /**  
         * 停止工作线程的运行  
         */  
        public void shutdown() {  
            running = false; // 设置运行状态为false,使线程退出运行循环  
            interrupt(); // 中断线程,使其从阻塞状态中恢复  
        }  
    }  
  
    /**  
     * 主方法,用于测试线程池功能  
     *  
     * @param args 命令行参数(此处未使用)  
     */  
    public static void main(String[] args) {  
        SimpleThreadPool pool = new SimpleThreadPool(5); // 创建一个包含5个工作线程的线程池  
  
        // 提交10个任务到线程池执行  
        for (int i = 0; i < 10; i++) {  
            final int index = i;  
            pool.execute(() -> { // 使用Lambda表达式定义任务内容  
                System.out.println("Task " + index + " is running."); // 打印任务开始执行的信息  
                try {  
                    Thread.sleep(1000); // 模拟任务执行耗时,暂停1秒钟  
                } catch (InterruptedException e) {  
                    e.printStackTrace(); // 打印异常堆栈信息  
                }  
            });  
        }  
  
        // 让任务运行一段时间后关闭线程池  
        try {  
            Thread.sleep(5000); // 主线程暂停5秒钟,等待任务执行一段时间  
        } catch (InterruptedException e) {  
            e.printStackTrace(); // 打印异常堆栈信息  
        }  
  
        pool.shutdown(); // 关闭线程池,停止所有工作线程的运行  
    }  
}

这个简单的线程池实现包含以下几个关键部分:

  1. 任务队列 (BlockingQueue<Runnable> taskQueue): 用于存储待执行的任务。这里使用的是LinkedBlockingQueue,它是一个线程安全的队列。

  2. 工作线程 (ThreadPoolWorker): 这是一个继承自Thread的类,每个工作线程会不断地从任务队列中取出任务并执行。当调用shutdown()方法时,工作线程会停止执行。

  3. 线程池主体 (SimpleThreadPool): 负责管理任务队列和工作线程。它提供了execute(Runnable task)方法来提交新任务,并提供了shutdown()方法来关闭线程池。

请注意,这个实现是为了教学目的而简化的。在生产环境中,您应该使用Java标准库提供的java.util.concurrent.ExecutorServicejava.util.concurrent.Executors来创建和管理线程池,因为它们提供了更完整、更健壮的功能集,并且经过了充分的测试和优化。

 

 

  • 31
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

CtrlCV 攻城狮

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

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

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

打赏作者

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

抵扣说明:

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

余额充值