【Java笔记分享】生产者与消费者模式

笔记源代码地址:B站 库米豪斯巴达锅铲祖师 空间

好好内卷吧家人们,up快要开学了qwq反射机制的笔记可能要服务不到位了

wait() 和 notify() 方法的认识

这两个是有关线程的方法,但是每个类里面一定会有这两个方法。这两个方法存在于Object类中

但是我们不能看源码分析它们是干啥用的qwq

但是没有关系啊,~经过几代程序猿呕心沥血的研究~ 经过一帮大佬对native方法的源码的阅读,现在我们可以了解到:

  • wait()方法被调用时,以该wait()方法所属的对象为锁的synchronized语句会主动抛弃它持有的锁,同时该语句所在的线程进入阻塞状态
  • notify()方法被调用时,会在所有因这个对象进入阻塞状态的线程中随机挑选一个唤醒,即进入就绪状态。线程不会退出同步语句块,而是接着上次的进度接着运行
  • 同时存在一个notifyAll()方法,它会唤醒所有它可以唤醒的线程

注意这几个方法只能写在synchronized语句块或方法中,要不给你报个异常

Exception in thread "main" java.lang.IllegalMonitorStateException: current thread is not owner
	at java.base/java.lang.Object.notify(Native Method)
	at test.Run.main(Run.java:19)

这么说可能有点难懂,咱们用代码来演示一下。这段演示代码是直接写在主方法里的

我们来创建一个线程,以一个Object对象为锁,写一个synchronized代码块

Object obj = new Object();
        
new Thread(() -> {
    synchronized (obj) {
        System.out.println("start");
        try {
            obj.wait();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("end");
    }
}).start();

这里我们用了两个输出语句来指示线程的开始与结束,在它们中间调用wait()方法 运行,我们发现:线程输出了"start",然后不会动了。这就是wait()方法起了作用,它让这个线程放弃了锁并处于阻塞状态

notify()notifyAll()同理,这里我们测试notify方法:我们让主线程等待一百毫秒,确保Thread-0进入等待,然后我们在一个同步代码块中调用notify方法唤醒这个线程。接下来我们在下面加入这段代码

try {
    Thread.sleep(100);
} catch (InterruptedException e) {
    e.printStackTrace();
}

synchronized (obj) {     
    obj.notify();
}

这次的运行结果是:线程输出了start,然后在一个短暂但肉眼可见的停顿后输出了字符串"end"。这意味着线程已经被唤醒

中年教师啰里巴嗦的通病:看例子啊,再次强调这三个方法外面都要包一层synchronized,不然报错了别来找我啊qwq

生产者与消费者模式的实现

然后,我们运用我们之前学的线程知识来实现一个生产者消费者模式

这个模式可以用于解决某些特定需求,比如有做小游戏的同学可以考虑用这个模式来维持敌对生物的数量

整个模式需要3个线程:

  • 生产者线程(Producer):负责生产,当它指定的仓库达到一定容积的时候停止生产
  • 消费者线程(Consumer):负责消费,可以表现为删除对象,当它指定的仓库为空时停止消费
  • 第三方线程:负责运行这两个线程并免费提供一个仓库

由此,我们可以理出这个模式的基本过程:两个线程共用一个仓库(通常表现为集合)程序开始时,生产者线程向仓库中填充对象,同时唤醒消费者线程来消费对象。当仓库为空或仓库已满的时候,一个线程会等待另一个线程生产或消费对象之后给它叫醒

仓库由两个线程共用,两个线程并行,而仓库中的数据被更改,所以这个仓库在运行时需要保证线程安全

知道了这么些事,我们就可以准备代码操作。创建3个类表示3个线程:ProducerConsumerRun,在Run里准备主方法以及仓库,在另两个线程中指定仓库,准备有参构造器。ProducerConsumer需要实现Runnable接口

准备阶段的main方法(我们使用栈结构来充当仓库,这样现象会比较容易分析)

public static void main(String[] args) {
    // 仓库
    Stack<Integer> bowl = new Stack<>();
}

准备阶段的Producer类(Consumer照样写)

public class Producer implements Runnable {
    // 仓库
    Stack<Integer> bowl;

    public Producer(Stack<Integer> bowl) {
        this.bowl = bowl;
    }
    
    @Override
    public void run() {
        
    }
}

由于我们需要一直消费和生产,我们需要一个死循环,然后加入同步语句块,指定仓库为锁。这时两个线程的run方法里面应该是这样的:

while (true) {
    synchronized (bowl) {

    }
}

我们先来解决一下生产者。这里我们假定容器的最大容量是10,那么当容量大于等于10的时候,它需要放弃自己的锁,不再生产对象。接下来在同步语句块中加入这句判断:

if (bowl.size() >= 10) {
    try {
        bowl.wait();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

接下来的的代码应该位于这个判断的正下方。如果程序成功经过了这个判断,那说明仓库里还没存满10个对象。于是,我们要生产对象。测试时,我们也可以输出这个对象来方便判断。整个功能是这样的:

Integer element = bowl.size();
bowl.push(element);
System.out.println("-Producer:加入了 " + element);

(Consumer的输出是没有小短杠的,这是观察方便增加的,强迫症可以删掉)

不用担心Consumer会抢先Producer运行。容器里没有对象,Consumer根本过不了这个判断。所以Producer运行到这里的时候,Consumer一定是蹲在那里盯着仓库,然而仓库里已经有东西了,于是Producer准备给它叫醒

这就是同步代码块中的最后一句,即通知Consumer起来消费

bowl.notify();

接下来我们将视线转到Consumer的同步代码块。同样是先加一步判断,对于消费者来说,只有仓库有东西的时候才能消费

if (bowl.isEmpty()) {
    try {
        bowl.wait();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

过了就消费嘛,这里我们的消费方式是:删除对象并输出

Integer element = bowl.pop();
System.out.println("Consumer:取出了 " + element);

那么消费过后,仓库一定不是满的了,Consumer很不满意,它会试图将Producer叫起来(已经醒着就不会有任何作用)

也是同步代码块的最后一句

bowl.notify();

到这里,我们已经完成了两个线程。然后,我们在主方法里让它们运行起来(都会吧?不放了哈?)这里如果向两个线程的死循环里面加了延时,它们运行起来就会非常和谐,一个生产另一个马上消费,达不到我们需要的效果。所以请大家自学把两个线程设置为守护线程,开启后主线程延时100ms,我们就有了一段样本

在这个程序中,元素的值只能是在[0, 9]范围内的整数,多次测试后发现,这样写的标准现象是:Producer和Consumer刚开始的时候通常会一直生产然后一直消费,好几个循环过后,它们会学会打断别人施法,也就是这个时候能够出现Producer没存满被Consumer截胡消费,消费还没消费完又被截胡回来。多次重新运行后,由于项目编译时间的省略,这种规律性的现象减弱,而随机截胡的现象会增加

其实up很想找一种能增强数据存取随机性的办法,延时1ms和线程让位都试过了,让位3次也试过了,不知道

finally——

哎qwq事情越来越多,然后就要服务不到位了qwq上学比较重要

java
104 views
Comments
登录后评论
Sign In