Producer—Consumer模拟,wait等待池和锁等待池【java养成】

Java养成计划(打卡第27天)


分享之前的思考:

最近不断学习Java基础,最开始是看视频,mooc,B站各类视频都看了,将Java se给过了一遍,确实广度上去了,并且有了一定的深度,也开拓了一些思路,比如UML,设计模式,之后又借了很多书看,发现确实 书本的内容更加翔实,让我自觉还有很多没有掌握,但是各种不同的书籍给人的感觉真的不同,并且对于某个细节具有不同的表述,让人眼前一亮。每个对象有且仅有一把🔒。

比如在例外处理中的try catch,打印例外信息,就很多中,比如直接使用例外类的实例方法,或者使用toString,,,总之解决同一个问题有很多种思路,还有比如倒计时,除了使用System的方法之外,也可以简单一点,直接累计时间就好了,但是system的方法更精确吧,但是由于CPU的随机性,还是不那么精确。

进程是一个程序的一次动态执行,对应程序从代码加载,执行,完毕整个过程

线程是进程执行过程种产生的多条执行线索。 一个线程局势一个程序内部的顺序控制流

线程同步

之前已经分析过synchronized互斥锁产生临界区,保证线程之间不会同时抢夺,混乱。那什么是线程同步呢?

所谓的线程同步是指,相互合作的两个线程需要交换一定的信息,当线程没有获得合作线程发来的信息,线程就等待,一直到有消息才唤醒执行,也就是处于waiting状态,(这里涉及到通信)

这里就用生产者-消费者问题来说明线程同步

Producer—Consumer

假设有两个线程,一个消费者线程,一个生产者线程,它们共用一个有界队列

生产者生产数据放入该队列,消费者线程从队列中取出元素加以利用,该队列是有界的,所以当队列满时,生产者就要处于等待状态,直到消费者取出数据;同样,当队列为空时,消费者线程应该处于等待状态,直到生产者放入数据。

在这里插入图片描述

我们接下来就来模拟这个过程,涉及到的类有Producer,Consumer,队列结点类QueueNode,队列Queue

Producer类,主要定义生产者线程的run方法

Producer类中与Queue的关联关系为has-a

package ThreadDemo;

public class Producer implements Runnable{
	Queue q;
	public Producer(Queue q) {
		this.q = q;
		new Thread(this).start();//启动线程,this就是传入的对象
	}
	@Override
	public void run() {//生产10个数,放入队列,每生产一个就睡眠1s
		for(int i = 1;i <= 10;i++)
		{
			//调用Queue的enqueue方法就是入队
			q.enqueue(Integer.valueOf(i));//还有一种使用构造方法的new Integer(i)弃用了,使用类方法valuleOf
			System.out.println("produced :" + i);
			try
			{
				Thread.sleep(1000);
			}catch(InterruptedException e) {
				e.printStackTrace();
			}
		}
	}

}
//这里生产一次就要睡眠
Consumer类,主要是定义run方法

只要调用构造方法就可以启动线程

package ThreadDemo;

final class Consumer implements Runnable{
	Queue q;
	public Consumer(Queue q) {
		this.q = q;
		new Thread(this).start();
	}
	@Override
	public void run() {//从有界队列中取出10个数
		for(int n = 1;n <= 10;n++)
		{
			int i = (Integer)(q.dequeue());
			System.out.println("Consumed:" + i);
			try
			{
				Thread.sleep(1000);
			}catch(InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
}
//这里int型数据直接接收了Integer类型数据,因为intValue不可用这里
起始类,主要是创建一个队列,并用队列启动线程
package ThreadDemo;

public class ConsumerTest {
	public static void main(String[] args) {
		Queue q = new Queue(6);
		new Producer(q);
		new Consumer(q);
	}
}
//这里队列的容量为6
队列结点类

其实就是C语言里的一个链表的结点,引用就是指针,所以操作和之前一样,一个存储数据的data,一个指向下一个结点的指针next

package ThreadDemo;

public class QueueNode {//结点类,队列是是以链表形式组织,每个队列结点包含一个指向Object型对象的引用,和指向另一个队列结点的引用,就是(链队列
	Object data;
	QueueNode next;
	public QueueNode(Object o) {//上转型
		this.data = o;
		this.next = null; //其实这个就相当与C里面的指针,对象型变量都和指针一样存储的是地址
	}
	//返回结点中存储的数据
	public Object getData() {
		return data;
	}
}
队列类Queue

创建一个队列,有队首指针和队尾指针,队列先进先出,队首删除,队尾插入,所以插入直接使用尾插法

package ThreadDemo;

public class Queue {//数据结构中的队列,这里来实现,就是要有队首元素,队尾元素,入队,出队,长度等各种属性和方法
	private QueueNode firstNode; //指向队首结点的引用
	private QueueNode lastNode;
	private int size; //容量
	private int lenth; //长度
	//构造方法,常见一个容量为n的空队列
	public Queue(int n) {
		this.size = n;
		this.lenth = 0;
		this.firstNode = this.lastNode = null;
	}
	//添加一个结点
	public void enqueue(Object item)//放入一个对象型数据
	{
		//若队列满,就睡眠等待,之后执行之后就创建
		while(lenth == size)
		{
			try
			{
				Thread.sleep(1000);
			}catch(InterruptedException e) {
				e.printStackTrace();
			}
		}
		//队列为空,就放入一个结点
		if(lenth == 0)
		{
			firstNode = lastNode = new QueueNode(item);
		}
		else {
			lastNode = lastNode.next = new QueueNode(item);//尾插法创建链表
		}
		lenth++;
	}
	
	//出队,返回队首的对象
	public Object dequeue()
	{
		//队列为空就睡眠
		while(lenth == 0)
		{
			try
			{
				Thread.sleep(1000);
			}catch(InterruptedException e) {
				e.printStackTrace();
			}
		}
		Object o = firstNode.data;//就是链表
		//删除队首元素,改变指针
		if(firstNode.equals(lastNode)) {//两个引用指向同一个地址
			firstNode = lastNode = null;
		}
		else {
			firstNode = firstNode.next; //这里的next就只要是个对象类型都能过达到效果,就是随便一个辅助指针p
		}
		lenth--;
		return o;
	}
}
//要注意入队和出队都要改变长度

这样之后就可以运行起始类,这里为多线程,与CPU有关

可能会抛出异常

produced :1
produced :2
Consumed:1
Exception in thread “Thread-0” Exception in thread “Thread-1”

Cannot assign field “next” because “this.lastNode” is null

NullPointerException: Cannot read field “data” because “this.firstNode” is null

这是什么原因,来模拟一下

生产者线程将数据入队,判断队列为空,所以就不沉睡,produced 1;

生产者沉睡,消费者线程和生产者之后同时进入了某段代码执行,在取出了1之后,生产者还是按照有结点的方式来创建,但是此时lastNode已经是空了,报错,不能生产,消费者也就按照有节点执行,但firtNode为空,报错

使用synchronized上锁

所以这里就必须加锁了,这里我们就给队列Queue的两个方法上锁,synchronized修饰成为临界区,只有一个线程能执行

public synchronized Object dequeue()

public synchronized void enqueue(Object item)

这样按照昨天的锁的解释,当生产者线程进入队列时,队列对象就上锁,关闭,当生产者线程出来之后,锁打开,消费者线程才能进入

这样如果情况恰当,就不会混乱

produced :1
Consumed:1
produced :2
Consumed:2
produced :3
Consumed:3
produced :4
Consumed:4
produced :5
Consumed:5
produced :6
Consumed:6
produced :7
Consumed:7
produced :8
Consumed:8
produced :9
Consumed:9
produced :10
Consumed:10

但多执行几次,发现产生新的问题

produced :1
Consumed:1
produced :2
Consumed:2

本来需要生产10个数,但这里生产两个数之后就卡住了,成为了死锁状态

这是怎么回事,这里就和对象🔒有关

比如这里Consumer线程执行拿出数据2,接下来生产者和消费者线程都可能执行,CPU给了消费者线程会怎么样?

消费者线程发现队空,进入睡眠状态,但就算sleep放弃了CPU,但是对象锁依然事关闭的,所以生产者线程不能进入 ,沉睡1s之后,因为生产者线程还在锁等待池等待,不能进入enqueue,所以队空,消费者就继续沉睡,形成了死锁

怎么解决死锁问题?

说白了,问题的关键就是sleep方法不会开锁,也即是关锁沉睡

那就应该有一个替代sleep的方法,对象状态不适合继续处理时,线程应该放弃CPU并同时开锁,进入等待状态,继续执行

  • public final void wait() throws InterruptedException 使执行线程放弃CPU并释放对象🔒,进入该对象的wait等待池
  • public final void notify() 从wait等待池中唤醒一个线程,并将其放入🔒等待池
  • public final void notifyAll() 将wait等待池中的所有线程唤醒,并移入🔒等待池

这三个方法可以作为sleep的替代方法,并且这三个方法只能在线程持有对象锁时才能运行

锁等待池,和wait等待池的级别都不一样,wait是线程三种状态之一,而锁等待只是执行时保证互斥的手段

在这里插入图片描述

这里就演示了线程的状态,当t1时刻进入临界区执行同步代码并关闭对象锁,之后进入wait等待并开锁,线程2就进入临界区执行代码,关锁,给一个notify信号让线程1进入锁等待池,线程2开锁之后,线程1执行

所以这里我们就重新定义下Queue类,使用wait方法和notify方法

public synchronized void enqueue(Object item)//放入一个对象型数据
	{
		//若队列满,就睡眠等待,之后执行之后就创建
		while(lenth == size)
		{
			try {
				this.wait();//当前线程进入等待池 
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		//队列为空,就放入一个结点
		if(lenth == 0)
		{
			firstNode = lastNode = new QueueNode(item);
		}
		else {
			lastNode = lastNode.next = new QueueNode(item);//尾插法创建链表
		}
		lenth++;
		this.notify();//将线程唤醒移入锁等待池
	}
	
	//出队,返回队首的对象
	public synchronized Object dequeue()
	{
		//队列为空就睡眠
		while(lenth == 0)
		{
			try
			{
				this.wait();
			}catch(InterruptedException e) {
				e.printStackTrace();
			}
		}
		Object o = firstNode.data;//就是链表
		//删除队首元素,改变指针
		if(firstNode.equals(lastNode)) {//两个引用指向同一个地址
			firstNode = lastNode = null;
		}
		else {
			firstNode = firstNode.next; //这里的next就只要是个对象类型都能过达到效果,就是随便一个辅助指针p
		}
		lenth--;
		this.notify(); //必须和wait配合使用,如果没有notify程序不能正常执行
		return o;
	}

主要变化就是将sleep换成了wait,并且在方法末尾都加上了notify唤醒

这样之后不管怎么执行都是正确的结果了,当然,这里可以改进,就是,也可以使用另外一个wait方法

  • public void wait(long timeout) throws InterrptedException

规定滞留时间,如果没有notify唤醒,到时间之后就自动进入🔒等待池

sleep和wait的比较
  • sleep是Thread类的静态方法,而wait是实例方法(很容易思考)
  • sleep让线程放弃CPU进入睡眠状态,到时间后进入就绪状态; wait方法使线程放弃CPU进入wait等待池并释放持有的对象锁,需要用notify方法唤醒进入锁等待池,再次获得对象锁时进入就绪状态

今天的分享就到此结束了,之后会给上几个题目,然后分析解答~

  • 4
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值