Java中的生产者/消费者模型

一、什么是生产者/消费者模型

生产者-消费者模型(Producer-Consumer problem)是一个非常经典的多线程并发协作的模型。

比如某个模块负责生产数据,而另一个模块负责处理数据。产生数据的模块就形象地被称为生产者;而处理数据的模块,则被称为消费者。

生产者和消费者在同一段时间内共用同一个存储空间,生产者往存储空间中添加产品,消费者从存储空间中取走产品,当存储空间为空时,消费者阻塞,当存储空间满时,生产者阻塞。

如图所示:

 二、生产者-消费者模式的优点

1、解耦

由于有缓冲区的存在,生产者和消费者之间不直接依赖,耦合度降低。

2、支持并发

由于生产者与消费者是两个独立的并发体,它们之间是通过缓冲区作为桥梁连接,生产者只需要往缓冲区里丢数据,就可以继续生产下一个数据,而消费者只需要从缓冲区中拿数据接口,这样就不会因为彼此的处理速度而发生阻塞。(通过使用多个生产者和消费者线程,可以实现并发处理,提高系统的吞吐量和响应性)

3、支持忙闲不均

缓冲区还有另一个好处:当数据生产过快的时候,消费者来不及处理,未处理的数据可以暂时存在缓冲区中。等消费者处理掉其他数据时,再从缓存区中取数据来处理。(通过使用缓冲区可以平衡生产者与消费者之间的速度差异,以及处理能力的不匹配)

三、生产者-消费者模式所遵循的规则

  • 生产者仅仅在缓冲区未满时生产,缓冲区满则停止生产。
  • 消费者仅仅在缓冲区有产品时才能消费,缓冲区为空则停止消费。
  • 当消费者发现缓冲区没有可消费的产品时会通知生产者。
  • 当生产者生产出可消费的产品时,应该通知等待的消费者去消费。

四、生产者-消费者模型的实现

1、通过阻塞队列方式实现

public class ProducerConsumerDemo1 {

    /**
     * 缓冲队列
     */
    private final ArrayBlockingQueue<Integer> blockingQueue = new ArrayBlockingQueue<>(10);

    /**
     * 生产者
     */
    class Producer implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                blockingQueue.offer(i);
            }
        }
    }

    /**
     * 消费者
     */
    class Consumer implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                blockingQueue.poll();
            }
        }
    }

    public Producer getProducer() {
        return new Producer();
    }

    public Consumer getConsumer() {
        return new Consumer();
    }


    public static void main(String[] args) {
        ProducerConsumerDemo1 producerConsumerDemo1 = new ProducerConsumerDemo1();
        new Thread(producerConsumerDemo1.getProducer()).start();
        new Thread(producerConsumerDemo1.getConsumer()).start();
    }
}

2、通过wait和notifyAll来实现

    /**
     * 缓冲区(仓库)
     */
    private final List<Object> list = new ArrayList<>();

    private final int bufferCount = 10;

    public final Object lock = new Object();

    /**
     * 生产者
     */
    class Producer implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                synchronized (lock) {
                    while (list.size() >= bufferCount) {
                        try {
                            lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    //仓库未满,继续生产产品
                    list.add(new Object());
                    //唤醒消费者去消费产品
                    lock.notifyAll();
                }
            }
        }
    }

    /**
     * 消费者
     */
    class Consumer implements Runnable {

        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                synchronized (lock) {
                    while (list.size() == 0) {
                        try {
                            lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    //仓库不是空,继续消费
                    list.remove(0);
                    //唤醒生产者去生产产品
                    lock.notifyAll();
                }
            }
        }
    }

    public Producer getProducer(){
        return new Producer();
    }

    public Consumer getConsumer(){
        return new Consumer();
    }

    public static void main(String[] args) {
        ProducerConsumerDemo2 producerConsumerDemo2=new ProducerConsumerDemo2();
        new Thread(producerConsumerDemo2.getProducer()).start();
        new Thread(producerConsumerDemo2.getConsumer()).start();
    }
}

3、通过ReentrantLock和Condition来实现

public class ProducerConsumerDemo3 {

    /**
     * 缓冲区(仓库)
     */
    private final List<Object> list = new ArrayList<>();
    /**
     * 缓冲区大小
     */
    private final int bufferCount = 10;
    public ReentrantLock lock = new ReentrantLock();
    //创建两个条件变量
    private final Condition condition1 = lock.newCondition();
    private final Condition condition2 = lock.newCondition();


    /**
     * 生产者
     */
    class Producer implements Runnable {

        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                try {
                    Thread.sleep(1000);//模拟生产操作
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                try {
                    lock.lock();
                    while (list.size() >= bufferCount) {
                        condition1.await();//当仓库数据数量超过缓冲区设定的最大数量,则让生产线程进入等待状态
                    }

                    list.add(new Object());
                    System.out.println(Thread.currentThread().getName() + "-生产者生产,数量为:" + list.size());
                    condition2.signal();//唤醒消费线程
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }

            }
        }
    }

    class Consumer implements Runnable {

        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                try {
                    lock.lock();
                    while (list.size() == 0) {
                        condition2.await();//当仓库中数据为空时,则让消费线程进入等待状态
                    }
                    list.remove(0);
                    System.out.println(Thread.currentThread().getName() + "-消费者消费,数量为:" + list.size());
                    condition1.signal();//唤醒生产线程
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
            }
        }
    }

    public Producer getProducer() {
        return new Producer();
    }

    public Consumer getConsumer() {
        return new Consumer();
    }

    public static void main(String[] args) {
        ProducerConsumerDemo3 producerConsumerDemo3 = new ProducerConsumerDemo3();
        new Thread(producerConsumerDemo3.getProducer()).start();
        new Thread(producerConsumerDemo3.getConsumer()).start();
    }
}

五、使用两个线程轮流打印字符串A和字符串B(A和B交替打印,各打印10次。

1、通过wait()和notifyAll()实现:

public class PrintDemo2 {

    private int num = 10;
    private boolean flag;

    private final Object obj = new Object();

    public static void main(String[] args) {
        PrintDemo2 printDemo2 = new PrintDemo2();
        printDemo2.printMethod();
    }

    public void printMethod() {
        Thread thread1 = new Thread("Thread One") {
            @Override
            public void run() {
                super.run();
                for (int i = 0; i < num; i++) {
                    synchronized (obj) {
                        while (flag) {
                            try {
                                obj.wait();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                        System.out.println(Thread.currentThread().getName() + "=========A");
                        flag = true;
                        obj.notifyAll();
                    }
                }

            }
        };

        Thread thread2 = new Thread("Thread Two") {
            @Override
            public void run() {
                super.run();
                for (int i = 0; i < num; i++) {
                    synchronized (obj) {
                        while (!flag) {
                            try {
                                obj.wait();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }

                        System.out.println(Thread.currentThread().getName() + "=========B");
                        flag = false;
                        obj.notifyAll();
                    }
                }
            }
        };

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

}

打印结果如下:

小结:关键字synchronized与wait()、notify()/notifyAll()方法相结合可以实现wait/notify模式,ReentrantLock同样可以实现同样的功能,但需要借助于Condition对象。  下面我们来看一下使用ReentrantLock+Condition来实现的方式。

2、方式二:使用ReentrantLock和Condition实现:

public class PrintDemo {

    private int num = 10;
    private boolean flag;

    public static void main(String[] args) {
        PrintDemo printDemo = new PrintDemo();
        printDemo.printMethod();
    }

    public void printMethod() {
        ReentrantLock lock = new ReentrantLock();
        Condition condition = lock.newCondition();

        Thread thread1 = new Thread("Thread One") {
            @Override
            public void run() {
                super.run();
                for (int i = 0; i < num; i++) {
                    try {
                        lock.lock();
                        while (flag) {
                            condition.await();
                        }

                        System.out.println(Thread.currentThread().getName() + "=========A");
                        flag = true;
                        condition.signalAll();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }finally {
                        lock.unlock();
                    }
                }
            }
        };


        Thread thread2 = new Thread("Thread Two") {
            @Override
            public void run() {
                super.run();
                for (int i = 0; i < num; i++) {
                    try {
                        lock.lock();
                        while (!flag) {
                            condition.await();
                        }

                        System.out.println(Thread.currentThread().getName() + "=========B");
                        flag = false;
                        condition.signalAll();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {
                        lock.unlock();
                    }
                }
            }
        };


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

}

打印结果如下:


       

六、等待通知机制

是指一个线程A调用了对象O的wait()方法进入等待状态,而另一个线程B调用了对象O的notify()或者notifyAll()方法,线程A收到通知后从对象O的wait()方法返回,进而执行后续操作。

  • wait():表示让当前线程进入等待状态,并且释放对象上的锁及CPU资源。然后无限期等待,直到被唤醒为止(注意:调用wait()方法后会释放对象的锁)
  • nofity():通知一个正在等待的线程(是从等待中的线程中随机通知一个),使其从wait方法返回,而返回的前提是该线程获取到了对象的锁,没有获得锁的线程重新进入WAITING状态。
  • notifyAll():与notify方法相同,唯一不同的是,notifyAll是通知所有等待的线程,而notify是随机地通知等待中的线程中的一个。
  • wait(long):超时等待一段时间,这里的参数时间是毫秒,也就是等待n毫秒。线程将等待指定的时间长度后自动苏醒,或者直到其他线程通过调用相同对象上的notify()或者notifyAll()来唤醒它。如果超过了指定时间没有被唤醒,则线程会自动苏醒。
  • wait(long ,int):同上述wait(long),只是对于超时的时间更细粒度的控制,可以达到纳秒。

注意:在调用wait()、notify()、notifyAll()方法之前,线程必须要获得对象的对象级别锁,即只能在同步方法或同步块中调用wait()方法、notify()、notifyAll()方法。

1、等待和通知的标准范式

等待方遵循如下原则:

(1)获取对象的锁

(2)如果条件不满足,那么调用对象的wait()方法,被通知后仍要检查条件

(3)条件满足则执行对应的逻辑

 通知方遵循如下原则:

(1)获得对象的锁

(2)改变条件

(3)通知所有等待在对象上的线程。

2、notify和notifyAll应该用哪一个

尽可能用notifyAll(),谨慎用notify(),因为notify()只会唤醒一个线程,我们无法确保被唤醒的这个线程一定就是我们需要唤醒的线程。

3、为什么wait方法必须在synchronized保护的同步代码块中使用?

先来看看wait方法的源码注释:

“wait method should always be used in a loop:

 synchronized (obj) {
     while (condition does not hold)
         obj.wait();
     ... // Perform action appropriate to condition
}

 This method should only be called by a thread tah is the owner of this object's monitors"

意思就是说,在使用wait方法时,必须把wait方法写在synchronized保护的while代码块中,并始终判断执行条件是否满足,如果满足就往下继续执行,如果不满足就执行wait方法,而在执行wait方法之前,必须先持有锁对象的monitor对象。

4、wait/notify 和 sleep方法的异同?

相同点:

(1)它们都可以让线程阻塞

(2)它们都可以响应interrupt中断:在执行的过程中,如果收到中断信号都可以响应,并抛出InterruptedException异常

不同点:

(1)wait方法必须在synchronized保护的代码中使用,而sleep方法并没有这个要求

(2)在同步代码中执行sleep方法时,并不会释放monitor锁,但执行wait方法时会主动释放monitor锁

(3)sleep方法中要求必须定义一个时间,时间到期后会主动恢复,而对于没有参数的wait方法而言,意味着永久等待,直到被中断或唤醒才能恢复,它并不会主动恢复。

(4)wait/notify 是Object 类的方法,而sleep是Thread类的方法。

5、Java多线程中wait()为什么要放在while循环中

将wait()方法放在while循环中是为了避免虚假唤醒(spurious wakeups)。

虚假唤醒是指当一个线程被唤醒时,尽管没有收到对应的通知信号,但它还是会继续执行。这种情况可能发生在某些特定的条件下,例如操作系统级别的中断、调度器的行为等。

为了防止虚假唤醒导致逻辑或数据不一致性问题,在使用wait()方法进行线程等待时,我们通常会将其放在一个while循环内,并且与需要满足的条件进行检查。只有当满足特定的条件才会继续执行后面的代码。如果不满足条件,则线程会再次进入等待状态。以下是一个示例:

synchronized (lock) {
    while (!condition) {
        lock.wait();
    }
    
    // 执行其他操作
}

通过使用while(condition)来检查条件是否满足,即使发生虚假唤醒也能够安全地重新检查和等待。因此,在多线程编程中建议将wait()方法置于while循环内部以保证正确性。

6、为什么wait/notify/notifyAll 被定义在Object类中,而sleep被定义在Thread类中?

wait(),notify()和notifyAll()方法被定义在Object类中而不是Thread类中的原因是因为它们操作的是对象的锁(monitor)。

这些方法用于实现线程之间的协作与通信机制,即等待其他线程满足特定条件后再继续执行。由于每个对象都有一个关联的锁,因此将这些方法定义在所有对象共享的父类Object中更加合适。通过调用这些方法来控制同步代码块或同步方法内部对锁资源进行管理。

另一方面,sleep()方法并定义在Thread类中,它使当前正在执行的线程暂停指定时间段,并且不会释放持有的任何锁。该方法属于线程级别的操作,在调用时直接影响当前运行状态下的线程自身。

总结起来:

wait(),notify()和notifyAll()是基于对象级别的操作,用于多个线程之间进行协作和通信。

由于每个Java对象都具备监视器(monitor)功能(即互斥锁),所以这些方法需要定义在所有Java对象共享的父类Object中。

而sleep()方法则是一个与当前正在运行状态下的Thread相关联,独立存在并影响其自身行为状态。

 7、ReentrantLock和Condition

Condition类是JDK5的技术,具有更好的灵活性,例如,可以实现多路通知功能,也就是在一个Lock对象中可以创建多个Condition实例,线程对象注册在指定的Condition中,从而可以有选择性地进行线程通知,在调度线程上更加灵活。

        在使用notify()/notifyAll()方法进行通知时,被通知的线程由JVM进行选择,而方法notifyAll()会通知所有的waiting线程,没有选择权,会出现相当大的效率问题,但使用ReentrantLock结合Condition类可以实现”选择性通知“,这个功能是Condition类默认提供的。

       Condition对象的作用是控制并处理线程的状态,它可以使线程呈现wait状态,也可以使线程继续运行。

        Condition的await()方法的作用是使当前线程在接到通知或被中断之前一直处于等待wait状态。它和wait()方法的作用一样。

condition.await()方法调用之前必须先调用lock.lock()方法获得锁,否则会抛出java.lang.IIIleagalMonitorStateException:current thread is not owner。如下所示:

public class PrintDemo3 {

    public static void main(String[] args) {
        PrintDemo3 printDemo3=new PrintDemo3();
        printDemo3.printMethod();
    }

    public void printMethod() {
        ReentrantLock lock = new ReentrantLock();
        Condition condition = lock.newCondition();
        Thread thread1 = new Thread() {
            @Override
            public void run() {
                super.run();
                try {
                    System.out.println("MM");
                    condition.wait();
                    System.out.println("NN");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        thread1.start();
    }
}

控制台的输出:

  • Object类中的wait()方法相当于Condition类中的await()方法。
  • Object类中的wait(long timeout)方法相当于Condition类中的await(long time,TimeUnit unit)方法。
  • Object类中的notify()方法相当于Condition类中的signal()方法。
  • Object类中的notifyAll()方法相当于Condition类中的signalAll()方法。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/52121.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

后端一次返回大量数据,前端做分页处理

问题描述&#xff1a;后端接口返回大量数据&#xff0c;没有做分页处理&#xff0c;不支持传参pageNum&#xff0c;pageSize 本文为转载文章&#xff0c;原文章&#xff1a;后端一次返回大量数据&#xff0c;前端做分页处理 1.template中 分页 <el-paginationsize-chang…

39.手机导航

手机导航 html部分 <div class"phone"><div class"content"><img class"active" src"./static/20180529205331_yhGyf.jpeg" alt"" srcset""><img src"./static/20190214214253_hsjqw…

DHCP部署与安全详解

文章目录 一、DHCP是什么&#xff1f;二、DHCP相关概念三、DHCP优点四、DHCP原理1. 客户机发送DHCP Discovery广播包&#xff08;发现谁是DHCP服务器&#xff09;2. 服务器响应DHCP Offer广播包3. 客户机发送DHCP Request广播包4. 服务器发送DHCP ACK广播包 五、DHCP续约六、部…

ElasticSearch基本使用--ElasticSearch文章一

文章目录 官网学习必要性elasticsearch/kibana安装版本数据结构说明7.x版本说明ElasticSearch kibana工具测试后续我们会一起分析 官网 https://www.elastic.co/cn/ 学习必要性 1、在当前软件行业中&#xff0c;搜索是一个软件系统或平台的基本功能&#xff0c; 学习Elastic…

使用 OpenCV 进行图像模糊度检测(拉普拉斯方差方法)

写在前面 工作中遇到&#xff0c;简单整理人脸识别中&#xff0c;对于模糊程度较高的图像数据&#xff0c;识别率低&#xff0c;错误率高。虽然使用 AdaFace 模型&#xff0c;对低质量人脸表现尤为突出。但是还是需要对 模糊程度高的图像进行丢弃处理当前通过阈值分类&#xff…

Java开发中的分层开发和整洁架构

分层开发(横向拆分) 分层开发的概念: maven多模块开发项目管理.可以利用这种管理功能,实现一个项目的多层次模块开发–分层开发. 比如,当前项目HelloController依赖HelloService 这样做目的: 复杂开发过程.解耦(不调整依赖关系,无法解耦).分层开发(横向拆分)和纵向拆分的区别…

c# 此程序集中已使用了资源标识符

严重性 代码 说明 项目 文件 行 禁止显示状态 错误 CS1508 此程序集中已使用了资源标识符“BMap.NET.WindowsForm.BMapControl.resources” BMap.NET.WindowsForm D:\MySource\Decompile\BMap.NET.WindowsForm\CSC 1 活动 运行程序时&a…

【机器学习】Feature Engineering and Polynomial Regression

Feature Engineering and Polynomial Regression 1. 多项式特征2. 选择特征3. 缩放特征4. 复杂函数附录 首先&#xff0c;导入所需的库&#xff1a; import numpy as np import matplotlib.pyplot as plt from lab_utils_multi import zscore_normalize_features, run_gradien…

session反序列化+SoapClientSSRF+CRLF

文章目录 session反序列化SoapClientSSRFCRLF前言bestphps revengecall_user_func()方法的特性SSRFCRLF组合拳session反序列化 解题步骤总结 session反序列化SoapClientSSRFCRLF 前言 从一道题分析通过session反序列化出发SoapClientSSRF利用CRLF解题 bestphp’s revenge 首…

超详细的74HC595应用指南(以stm32控制点阵屏为例子)

74HC595是一款常用的串行输入/并行输出&#xff08;Serial-in/Parallel-out&#xff09;移位寄存器芯片&#xff0c;在数字电子领域有着广泛的应用。它具有简单的接口和高效的扩展能力&#xff0c;成为了许多电子爱好者和工程师们的首选之一。本文将深入介绍74HC595芯片的功能、…

UE5、CesiumForUnreal加载无高度地形

文章目录 1.实现目标2.实现过程3.参考资料1.实现目标 在UE5中,CesiumForUnreal插件默认的地形都是带高度的,这里加载没有高度的地形,即大地高程为0,GIF动图如下: 2.实现过程 参考官方的教程,下载无高度的DEM,再切片加载到UE中。 (1)下载无高度地形DEM0。 在官方帖子…

网络安全(黑客)自学——从0开始

为什么学习黑客知识&#xff1f;有的人是为了耍酷&#xff0c;有的人是为了攻击&#xff0c;更多的人是为了防御。我觉得所有人都应该了解一些安全知识&#xff0c;了解基本的进攻原理。这样才可以更好的保护自己。这也是这系列文章的初衷。让大家了解基本的进攻与防御。 一、怎…

学习中遇到的好博客

c日志工具之——log4cpp ECU唤醒的本质就是给ECU供电。 小文件&#xff1a;零拷贝技术 传输大文件&#xff1a;异步 IO 、直接 IO&#xff1a;如何高效实现文件传输&#xff1a;小文件采用零拷贝、大文件采用异步io直接io (123条消息) Linux网络编程 | 彻底搞懂…

Pytest学习教程_装饰器(二)

前言 pytest装饰器是在使用 pytest 测试框架时用于扩展测试功能的特殊注解或修饰符。使用装饰器可以为测试函数提供额外的功能或行为。   以下是 pytest 装饰器的一些常见用法和用途&#xff1a; 装饰器作用pytest.fixture用于定义测试用例的前置条件和后置操作。可以创建可重…

6.2.tensorRT高级(1)-第一个完整的分类器程序

目录 前言1. CNN分类器2. 补充知识2.1 知识点2.2 智能指针封装 总结 前言 杜老师推出的 tensorRT从零起步高性能部署 课程&#xff0c;之前有看过一遍&#xff0c;但是没有做笔记&#xff0c;很多东西也忘了。这次重新撸一遍&#xff0c;顺便记记笔记。 本次课程学习 tensorRT …

【雕爷学编程】MicroPython动手做(13)——掌控板之RGB三色灯

知识点&#xff1a;什么是掌控板&#xff1f; 掌控板是一块普及STEAM创客教育、人工智能教育、机器人编程教育的开源智能硬件。它集成ESP-32高性能双核芯片&#xff0c;支持WiFi和蓝牙双模通信&#xff0c;可作为物联网节点&#xff0c;实现物联网应用。同时掌控板上集成了OLED…

第26天-秒杀服务(秒杀系统设计与实现)

1.秒杀设计 1.1.秒杀业务 秒杀具有瞬间高并发特点&#xff0c;针对这一特点&#xff0c;必须要做限流异步缓存&#xff08;页面静态化&#xff09;独立部署。 限流方式&#xff1a; 前端限流&#xff0c;一些高并发的网站直接在前端页面开始限流&#xff0c;例如&#xff1a…

vue项目环境 搭建

1、安装nodejs 2、安装vue-cli, npm i -g vue/cli-init 3、初始化项目 vue init webpack test 4、运行 cd test npm run dev

看完这篇 教你玩转渗透测试靶机Vulnhub——HarryPotter:Aragog(1.0.2)

Vulnhub靶机HarryPotter:Aragog渗透测试详解 Vulnhub靶机介绍&#xff1a;Vulnhub靶机下载&#xff1a;Vulnhub靶机安装&#xff1a;Vulnhub靶机漏洞详解&#xff1a;①&#xff1a;信息收集&#xff1a;②&#xff1a;漏洞发现&#xff1a;③&#xff1a;漏洞利用&#xff1a;…

解决eclipse 打开报错 An error has occurred. See the log file null.

解决eclipse 打开报错an error has ocurred. See the log file null 出现原因&#xff1a;安装了高版本的jdk,更换 jdk 版本&#xff0c;版本太高了。 解决方案&#xff1a;更改环境变量 改成 jkd 1.8