Java多线程13:手写生产者-消费者模式

前言:生产者—消费者模式是如此的重要,是理解 Java 多线程并发和消息队列的核心知识点,不少同学面试时,常规操作是当着面试官的面,手写一个生产者消费者模式。我今年参加美团社招就被要求手写生产者-消费者模式,当时是一脸懵逼。俗话说,从哪里跌倒就要从哪里爬起来,今天我决定手敲一个生产者消费者模式压压惊。


一、生产者-消费者模式

生产者消费者模式,其实很简单。无非就是生产者不停的生产数据,消费者不停的消费数据。

1.1、生产者-消费者模式设计要求

这种设计模式需要满足以下三点要求:

(1)生产者生产数据到缓冲区中,消费者从缓冲区中取数据。

(2)如果缓冲区已经满了,则生产者线程阻塞;

(3)如果缓冲区为空,那么消费者线程阻塞。

编写之前分析:

(1)定义一个缓存队列,选择一个集合当做缓存,给予缓存上限,缓存队列只有两种行为(生产数据和消费数据);

(2)定义一个生产者线程,调用缓存队列中的生产行为;

(3)定义一个消费者线程,调用缓存队列中的消费行为;

1.2、生产者-消费者模实现方式

通常情况下,有三种方式来实现

(1)synchronized + wait() + notify() 方式

(2) 可重入锁ReentrantLock (配合Condition)

(3) BlockingQueue 阻塞队列方式

一般面试官不允许你用阻塞队列实现,所以生产者消费者模式我们可以用sychronized+wait+notify版本写,更加高阶的可以用Reentranlock版本+condition(条件变量)来写。为了达到精准通知的目的,我们还可以使用ReentrantLock版本+多个condition(条件变量来写)。


二、Object的wait-notify-notifyAll简介

2.1、Object的wait、notify和notifyAll方法

wait方法

当一个线程调用一个共享变量的wait()方法时,该调用线程会被阻塞挂起(进入waiting状态),直到发生下面几件事情之一才能返回:

  • 其他线程调用了该共享对象的notify()或者notifyAll()方法;

  • 其他线程调用了该线程的interrupt()方法,该线程抛出InterruptedException异常返回。

另外需要注意的是,如果调用wait()方法的线程没有事先获取该对象的监视器锁,则调用wait()方法时调用线程会抛出IllegalMonitorStateException异常。如果当前线程已经获取了锁资源,调用wait方法之后会释放这个锁资源,但是只会释放当前共享变量上的锁,如果当前线程还持有其他共享变量的锁,则这些锁是不会被释放的。

wait方法还有一个重载方法wait(long time),这个方法会等待time时间,如果在这个时间内没有其他线程来唤醒它的话,这个线程会自己唤醒继续获得执行机会。

notify方法

notify方法会唤醒等待对象监视器的单个线程,如果等待对象监视器的有多个线程,则选取其中一个线程进行唤醒,到底选择唤醒哪个线程是任意的,由CPU自己决定。如果没有再调用notify方法,其他阻塞的线程可能就永远得不到再执行的机会了。

此外,被唤醒的线程不能马上从wait方法返回并继续执行,它必须在获取了共享对象的监视器锁后才可以返回,也就是唤醒它的线程释放了共享变量上的监视器锁后,被唤醒的线程也不一定会获取到共享对象的监视器锁,这是因为该线程还需要和其他线程一起竞争该锁,只有该线程竞争到了共享变量的监视器锁后才可以继续执行。

一个还需要注意的地方是,在共享变量上调用notifyAll()方法只会唤醒调用这个方法前调用了wait系列函数而被放入共享变量等待集合里面的线程。如果调用notifyAll()方法后一个线程调用了该共享变量的wait()方法而被放入阻塞集合,则该线程是不会被唤醒的

类似wait系列方法,只有当前线程获取到了共享变量的监视器锁后,才可以调用共享变量的notify()方法,否则会抛出IllegalMonitorStateException异常。

notifyAll方法

notify方法还有个兄弟方法notifyAll,这个方法会唤醒所有等待监视器对象的线程。

2.2、wait-notify模式的典型应用

wait-notify模式的一个典型应用就是可以实现生产者-消费者模式

有10个人向这个箱子中每次随机放入一个苹果,有10个人每次随机从这个箱子中随机拿走一个苹果,同时需要满足箱子中的苹果总数不能超过50个。请用代码实现上面的场景(不能使用并发集合框架或者阻塞队列)


public class AppleBox {

    private int appleCount;

    public synchronized void putApple() {
        while (appleCount >= 50) {
            try {
                //会释放锁
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        appleCount++;
        String name = Thread.currentThread().getName();
        System.out.println("[" + name + "]放入一个,当前盒子中苹果数:" + appleCount);
        this.notifyAll();
    }


    public synchronized void takeApple() {
        while (appleCount <= 0) {
            try {
                //会释放锁
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        appleCount--;
        String name = Thread.currentThread().getName();
        System.out.println("[" + name + "]拿走一个,当前盒子中苹果数:" + appleCount);
        this.notifyAll();
    }

    private static class AppleTaker implements Runnable {

        private AppleBox appleBox;

        public AppleTaker(AppleBox appleBox) {
            this.appleBox = appleBox;
        }

        @Override
        public void run() {
            while (true) {
                appleBox.takeApple();
                try {
                    TimeUnit.MILLISECONDS.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }


    private static class ApplePutter implements Runnable {

        private AppleBox appleBox;

        public ApplePutter(AppleBox appleBox) {
            this.appleBox = appleBox;
        }

        @Override
        public void run() {
            while (true) {
                appleBox.putApple();
                try {
                    TimeUnit.MILLISECONDS.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }


    public static void main(String[] args) {
        AppleBox appleBox = new AppleBox();

        for (int i = 0; i < 20; i++) {
            Thread t = new Thread(new ApplePutter(appleBox));
            t.setName("ApplePutter:" + i);
            t.start();
        }

        for (int i = 0; i < 20; i++) {
            Thread t = new Thread(new AppleTaker(appleBox));
            t.setName("AppleTaker:" + i);
            t.start();
        }

    }
}

执行结果如下:

[ApplePutter:0]放入一个,当前盒子中苹果数:1
[ApplePutter:1]放入一个,当前盒子中苹果数:2
[ApplePutter:5]放入一个,当前盒子中苹果数:3
[ApplePutter:9]放入一个,当前盒子中苹果数:4
[ApplePutter:13]放入一个,当前盒子中苹果数:5
[ApplePutter:2]放入一个,当前盒子中苹果数:6
[ApplePutter:6]放入一个,当前盒子中苹果数:7
[ApplePutter:10]放入一个,当前盒子中苹果数:8
[ApplePutter:17]放入一个,当前盒子中苹果数:9
[ApplePutter:14]放入一个,当前盒子中苹果数:10
[ApplePutter:18]放入一个,当前盒子中苹果数:11
[ApplePutter:3]放入一个,当前盒子中苹果数:12
[ApplePutter:7]放入一个,当前盒子中苹果数:13
[ApplePutter:11]放入一个,当前盒子中苹果数:14
[ApplePutter:8]放入一个,当前盒子中苹果数:15
[ApplePutter:15]放入一个,当前盒子中苹果数:16
[ApplePutter:19]放入一个,当前盒子中苹果数:17
[ApplePutter:4]放入一个,当前盒子中苹果数:18
[AppleTaker:3]拿走一个,当前盒子中苹果数:17
[ApplePutter:12]放入一个,当前盒子中苹果数:18
[AppleTaker:1]拿走一个,当前盒子中苹果数:17
[AppleTaker:5]拿走一个,当前盒子中苹果数:16
[ApplePutter:16]放入一个,当前盒子中苹果数:17
[AppleTaker:0]拿走一个,当前盒子中苹果数:16
[AppleTaker:12]拿走一个,当前盒子中苹果数:15
[AppleTaker:8]拿走一个,当前盒子中苹果数:14
[AppleTaker:16]拿走一个,当前盒子中苹果数:13
[AppleTaker:7]拿走一个,当前盒子中苹果数:12
[AppleTaker:11]拿走一个,当前盒子中苹果数:11
[AppleTaker:19]拿走一个,当前盒子中苹果数:10
[AppleTaker:9]拿走一个,当前盒子中苹果数:9
[AppleTaker:13]拿走一个,当前盒子中苹果数:8
[AppleTaker:2]拿走一个,当前盒子中苹果数:7
[AppleTaker:6]拿走一个,当前盒子中苹果数:6
[AppleTaker:10]拿走一个,当前盒子中苹果数:5
[AppleTaker:14]拿走一个,当前盒子中苹果数:4
[AppleTaker:4]拿走一个,当前盒子中苹果数:3
[AppleTaker:15]拿走一个,当前盒子中苹果数:2
[AppleTaker:18]拿走一个,当前盒子中苹果数:1
[AppleTaker:17]拿走一个,当前盒子中苹果数:0
[ApplePutter:0]放入一个,当前盒子中苹果数:1
[ApplePutter:1]放入一个,当前盒子中苹果数:2
[ApplePutter:5]放入一个,当前盒子中苹果数:3
[ApplePutter:9]放入一个,当前盒子中苹果数:4
[ApplePutter:13]放入一个,当前盒子中苹果数:5
[ApplePutter:17]放入一个,当前盒子中苹果数:6
[ApplePutter:2]放入一个,当前盒子中苹果数:7
[ApplePutter:6]放入一个,当前盒子中苹果数:8
[ApplePutter:10]放入一个,当前盒子中苹果数:9
[ApplePutter:14]放入一个,当前盒子中苹果数:10
[ApplePutter:18]放入一个,当前盒子中苹果数:11
[ApplePutter:3]放入一个,当前盒子中苹果数:12
[ApplePutter:7]放入一个,当前盒子中苹果数:13
[ApplePutter:11]放入一个,当前盒子中苹果数:14
[ApplePutter:15]放入一个,当前盒子中苹果数:15
[ApplePutter:19]放入一个,当前盒子中苹果数:16
[AppleTaker:3]拿走一个,当前盒子中苹果数:15
[ApplePutter:4]放入一个,当前盒子中苹果数:16
[ApplePutter:8]放入一个,当前盒子中苹果数:17
[ApplePutter:12]放入一个,当前盒子中苹果数:18

**PS: 多线程编程中,最要的重要的两点是先抽象出共享变量是什么,任务类(Runner)是什么 **

2.3、wait-notify模式的经典写法

生产者和消费者的逻辑都可以统一抽象成以下几个步骤:

  • step1:获得对象的锁;

  • step2:循环判断是否需要进行生产活动,如果不需要进行生产就调用wait方法,暂停当前线程;如果需要进行生产活动,进行对应的生产活动;

  • step3:通知等待线程

伪代码如下:

synchronized(对象) {
    //这边进行循环判断的原因是为了防止伪唤醒,也就是不是消费线程或者生产线程调用notify方法将waiting线程唤醒的
    while(条件){
        对象.wait();
    }
    //进行生产或者消费活动
    doSomething();
    对象.notifyAll();
}

三、sychronized+wait()-notify()方法实现

wait()/ nofity()方法是基类Object的两个方法,也就意味着所有Java类都会拥有这两个方法,这样,我们就可以为任何对象实现同步机制。

wait():当缓冲区已满/空时,生产者/消费者线程停止自己的执行,放弃锁,使自己处于等待状态,让其他线程执行。

notify():当生产者/消费者向缓冲区放入/取出一个产品时,向其他等待的线程发出可执行的通知,同时放弃锁,使自己处于等待状态。

生产者

import java.util.Queue;
import java.util.Random;

public class Producer extends Thread {
    private Queue<Integer> queue;
    String name;
    int maxSize;
    int i=0;
    public Producer(String name,Queue<Integer> queue,int maxSize){
        super(name);
        this.name=name;
        this.queue=queue;
        this.maxSize=maxSize;
    }
    @Override
    public void run(){
        while (true){
            synchronized (queue){
                while (queue.size()==maxSize){
                    try{
                        System.out.println("队列已经满了,生产者["+name+"]线程等待"+"消费者从队列中消费产品。");
                        queue.wait();
                    }catch (Exception e){
                        e.printStackTrace();
                    }
                }
                System.out.println("[" + name + "] 生产产品  : +" + i);
                queue.offer(i++);
                queue.notifyAll();
                try{
                    Thread.sleep(new Random().nextInt(1000));
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
            }
        }
    }
}

消费者

import java.util.Queue;
import java.util.Random;

public class Consumer extends Thread {
    private Queue<Integer> queue;
    String name;
    int maxSize;
    public Consumer(String name,Queue<Integer>queue,int maxSize){
        super(name);
        this.name=name;
        this.queue=queue;
        this.maxSize=maxSize;
    }
    @Override
    public void run(){
        while (true){
            synchronized (queue){
                while (queue.isEmpty()){
                    try{
                        System.out.println("队列是空的 消费者[" + name + "] 等待生产者生产");
                        queue.wait();
                    }catch (Exception e){
                        e.printStackTrace();
                    }
                }
                int x=queue.poll();
                System.out.println("[" + name + "] 消费产品 : " + x);
                queue.notifyAll();
                try{
                    Thread.sleep(new Random().nextInt(1000));
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
            }
        }
    }
}

生产者消费者模式
 

import java.util.LinkedList;
import java.util.Queue;

/**
 * 生产者消费者模式:使用Object.wait()/notify()方法实现
 */
public class ProdicerConsumer {
    private static final int CAPACITY = 500;

    public static void main(String[] args) {
        Queue<Integer> queue = new LinkedList<Integer>();
        Thread producer1 = new Producer("P-1", queue, CAPACITY);
        Thread producer2 = new Producer("P-2", queue, CAPACITY);
        Thread consumer1 = new Consumer("C1", queue, CAPACITY);
        Thread consumer2 = new Consumer("C2", queue, CAPACITY);
        Thread consumer3 = new Consumer("C3", queue, CAPACITY);

        producer1.start();
        producer2.start();
        consumer1.start();
        consumer2.start();
        consumer3.start();
    }
}

输出结果: 

 注意要点

判断Queue大小为0或者大于queueSize时须使用 while (condition) {},不能使用 if(condition) {}。其中 while(condition)循环,它又被叫做“自旋锁”。为防止该线程没有收到notify()调用也从wait()中返回(也称作虚假唤醒),这个线程会重新去检查condition条件以决定当前是否可以安全地继续执行还是需要重新保持等待,而不是认为线程被唤醒了就可以安全地继续执行了。


四、使用Reentranlock和Condition的await() / signal()方法实现

在JDK5.0之后,Java提供了更加健壮的线程处理机制,包括同步、锁定、线程池等,它们可以实现更细粒度的线程控制。Condition接口的await()和signal()就是其中用来做同步的两种方法,它们的功能基本上和Object的wait()/ nofity()相同,完全可以取代它们,但是它们和新引入的锁定机制Lock直接挂钩,具有更大的灵活性。通过在Lock对象上调用newCondition()方法,将条件变量和一个锁对象进行绑定,进而控制并发程序访问竞争资源的安全。

生产者消费者模式:使用Lock和Condition实现


import java.util.LinkedList;
import java.util.Queue;
import java.util.Random;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ProducerConsumerByLock {
    private static final int CAPACITY = 5;
    private static final Lock lock = new ReentrantLock();
    private static final Condition fullCondition = lock.newCondition();     //队列满的条件
    private static final Condition emptyCondition = lock.newCondition();        //队列空的条件


    public static void main(String args[]){
        Queue<Integer> queue = new LinkedList<Integer>();

        Thread producer1 = new Producer("P-1", queue, CAPACITY);
        Thread producer2 = new Producer("P-2", queue, CAPACITY);
        Thread consumer1 = new Consumer("C1", queue, CAPACITY);
        Thread consumer2 = new Consumer("C2", queue, CAPACITY);
        Thread consumer3 = new Consumer("C3", queue, CAPACITY);

        producer1.start();
        producer2.start();
        consumer1.start();
        consumer2.start();
        consumer3.start();
    }

    /**
     * 生产者
     */
    public static class Producer extends Thread{
        private Queue<Integer> queue;
        String name;
        int maxSize;
        int i = 0;

        public Producer(String name, Queue<Integer> queue, int maxSize){
            super(name);
            this.name = name;
            this.queue = queue;
            this.maxSize = maxSize;
        }

        @Override
        public void run(){
            while(true){

                //获得锁
                lock.lock();
                while(queue.size() == maxSize){
                    try {
                        System.out .println("队列已经满了,生产者["+name+"]线程等待"+"消费者从队列中消费产品。");
                        //条件不满足,生产阻塞
                        fullCondition.await();
                    } catch (InterruptedException ex) {
                        ex.printStackTrace();
                    }
                }
                System.out.println("[" + name + "] 生产产品  : +" + i);
                queue.offer(i++);

                //唤醒其他所有生产者、消费者
                fullCondition.signalAll();
                emptyCondition.signalAll();

                //释放锁
                lock.unlock();

                try {
                    Thread.sleep(new Random().nextInt(1000));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

        }
    }

    /**
     * 消费者
     */
    public static class Consumer extends Thread{
        private Queue<Integer> queue;
        String name;
        int maxSize;

        public Consumer(String name, Queue<Integer> queue, int maxSize){
            super(name);
            this.name = name;
            this.queue = queue;
            this.maxSize = maxSize;
        }

        @Override
        public void run(){
            while(true){
                //获得锁
                lock.lock();

                while(queue.isEmpty()){
                    try {
                        System.out.println("队列是空的 消费者[" + name + "] 等待生产者生产");
                        //条件不满足,消费阻塞
                        emptyCondition.await();
                    } catch (Exception ex) {
                        ex.printStackTrace();
                    }
                }
                int x = queue.poll();
                System.out.println("[" + name + "] 消费产品 : " + x);

                //唤醒其他所有生产者、消费者
                fullCondition.signalAll();
                emptyCondition.signalAll();

                //释放锁
                lock.unlock();

                try {
                    Thread.sleep(new Random().nextInt(1000));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

输出结果:


五、使用BlockingQueue阻塞队列实现

数据类Data

public class Data {
    private int id;
    //生产量
    private int num;
    public Data(int id,int num){
        this.id=id;
        this.num=num;
    }
    public Data(){

    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public int getNum() {
        return num;
    }

    public void setNum(int num) {
        this.num = num;
    }
}

生产者

import java.util.Random;
import java.util.concurrent.BlockingDeque;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

public class Producer implements Runnable {
    //共享阻塞队列
    private BlockingDeque<Data> queue;
    //是否还在运行
    private volatile boolean isRunning = true;
    //id生成器原子操作
    private static AtomicInteger count = new AtomicInteger();
    // 生成随机数
    private static Random random = new Random();
    public Producer(BlockingDeque<Data> queue){
        this.queue=queue;
    }
    @Override
    public void run() {
        try{
            while(isRunning){
                // 模拟生产耗时
                Thread.sleep(random.nextInt(1000));
                int num=count.incrementAndGet();
                Data data=new Data(num,num);
                System.out.println("当前>>生产者:"+Thread.currentThread().getName()+"生产量"+num);
                if(!queue.offer(data,2, TimeUnit.SECONDS)){
                    System.out.println("生产失败...");
                }
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }
    public void stop(){
        isRunning=false;
    }
}

消费者

import java.util.Random;
import java.util.concurrent.BlockingDeque;

public class Consumer implements  Runnable {
    //双端队列,加入或者取出元素都是线程安全的
    private BlockingDeque<Data> queue;
    private static Random random=new  Random();
    public Consumer(BlockingDeque<Data> queue){
        this.queue=queue;
    }
    @Override
    public void run(){
        while (true){
            try{
                // 检索并删除,如果需要等待、直到元素可用。
                Data data= queue.take();
                //模拟消费耗时
                Thread.sleep(random.nextInt(1000));
                if(data!=null){
                    System.out.println("当前<<消费者:"+Thread.currentThread().getName()+",消费量"+data.getNum());
                }
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }
}

测试

import java.util.concurrent.*;

public class TestPro {
    public static void main(String[] args) throws InterruptedException{
        BlockingDeque<Data> queue = new LinkedBlockingDeque<>(10);

        Producer producer1 = new Producer(queue);
        Producer producer2 = new Producer(queue);
        Producer producer3 = new Producer(queue);

        Consumer consumer1 = new Consumer(queue);
        Consumer consumer2 = new Consumer(queue);
        Consumer consumer3 = new Consumer(queue);

        ExecutorService service= Executors.newCachedThreadPool();
        service.execute(producer1);
        service.execute(producer2);
        service.execute(producer3);
        service.execute(consumer1);
        service.execute(consumer2);
        service.execute(consumer3);

        Thread.sleep(3000);
        producer1.stop();
        producer2.stop();
        producer3.stop();

        Thread.sleep(1000);
        service.shutdown();
    }
}

最后一次生产20此时所有的生产者都停止生产了,但是此时产品池还没空,于是消费者继续消费,直到把产品池中的数据消耗完


参考链接:

手写一个生产者消费者模式

生产者消费者模式java的三种实现

  • 4
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
生产者-消费者问题是一个经典的多线程并发问题,主要涉及到生产者线程和消费者线程之间的协作与同步。在这个问题中,生产者线程负责生产数据,并将数据存入一个共享的缓冲区中,而消费者线程则从缓冲区中取出数据进行消费。为了避免生产者消费者线程之间的竞争条件和死锁问题,需要使用线程同步技术。 下面是一个使用 Python 语言实现的生产者-消费者问题的示例代码: ```python import threading import time import random # 缓冲区大小 BUFFER_SIZE = 5 # 共享的缓冲区 buffer = [] # 生产者线程 class ProducerThread(threading.Thread): def run(self): global buffer while True: # 生产一个随机数 item = random.randint(1, 10) print("生产者生产了数据:", item) # 获取锁 lock.acquire() # 如果缓冲区已满,等待消费者线程消费数据 while len(buffer) >= BUFFER_SIZE: print("缓冲区已满,生产者等待...") lock.wait() # 将数据存入缓冲区 buffer.append(item) print("生产者将数据存入缓冲区:", buffer) # 释放锁 lock.release() # 随机等待一段时间 time.sleep(random.randint(1, 3)) # 消费者线程 class ConsumerThread(threading.Thread): def run(self): global buffer while True: # 获取锁 lock.acquire() # 如果缓冲区为空,等待生产者线程生产数据 while len(buffer) == 0: print("缓冲区为空,消费者等待...") lock.wait() # 从缓冲区取出数据进行消费 item = buffer.pop(0) print("消费者消费了数据:", item) # 释放锁 lock.release() # 随机等待一段时间 time.sleep(random.randint(1, 3)) # 创建锁 lock = threading.Condition() # 创建生产者线程和消费者线程 producer_thread = ProducerThread() consumer_thread = ConsumerThread() # 启动线程 producer_thread.start() consumer_thread.start() # 等待线程结束 producer_thread.join() consumer_thread.join() ``` 在这个示例代码中,我们使用了 Python 中的 Condition 类来实现线程同步和协作。在生产者线程中,如果缓冲区已满,则使用 wait() 方法等待消费者线程消费数据;在消费者线程中,如果缓冲区为空,则使用 wait() 方法等待生产者线程生产数据。当生产者线程向缓冲区中添加数据或消费者线程从缓冲区中取出数据时,需要使用 acquire() 方法获取锁,以避免竞争条件的发生。 需要注意的是,在生产者-消费者问题中,线程同步和协作是非常重要的,如果实现不当,将会导致死锁、竞争条件等问题。因此,在实际开发中,需要仔细设计和测试多线程程序,以确保程序的正确性和稳定性。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Java架构何哥

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

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

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

打赏作者

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

抵扣说明:

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

余额充值