java实战(五):理解多线程与多线程实现冒泡排序及可视化

1.多线程理解

1.1线程概念

线程:计算机中能够执行独立任务的最小单位。在操作系统中,每个程序都运行在一个或多个线程中。线程可以同时执行多个任务,使得程序能够并发执行,提高了程序的效率和响应能力。

与进程不同,线程是在进程内部创建和管理的。一个进程可以包含多个线程,这些线程共享进程的资源,如内存空间和文件句柄等。每个线程有自己的执行路径和状态,可以独立执行不同的任务。

线程的创建和调度由操作系统负责,它会为每个线程分配资源,并按照一定的调度策略来决定线程的执行顺序。线程之间可以通过共享内存或消息传递等方式进行通信和同步。

多线程编程可以提高程序的性能和响应能力,特别适用于需要同时处理多个任务或需要实时交互的应用程序。然而,多线程编程也带来了一些挑战,如线程安全性、竞态条件和死锁等问题,需要仔细考虑和处理。

1.2线程的创建和启动

线程的创建和启动可以通过继承Thread类或实现Runnable接口来实现。

  1. 继承Thread类:
    • 创建一个继承自Thread类的自定义线程类,重写run()方法,在run()方法中定义线程的执行逻辑。
    • 在自定义线程类中,可以添加其他成员变量和方法,用于线程的控制和数据传递。
    • 在主程序中,创建自定义线程类的实例,并调用start()方法启动线程。

示例代码如下:

public class MyThread extends Thread {
    @Override
    public void run() {
        // 线程执行的代码逻辑
        // ...
    }
}

public class Main {
    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start(); // 启动线程
    }
}
  1. 实现Runnable接口:
    • 创建一个实现了Runnable接口的类,实现run()方法,在run()方法中定义线程的执行逻辑。
    • 在主程序中,创建Runnable接口实现类的实例,并将其作为参数传递给Thread类的构造方法。
    • 调用Thread类的start()方法启动线程。

示例代码如下:

public class MyRunnable implements Runnable {
    @Override
    public void run() {
        // 线程执行的代码逻辑
        // ...
    }
}

public class Main {
    public static void main(String[] args) {
        MyRunnable runnable = new MyRunnable();
        Thread thread = new Thread(runnable);
        thread.start(); // 启动线程
    }
}

无论是继承Thread类还是实现Runnable接口,都需要重写run()方法,在run()方法中定义线程的执行逻辑。线程的实际执行逻辑应该写在run()方法中。

通过调用start()方法来启动线程,start()方法会在后台创建一个新的线程,并调用run()方法来执行线程的逻辑。

需要注意的是,不要直接调用run()方法来启动线程,这样只会在当前线程中执行run()方法,而不会创建新的线程。

1.3线程的同步与互斥

线程的同步和互斥是为了保证多个线程之间的正确协作和共享资源的安全访问。

  1. 同步:线程同步是指多个线程按照一定的顺序执行,以达到协作的目的。常用的同步机制有:

    • 使用synchronized关键字:通过在方法或代码块前加上synchronized关键字,可以确保同一时间只有一个线程可以执行被synchronized修饰的代码段。
    • 使用Lock接口ReentrantLock类Lock接口提供了更灵活的锁定机制,可以使用lock()方法获取锁,使用unlock()方法释放锁。
  2. 互斥:线程互斥是指多个线程之间对共享资源的访问进行控制,保证同一时间只有一个线程可以访问共享资源,避免数据的不一致性和冲突。常用的互斥机制有:

    • 使用synchronized关键字:通过在方法或代码块前加上synchronized关键字,可以确保同一时间只有一个线程可以执行被synchronized修饰的代码段。
    • 使用Lock接口ReentrantLock类Lock接口提供了更灵活的锁定机制,可以使用lock()方法获取锁,使用unlock()方法释放锁。
    • 使用信号量(Semaphore):信号量可以控制同时访问某个资源的线程数量,通过acquire()方法获取信号量,release()方法释放信号量。

同步和互斥机制可以保证线程之间的协作和共享资源的安全访问,避免了数据竞争和不一致性的问题。

需要注意的是,在使用同步和互斥机制时,要避免死锁和活锁等问题,合理设计和使用锁定机制。

当多个线程同时访问共享资源时,可以使用同步和互斥机制来确保数据的一致性和避免冲突。以下是两个简单的例子:

  1. 使用synchronized关键字:
public class Counter {
    private int count;

    public synchronized void increment() {
        count++;
    }
}

public class Main {
    public static void main(String[] args) {
        Counter counter = new Counter();

        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });

        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });

        thread1.start();
        thread2.start();

        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(counter.getCount()); // 输出结果应为2000
    }
}

在上述例子中,Counter类中的increment()方法使用了synchronized关键字,确保了对count变量的访问是互斥的。两个线程分别执行increment()方法,通过对count进行加一操作,最终得到的结果应为2000。

  1. 使用Lock接口ReentrantLock类
public class Counter {
    private int count;
    private Lock lock = new ReentrantLock();

    public void increment() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock();
        }
    }
}

public class Main {
    public static void main(String[] args) {
        Counter counter = new Counter();

        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });

        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });

        thread1.start();
        thread2.start();

        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(counter.getCount()); // 输出结果应为2000
    }
}

在上述例子中,Counter类中的increment()方法使用了Lock接口ReentrantLock类,通过lock()方法获取锁,使用unlock()方法释放锁。两个线程分别执行increment()方法,通过对count进行加一操作,最终得到的结果应为2000。

这些例子展示了如何使用同步和互斥机制来确保多个线程对共享资源的安全访问。通过使用synchronized关键字、Lock接口ReentrantLock类等机制,可以避免数据竞争和不一致性的问题。

1.4线程的状态和生命周期

在这里插入图片描述

  1. 新建状态(New):线程对象被创建,但还没有调用start()方法。

  2. 就绪状态(Runnable):调用线程对象的start()方法后,线程进入就绪状态,等待CPU分配时间片。

  3. 运行状态(Running):当线程获得CPU时间片后,进入运行状态,执行run()方法中的代码。

  4. 阻塞状态(Blocked):线程在某些情况下会进入阻塞状态,暂时停止执行,直到满足某个条件后才能继续执行。

  5. 等待状态(Waiting):线程在某些情况下会进入等待状态,等待其他线程的唤醒。

  6. 计时等待状态(Timed Waiting):线程在某些情况下会进入计时等待状态,等待一段时间或满足某个条件后继续执行。

  7. 终止状态(Terminated):线程执行完run()方法或发生异常导致线程终止后,进入终止状态。

需要注意的是,线程的状态不是固定的,线程可以在不同的状态之间转换。例如,一个线程在运行状态下可能被阻塞或进入等待状态,然后再次回到运行状态。

1.5线程间的通信

线程间的通信是指多个线程之间通过共享的内存或其他方式进行信息交换和数据传递的过程。在Java中,线程间的通信可以通过以下几种方式实现:

  1. 共享变量:多个线程可以通过共享的变量进行通信。通过对共享变量的读写操作,线程可以传递信息和数据。需要注意的是,对于共享变量的读写操作需要进行同步,以确保线程安全。

  2. 等待/通知机制:通过使用Object类wait()notify()notifyAll()方法,线程可以进行等待和唤醒操作。一个线程可以调用wait()方法进入等待状态,等待其他线程调用notify()notifyAll()方法来唤醒它。

下面是一个简单的例子,演示了线程间通过共享变量和等待/通知机制进行通信:

public class Message {
    private String content;
    private boolean isAvailable = false;

    public synchronized void send(String message) {
        while (isAvailable) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        content = message;
        isAvailable = true;
        notifyAll();
    }

    public synchronized String receive() {
        while (!isAvailable) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        String message = content;
        isAvailable = false;
        notifyAll();
        return message;
    }
}

public class Sender implements Runnable {
    private Message message;

    public Sender(Message message) {
        this.message = message;
    }

    @Override
    public void run() {
        String[] messages = {"Hello", "World", "Goodbye"};
        for (String msg : messages) {
            message.send(msg);
            System.out.println("Sent: " + msg);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class Receiver implements Runnable {
    private Message message;

    public Receiver(Message message) {
        this.message = message;
    }

    @Override
    public void run() {
        for (int i = 0; i < 3; i++) {
            String receivedMsg = message.receive();
            System.out.println("Received: " + receivedMsg);
        }
    }
}

public class Main {
    public static void main(String[] args) {
        Message message = new Message();
        Thread senderThread = new Thread(new Sender(message));
        Thread receiverThread = new Thread(new Receiver(message));

        senderThread.start();
        receiverThread.start();
    }
}

在上述例子中,Message类表示一个消息对象,包含一个共享的字符串变量content和一个标志位isAvailableSender线程通过调用send()方法向Message对象发送消息,Receiver线程通过调用receive()方法接收消息。通过使用synchronized关键字和wait()notify()方法,实现了线程间的等待和唤醒操作,确保了消息的正确传递。

1.6处理线程的异常和错误

在处理线程的异常和错误时,我们可以采取以下几种方式:

  1. 使用try-catch块捕获异常:在线程的run()方法中,可以使用try-catch块来捕获可能发生的异常,并在catch块中进行相应的处理。这样可以确保异常不会导致线程终止,而是继续执行后续的代码。
public class MyThread implements Runnable {
    @Override
    public void run() {
        try {
            // 执行可能抛出异常的代码
        } catch (Exception e) {
            // 处理异常
        }
    }
}
  1. 在线程内部抛出异常:如果在线程的run()方法中抛出了异常,可以通过在run()方法中直接抛出异常,然后在线程的调用方(例如主线程)中捕获并处理异常。
public class MyThread implements Runnable {
    @Override
    public void run() {
        // 执行可能抛出异常的代码
        throw new RuntimeException("Something went wrong");
    }
}

public class Main {
    public static void main(String[] args) {
        try {
            Thread myThread = new Thread(new MyThread());
            myThread.start();
            myThread.join();
        } catch (Exception e) {
            // 处理异常
        }
    }
}
  1. 使用UncaughtExceptionHandler处理未捕获的异常:如果在线程中的异常没有被捕获,可以通过设置线程的UncaughtExceptionHandler来处理未捕获的异常。UncaughtExceptionHandler是一个接口,可以自定义实现来处理异常。
public class MyThread implements Runnable {
    @Override
    public void run() {
        // 执行可能抛出异常的代码
        throw new RuntimeException("Something went wrong");
    }
}

public class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {
    @Override
    public void uncaughtException(Thread t, Throwable e) {
        // 处理未捕获的异常
    }
}

public class Main {
    public static void main(String[] args) {
        Thread myThread = new Thread(new MyThread());
        myThread.setUncaughtExceptionHandler(new MyUncaughtExceptionHandler());
        myThread.start();
    }
}

通过设置UncaughtExceptionHandler,可以在发生未捕获的异常时进行处理,例如记录日志、发送通知等。

1.7实践

  1. 生产者-消费者模型:
    这是一个经典的多线程问题,其中一个线程(生产者)生成数据,另一个线程(消费者)消费数据。这个模型可以用于解决生产者和消费者之间的数据交互问题。

关键部分:

  • 创建一个共享的缓冲区,用于生产者和消费者之间的数据交换。
  • 使用synchronized关键字来确保在访问共享缓冲区时的线程安全。
  • 生产者线程在缓冲区未满时生成数据,并通知消费者线程。
  • 消费者线程在缓冲区非空时消费数据,并通知生产者线程。
  1. 并行计算:
    多线程可以用于并行计算,将一个大任务分解成多个小任务,然后并行执行这些小任务,最后将结果合并。

关键部分:

  • 创建一个线程池,用于管理并发执行的任务。
  • 将大任务分解成多个小任务,每个小任务实现Runnable接口。
  • 将小任务提交给线程池进行并行执行。
  • 等待所有任务执行完成后关闭线程池。
  1. 多线程网络编程:
    多线程可以用于处理并发的网络请求,每个请求都可以在独立的线程中进行处理,提高系统的并发处理能力。

关键部分:

  • 创建一个服务器套接字,监听指定的端口。
  • 当有客户端连接时,为每个客户端创建一个独立的线程来处理请求。
  • 在线程中处理客户端的输入和输出流,实现具体的业务逻辑。
  1. 多线程图像处理:
    多线程可以用于图像处理,例如对一张大图进行分块处理,每个线程处理一个块,最后将处理后的块合并成最终的图像。

关键部分:

  • 读取图像文件并创建BufferedImage对象。
  • 将图像分成多个块,每个块由一个线程处理。
  • 在每个线程中,对指定的图像块进行处理,例如应用滤镜、调整亮度等操作。
  • 最后将处理后的图像块合并成最终的图像。
    当然,下面是四个经典的实际应用程序,涉及到Java多线程的实践和练习。我将提供简要的代码示例,以帮助您更好地理解。
  1. 生产者-消费者模型:
    这是一个经典的多线程问题,其中一个线程(生产者)生成数据,另一个线程(消费者)消费数据。这个模型可以用于解决生产者和消费者之间的数据交互问题。
import java.util.LinkedList;

class ProducerConsumer {
    private LinkedList<Integer> buffer = new LinkedList<>();
    private int capacity = 5;

    public void produce() throws InterruptedException {
        int value = 0;
        while (true) {
            synchronized (this) {
                while (buffer.size() == capacity) {
                    wait();
                }
                System.out.println("Producer produced: " + value);
                buffer.add(value++);
                notify();
                Thread.sleep(1000);
            }
        }
    }

    public void consume() throws InterruptedException {
        while (true) {
            synchronized (this) {
                while (buffer.isEmpty()) {
                    wait();
                }
                int value = buffer.removeFirst();
                System.out.println("Consumer consumed: " + value);
                notify();
                Thread.sleep(1000);
            }
        }
    }
}

public class Main {
    public static void main(String[] args) {
        ProducerConsumer pc = new ProducerConsumer();

        Thread producerThread = new Thread(() -> {
            try {
                pc.produce();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        Thread consumerThread = new Thread(() -> {
            try {
                pc.consume();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        producerThread.start();
        consumerThread.start();
    }
}
  1. 并行计算:
    多线程可以用于并行计算,将一个大任务分解成多个小任务,然后并行执行这些小任务,最后将结果合并。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

class Task implements Runnable {
    private int taskId;

    public Task(int taskId) {
        this.taskId = taskId;
    }

    @Override
    public void run() {
        System.out.println("Task " + taskId + " is running.");
        // 执行任务的逻辑
    }
}

public class Main {
    public static void main(String[] args) {
        int numTasks = 10;
        ExecutorService executor = Executors.newFixedThreadPool(numTasks);

        for (int i = 0; i < numTasks; i++) {
            executor.submit(new Task(i));
        }

        executor.shutdown();
        try {
            executor.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
  1. 多线程网络编程:
    多线程可以用于处理并发的网络请求,每个请求都可以在独立的线程中进行处理,提高系统的并发处理能力。
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;

class ClientHandler implements Runnable {
    private Socket clientSocket;

    public ClientHandler(Socket clientSocket) {
        this.clientSocket = clientSocket;
    }

    @Override
    public void run() {
        try {
            InputStream input = clientSocket.getInputStream();
            OutputStream output = clientSocket.getOutputStream();

            // 处理客户端请求的逻辑
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

public class Main {
    public static void main(String[] args) {
        int port = 8080;
        try {
            ServerSocket serverSocket = new ServerSocket(port);
            while (true) {
                Socket clientSocket = serverSocket.accept();
                Thread clientThread = new Thread(new ClientHandler(clientSocket));
                clientThread.start();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
  1. 多线程图像处理:
    多线程可以用于图像处理,例如对一张大图进行分块处理,每个线程处理一个块,最后将处理后的块合并成最终的图像。
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;

class ImageProcessor implements Runnable {
    private BufferedImage image;
    private int startX;
    private int startY;
    private int width;
    private int height;

    public ImageProcessor(BufferedImage image, int startX, int startY, int width, int height) {
        this.image = image;
        this.startX = startX;
        this.startY = startY;
        this.width = width;
        this.height = height;
    }

    @Override
    public void run() {
        // 图像处理逻辑,例如对指定区域进行滤镜处理等
    }
}

public class Main {
    public static void main(String[] args) {
        String imagePath = "path/to/image.jpg";
        try {
            BufferedImage image = ImageIO.read(new File(imagePath));
            int numThreads = 4;
            int imageWidth = image.getWidth();
            int imageHeight = image.getHeight();
            int blockWidth = imageWidth / numThreads;
            int blockHeight = imageHeight / numThreads;

            for (int i = 0; i < numThreads; i++) {
                for (int j = 0; j < numThreads; j++) {
                    int startX = i * blockWidth;
                    int startY = j * blockHeight;
                    Thread thread = new Thread(new ImageProcessor(image, startX, startY, blockWidth, blockHeight));
                    thread.start();
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

2.效果

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

3.代码

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.Arrays;
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class BubbleSortVisualization extends JFrame {
    private int[] array;
    private int[] sortedArray;
    private JPanel originalBarPanel;
    private JPanel sortedBarPanel;
    private JTextField lengthField;
    private JTextField threadField;

    public BubbleSortVisualization() {
        setTitle("冒泡排序可视化");
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setLayout(new BorderLayout());

        // 顶部输入框和按钮
        JPanel inputPanel = new JPanel();
        inputPanel.setLayout(new GridLayout(1,6));
        JLabel lengthLabel = new JLabel("数组长度:");
        lengthField = new JTextField(10);

        JLabel threadLabel = new JLabel("线程数:");
        threadField = new JTextField(10);

        JButton generateButton = new JButton("生成数组");
        JButton sortButton = new JButton("开始排序");
        generateButton.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                generateArray();
            }
        });

        inputPanel.add(lengthLabel);
        inputPanel.add(lengthField);
        inputPanel.add(threadLabel);
        inputPanel.add(threadField);
        inputPanel.add(generateButton);
        inputPanel.add(sortButton);

        // 中间柱状图面板
        originalBarPanel = new JPanel() {
            @Override
            protected void paintComponent(Graphics g) {
                super.paintComponent(g);
                drawOriginalBars(g);
            }
        };

        sortedBarPanel = new JPanel() {
            @Override
            protected void paintComponent(Graphics g) {
                super.paintComponent(g);
                drawSortedBars(g);
            }
        };

        // 底部排序按钮

        sortButton.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                int threads = Integer.parseInt(threadField.getText());
                sortArray(threads);
            }
        });

        JPanel barPanelContainer = new JPanel(new GridLayout(2, 1));

        barPanelContainer.add(originalBarPanel);
        barPanelContainer.add(sortedBarPanel);

        add(inputPanel, BorderLayout.NORTH);
        add(barPanelContainer, BorderLayout.CENTER);

        setSize(1500,600);


        setLocationRelativeTo(null);
        setVisible(true);
        array= new int[]{2, 5, 1, 6, 8, 7, 9, 11, 15, 17};
        originalBarPanel.repaint();
        sortedArray=new int[]{1,2,5,6,7,8,9,11,15,17};
        sortedBarPanel.repaint();
    }

    private void generateArray() {
        try {
            int length = Integer.parseInt(lengthField.getText());
            array = new int[length];
            sortedArray = new int[length];
            Random random = new Random();
            for (int i = 0; i < length; i++) {
                int num = random.nextInt(100);
                array[i] = num;
                sortedArray[i] = num;
            }
            originalBarPanel.repaint();

        } catch (NumberFormatException e) {
            JOptionPane.showMessageDialog(this, "请输入有效的数组长度", "错误", JOptionPane.ERROR_MESSAGE);
        }
        lengthField.setText(" ");
    }

    private void sortArray(int threads) {
        ExecutorService executorService = Executors.newFixedThreadPool(threads);
        int chunkSize = array.length / threads;
        for (int i = 0; i < threads; i++) {
            int start = i * chunkSize;
            int end = (i == threads - 1) ? array.length : (i + 1) * chunkSize;
            executorService.execute(new BubbleSortRunnable(start, end));
        }
        executorService.shutdown();
        while (!executorService.isTerminated()) {
            // 等待所有线程完成排序
        }
        bubbleSort(sortedArray); // 主线程进行冒泡排序
        sortedBarPanel.repaint();
        threadField.setText(" ");
    }

    private void bubbleSort(int[] arr) {
        int n = arr.length;
        for (int i = 0; i < n - 1; i++) {
            for (int j = 0; j < n - i - 1; j++) {
                if (arr[j] > arr[j + 1]) {
                    int temp = arr[j];
                    arr[j] = arr[j + 1];
                    arr[j + 1] = temp;
                }
            }
        }
    }

    private void drawOriginalBars(Graphics g) {
        int barWidth = originalBarPanel.getWidth() / array.length;
        int barHeightScale = originalBarPanel.getHeight() / Arrays.stream(array).max().getAsInt();
        g.setColor(Color.BLUE);
        for (int i = 0; i < array.length; i++) {
            int barHeight = array[i] * barHeightScale;
            g.fillRect(i * barWidth, originalBarPanel.getHeight() - barHeight, barWidth, barHeight);
        }
    }

    private void drawSortedBars(Graphics g) {
        int barWidth = sortedBarPanel.getWidth() / sortedArray.length;
        int barHeightScale = sortedBarPanel.getHeight() / Arrays.stream(sortedArray).max().getAsInt();
        g.setColor(Color.RED);
        for (int i = 0; i < sortedArray.length; i++) {
            int barHeight = sortedArray[i] * barHeightScale;
            g.fillRect(i * barWidth, sortedBarPanel.getHeight() - barHeight, barWidth, barHeight);
        }
    }

    private class BubbleSortRunnable implements Runnable {
        private int start;
        private int end;

        public BubbleSortRunnable(int start, int end) {
            this.start = start;
            this.end = end;
        }

        @Override
        public void run() {
            for (int i = start; i < end - 1; i++) {
                for (int j = start; j < end - i - 1; j++) {
                    if (sortedArray[j] > sortedArray[j + 1]) {
                        int temp = sortedArray[j];
                        sortedArray[j] = sortedArray[j + 1];
                        sortedArray[j + 1] = temp;
                    }
                }
            }
        }
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                new BubbleSortVisualization();
            }
        });
    }
}
  • 16
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

从零开始的奋豆

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

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

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

打赏作者

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

抵扣说明:

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

余额充值