【学习笔记】线程(八)之线程通信
生产者消费者问题
这是一个线程同步问题,生产者和消费者共享同一个资源,并且生产者和消费者之间互相依赖,互为条件
-
对于生产者,没有生产产品之前,要通知消费者等待,而生产了产品之后,又需要马上通知消费者消费
-
对于消费者,在消费之后,要通知生产者已经结束消费,需要生产新的产品以供消费
-
-
synchronized 可阻止并发更新同一共享资源,实现了同步
-
synchronized 不能用来实现不同线程之间的消息传递(通信)
-
在Object 类中有两个关于线程通信的方法:
wait() //表示线程一直等待,直到其他线程通知,与sleep不同,会释放锁
wait(long timeout) //指定等待的毫秒数
notify() //唤醒一个处于等待状态的线程
notifyAll() //唤醒同一个对象上所有调用wait() 方法的线程,优先级别高的线程优先调度
注意:这两个方法,都只能在同步方法或同步代码块中使用,否则会抛出异常IIIegaIMonitorStateException
解决方式1:管程法
并发协作模型”生产者/消费者模式“--->管程法
-
生产者:负责生产数据的模块(可能是方法、对象、线程、进程)
-
消费者:负责处理数据的模块(可能是方法、对象、线程、进程)
-
缓冲区:消费者不能直接使用生产者的数据,它们之间有个”缓冲区“
生产者将生产好的数据放入缓冲区,消费者从缓冲区中拿出数据
package com.thread.cooperate;
public class TestPC {
public static void main(String[] args) {
SynContainer synContainer = new SynContainer();
new Productor(synContainer).start();
new Consumer(synContainer).start();
}
}
//产品
class Chicken{
int id;
public Chicken(int id) {
this.id = id;
}
}
//生产者
class Productor extends Thread{
SynContainer synContainer;
public Productor(SynContainer synContainer) {
this.synContainer = synContainer;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
synContainer.push(new Chicken(i));
System.out.println("生产了" + i +"只鸡");
}
}
}
//消费者
class Consumer extends Thread{
SynContainer synContainer;
public Consumer(SynContainer synContainer) {
this.synContainer = synContainer;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println("消费了"+synContainer.pop().id+"只鸡" );
}
}
}
//缓冲区
class SynContainer{
Chicken[] chickens = new Chicken[10];
int count = 0;
//生产者放入产品
public synchronized void push(Chicken chicken) {
//如果容器满了,需要生产者等待
while (count == chickens.length){
try {
this.wait();
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//如果没有满,需要生产者放入产品
chickens[count] = chicken;
count++;
//通知消费者来消费
this.notifyAll();
}
//消费者消费产品
public synchronized Chicken pop(){
//等待生产者生产
while(count <= 0){
try {
this.wait();
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//消费
count--;
Chicken chicken = chickens[count];
//通知生产者生产
this.notifyAll();
return chicken;
}
}
在这个案例中,生产者和消费者的运行速度不同,所以可能会出现生产者还没有打印出生产第十只鸡,然而消费者却已经打印出消费了第十只鸡的情况
还有一种情况:消费者已经消费了第9只鸡,却没有打印,而生产者被通知可以生产后,立马生产并打印出了第十只鸡,所以造成了生产者连续生产了11只鸡的情况
所以我在程序中加入了sleep,分别在生产者和消费者被唤醒后加了100ms,在消费者消费前加了100ms,这样就不会出现上述问题了
当有多个消费者或生产者时,在判断容器是否满了,或者空了时,不要用if来判断,要用while,因为存在虚假唤醒。
-
虚假唤醒:
-
在一般条件下,当线程进入wait状态下,需要其他线程调用notify方法后,线程才会从wait方法中返回, 而虚假唤醒是指线程通过其他方式,从wait方法中返回。
-
举一个例子:当购买车票时,线程A买票,如果发现没有余票,则会调用wait方法,线程进入等待队列中,线程B进行退票操作,余票数量加一,然后调用notify 方法通知等待线程,此时线程A被唤醒执行购票操作。如果是按照if(余票数<0)判断,程序按逻辑完全可行。
那么此时线程A的状态应该如下: (1)释放锁并阻塞
(2)等待条件cond发生
(3)获取通知后,竞争获取锁
但如果在B线程退票同时,进来一个线程C,此时由于synchronized,导致C进入阻塞状态,而当B退票后,余票>0,此时C线程立即竞争到锁余票减一,而此时A线程还处于获取竞争锁状态,继续执行,此时余票是没有的,导致系统报出异常。
-
解决方法:
使用which方法代替if方法即可,在每一次调用都判断是否满足条件,但如果一直在which循环中也有缺点,那就是会导致CPU消耗。
解决方式2:信号灯法
设置一个标志位,如果为真就等待,如果为假就通知另一个线程
通过一个演员观众的例子来了解信号灯法
package com.thread.cooperate;
public class TestPC2 {
public static void main(String[] args) {
TV tv = new TV();
new Player(tv).start();
new Watcher(tv).start();
}
}
//生产者-->演员
class Player extends Thread{
TV tv;
public Player(TV tv){
this.tv = tv;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
if (i%2==0){
tv.play("声生不息");
}else {
tv.play("广告");
}
}
}
}
//消费者-->观众
class Watcher extends Thread{
TV tv;
public Watcher(TV tv){
this.tv = tv;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
tv.watch();
}
}
}
//产品-->节目
class TV{
String voice; //节目
boolean flag = true; //标志位
//演员表演
public synchronized void play(String voice){
if (!flag){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("演员表演了:" + voice);
this.notifyAll(); //通知观众
this.voice = voice;
this.flag = !this.flag;
}
//观众观看
public synchronized void watch(){
if (flag){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("观众观看了:" + voice);
this.notifyAll();
this.flag = !this.flag;
}
}
线程池
-
背景:我们经常创建和销毁、使用特别大量的资源,比如并发情况下的线程,对性能影响特别大
-
思路:我们可以提前创建一些线程,放在线程池中,使用时直接获取,可以比卖你频繁地创建和销毁,实现重复利用
-
好处:
-
提高响应速度
-
降低资源消耗
-
便于线程管理
-
使用线程池:
-
ExecutorService:真正的线程池接口,常见子类ThreadPoolExecutor
-
void execute(Runnable command):执行任务/命令,没有返回值,一般用来执行Runnable
-
< T >Future <T> submit(Callable<T> task) :执行任务,有返回值,一般用来执行Callable
-
void shutdown():关闭线程池
-
-
Executor:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
package com.thread.cooperate;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TestPool {
public static void main(String[] args) {
//创建线程池
//参数是线程池大小
ExecutorService service = Executors.newFixedThreadPool(10);
//执行
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());
//关闭连接
service.shutdown();
}
}
class MyThread implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!