笔记源代码地址: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个线程:Producer
,Consumer
,Run
,在Run里准备主方法以及仓库,在另两个线程中指定仓库,准备有参构造器。Producer
和Consumer
需要实现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上学比较重要