多线程之生产者-消费者问题(信号量机制)

一、生产者消费者问题

生产者-消费者问题,简单描述就是,生产者向仓库中存入生产的产品,消费者从仓库中取走产品消费。

需要满足的三个条件

1.如果仓库已满,那么生产者不能再向仓库中存入产品,只能等待仓库有空闲;

2.如果仓库为空,那么消费者无法从仓库中取出任何产品进行消费,只能等待仓库有库存产品;

3.生产者和消费者不能同时访问仓库。

注1:条件1和2即是满足了生产者与消费者间的同步,消费者需要等待生产者生产出产品后才可以进行消费;

注2:条件3即是满足了生产者与消费者间的互斥

二、信号量机制实现同步与互斥

1.信号量简介:

信号量是用软件实现的解决临界区访问问题的同步工具,比硬件实现方式更简单。信号量S是一个整型变量,除了初始化外,只能有两个标准原子操作——acquire()和release()来访问。这两个操作原来被称为P操作(测试)和V操作(增加)。信号量有两种:计数信号量(值域不受限制)和二进制信号量(值域0或1)。

假设value表示信号量的整型值

acquire() {
    while value <= 0    //测试
    value--;
}

release() {
    value++;
}

2.使用方式(实现互斥访问,属于忙等):

Semaphore S = new Semaphore(1);    //初值为1,代表同时只有一个资源可供线程访问

S.acquire();
...
//临界区
...
S.release();
...
//剩余区
...

如果此时有两个线程A和B:

A先申请进入临界区,通过acquire()测试后发现S.value>0(意为能获得资源),执行value--(结果value为0),这样A就获得临界区访问权限,可以顺利进入临界区;若此时B也想进入临界区,通过acquire()测试后发现S.value<=0(意为无资源可用),这样B就不断的在此处执行while测试,等待条件不满足获得资源。

当A访问临界区结束后,通过release()操作,执行value++(结果value为1),意味着A释放临界区资源访问权限,A线程继续向下执行剩余代码;此时一直在不断while测试的B线程,突然发现value>0了(也就有资源可用),退出循环后执行value--,进入临界区。

这样就通过信号量,实现了A和B线程对临界区的互斥访问。

3.改进版(克服忙等,加入等待队列)

上述描述的B线程在申请临界区资源访问权限时,不断地在执行while循环,测试条件是否不满足value<=0,这种现象称为忙等(不断占用CPU时间做测试,没有实际意义)。这就衍生出了改进版本的acquire()操作和release()操作,将忙等线程加入等待队列中,等待时机唤醒。如下:

acquire() {
    value--;
    if (value < 0) {
        //1.将当前线程加入到当前信号量的等待队列;
        add this process to list;
        //2.将当前线程挂起阻塞;
        block;
    }    
}

release() {
    value++;
    if (value <= 0) {
        //1.从当前信号量的等待队列上移除一个线程
        remove a process P from list;
        //2.唤醒阻塞线程P
        wakeup(P);
    }
}

 注1:在具有忙等的信号量的经典定义下,信号量的值不可能为负,但是这个实现可能出现负的信号量值。

 注2:1)当信号量值为+n时,代表允许n个线程同时访问临界区资源,特例:当n=1时,即为互斥访问;2)当信号量值为0时,代表不允许任何线程访问临界区资源;3)当信号量值为-n时,代表有n个等待中的线程等待访问临界区。

4.实现同步

同步:也就是两个线程按照一定顺序推进执行,例如:B线程想要执行,必须在A线程完成后。如下伪代码,为基本的同步关系实现。

//信号量值初始为0
Semaphore S = new Semaphore(0);

Thread_A() {
    ...
    //A的执行逻辑;
    ...
    S.release();
}

Thread_B() {
    S.acquire();
    ...
    //B的执行逻辑;
    ...
}

 注:信号量S初值为0,线程B不能得到申请资源,加入S的等待队列,直到线程A执行完release()释放资源,线程B才能被唤醒,继续向下执行B的逻辑,者就实现了一个最简单的A和B的同步过程。

5.死锁发生

死锁,也就是线程A与线程B同时都需要等待对方释放资源后才能继续往下执行时构成的。简单例子如下:

假设P0执行S.acquire(),接着P1执行Q.acquire()。当P0执行Q.acquire()时需要等待,直到P1执行Q.release()。同样,当P1执行S.acquire()时也需要等待,直到P0执行S.release()。这样P0和P1都在等待对方释放资源而无法继续执行下去的状态,就构成了死锁。

Semaphore S = new Semaphore(1);
Semaphore Q = new Semaphore(1);

P0() {
    S.acquire();
    Q.acquire();
    .
    .
    .
    S.release();
    Q.release();
}

P1() {
    Q.acquire();
    S.acquire();
    .
    .
    .
    Q.release();
    S.release();
}

 

三、生产者-消费者问题的代码实现

使用信号量机制实现生产者与消费者同步、互斥访问缓冲区,如下Java代码:

1.定义一个缓冲区接口Buffer,insert() 和 remove() 两个方法;

2.实现一个有限大小缓冲区类BoundedBuffer,实现insert() 和 remove() 两个方法;

2.1.定义互斥信号量 mutex = 1:意为缓冲区一次只能有一个线程访问,即:生产者与消费者线程的互斥访问;

2.2.定义同步信号量 empty = BUFFER_SIZE(缓冲区大小):意为缓冲区空闲单元的资源数量,供生产者生产的产品存入;

2.3.定义同步信号量 full = 0:意为缓冲区满单元(存有产品)的资源数目,供消费者消费(取出)产品;

3.实现生产者类,调用insert()向缓冲区添加产品数据;

4.实现消费者类,调用remove()从缓冲区消费产品数据;

5.实现工厂,启动生产者/消费者线程。

package com.thread.sem.pc;

/*
 * 缓冲区接口
 */
public interface Buffer {
	
	public void insert(Object o);
	
	public Object remove();
}
package com.thread.sem.pc;

import java.util.concurrent.Semaphore;

/*
 * 有限缓冲区
 */
public class BoundedBuffer implements Buffer{

	//缓冲区大小
	private static final int BUFFER_SIZE = 5;
	//缓冲区对象数组
	private Object[] buffer;
	//缓冲区输入输出指针
	private int in, out;
	//缓冲区-访问互斥信号量(生产/消费互斥,初始为 1)
	private Semaphore mutex;
	//空缓冲单元数-访问互斥信号量(初始为BUFFER_SIZE = 5)
	private Semaphore empty;
	//满缓冲单元数-访问互斥信号量(初始为 0)
	private Semaphore full;
	
	public BoundedBuffer() {
		//初始化缓冲区为空
		in = 0;
		out = 0;
		buffer = new Object[BUFFER_SIZE];
		
		//初始化信号量
		mutex = new Semaphore(1);
		empty = new Semaphore(BUFFER_SIZE);
		full = new Semaphore(0);
	}
	
	@Override
	public void insert(Object o) {
		try {
			/*
			 * 申请空缓冲单元资源
			 * 1.当 empty <= 0 时,无资源可用,将当前线程加入信号量empty的等待队列,此时线程处于阻塞状态;
			 * 2.当 empty > 0 时,有资源可用,自动执行'empty--;',线程继续向下执行。
			 */
			empty.acquire();
			/*
			 * 申请缓冲区访问权限
			 * 1.当 mutex <= 0 时,无资源可用,将当前线程加入信号量mutex的等待队列,此时线程处于阻塞状态;
			 * 2.当 mutex > 0 时,有资源可用,自动执行'mutex--;',线程继续向下执行。
			 */
			mutex.acquire();
			
			//向缓冲区插入新元素
			buffer[in] = o;
			in = (in + 1) % BUFFER_SIZE;//循环
			System.out.println(Thread.currentThread().getName()
					+ "生产了一个产品" + in + " : " + o.toString());;
			
			/*
			 * 释放缓冲区访问权限
			 * 1.自动执行'mutex++;';
			 * 2.从信号量mutex的等待队列中唤醒一个线程,使其继续向下执行;
			 * 3.当前线程也继续向下执行。
			 */
			mutex.release();
			/*
			 * 释放满缓冲单元数资源
			 * 1.自动执行'full++;';
			 * 2.从信号量full的等待队列中唤醒一个线程,使其继续向下执行;
			 * 3.当前线程也继续向下执行。
			 */
			full.release();
			
		} catch (InterruptedException ie) {
			ie.printStackTrace();
		}
		
	}

	@Override
	public Object remove() {
		try {
			/*
			 * 申请满缓冲单元资源
			 * 1.当 full <= 0 时,无资源可用,将当前线程加入信号量full的等待队列,此时线程处于阻塞状态;
			 * 2.当 full > 0 时,有资源可用,自动执行'full--;',线程继续向下执行。
			 */
			full.acquire();
			/*
			 * 申请缓冲区访问权限
			 * 1.当 mutex <= 0 时,无资源可用,将当前线程加入信号量mutex的等待队列,此时线程处于阻塞状态;
			 * 2.当 mutex > 0 时,有资源可用,自动执行'mutex--;',线程继续向下执行。
			 */
			mutex.acquire();
			
			//从缓冲区取出一个元素
			Object o = buffer[out];
			out = (out + 1) % BUFFER_SIZE;//循环
			System.out.println(Thread.currentThread().getName()
					+ "取出了一个产品" + out + " : " + o.toString());;
			
			/*
			 * 释放缓冲区访问权限
			 * 1.自动执行'mutex++;';
			 * 2.从信号量mutex的等待队列中唤醒一个线程,使其继续向下执行;
			 * 3.当前线程也继续向下执行。
			 */
			mutex.release();
			/*
			 * 释放空缓冲单元数资源
			 * 1.自动执行'empty++;';
			 * 2.从信号量empty的等待队列中唤醒一个线程,使其继续向下执行;
			 * 3.当前线程也继续向下执行。
			 */
			empty.release();
			
			return o;
			
		} catch (InterruptedException ie) {
			ie.printStackTrace();
			return null;
		} 
	}
	
}
package com.thread.sem.pc;

import java.util.Date;

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

	//缓冲区
	private Buffer buffer;
	
	public Producer(Buffer buffer) {
		this.buffer = buffer;
	}
	
	@Override
	public void run() {
		
		Date message;
		while (true) {
			try {
				//生产者生产产品,并插入缓冲区
				Thread.sleep(1000);
				message = new Date();
				buffer.insert(message);
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		
	}

}
package com.thread.sem.pc;

import java.util.Date;

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

	private Buffer buffer;
	
	public Consumer(Buffer buffer) {
		this.buffer = buffer;
	}
	
	@Override
	public void run() {
		
		Date message;
		while (true){
			try {
				//从缓冲区取出一个产品
				Thread.sleep(1000);
				message = (Date)buffer.remove();
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		
	}

}
package com.thread.sem.pc;

/*
 * 生产消费者工厂
 */
public class Factory {

	public static void main(String[] args) {
		
		//初始化有限缓冲区
		Buffer buffer = new BoundedBuffer();
		
		//创建生产者消费者
		Thread producer = new Thread(new Producer(buffer));
		Thread consumer = new Thread(new Consumer(buffer));
		
		//启动生产消费者
		producer.start();
		consumer.start();
	}
}

程序执行的输出结果(部分): 

Thread-0生产了一个产品1 : Sun Apr 28 23:13:40 CST 2019
Thread-1取出了一个产品1 : Sun Apr 28 23:13:40 CST 2019
Thread-0生产了一个产品2 : Sun Apr 28 23:13:43 CST 2019
Thread-1取出了一个产品2 : Sun Apr 28 23:13:43 CST 2019
Thread-0生产了一个产品3 : Sun Apr 28 23:13:45 CST 2019
Thread-1取出了一个产品3 : Sun Apr 28 23:13:45 CST 2019
Thread-0生产了一个产品4 : Sun Apr 28 23:13:47 CST 2019
Thread-1取出了一个产品4 : Sun Apr 28 23:13:47 CST 2019
Thread-0生产了一个产品0 : Sun Apr 28 23:13:49 CST 2019
Thread-1取出了一个产品0 : Sun Apr 28 23:13:49 CST 2019
Thread-0生产了一个产品1 : Sun Apr 28 23:13:51 CST 2019
Thread-1取出了一个产品1 : Sun Apr 28 23:13:51 CST 2019
Thread-0生产了一个产品2 : Sun Apr 28 23:13:53 CST 2019
Thread-1取出了一个产品2 : Sun Apr 28 23:13:53 CST 2019

四、死锁

如果将remove()改为如下,即:调换full.acquire()和mutex.acquire()的执行顺序。这样如果整个程序先执行消费者Consumer线程,初始时mutex=1、full=0,那么Consumer线程将在mutex上获得资源后,在full上发生阻塞等待;此时生产者Producer线程在完成empty资源申请后,在mutex上(此时mutex=0)将发生阻塞等待。这样Producer线程无法执行下去,等待Consumer线程释放mutex资源;而Consumer线程也无法执行下去,等待Producer线程执行full.release()释放full上的资源。最终就构成了两者的死锁状态

public Object remove() {

    //调换顺序
    mutex.acquire();
    full.acquire();
			
    //从缓冲区取出一个元素
    Object o = buffer[out];
    out = (out + 1) % BUFFER_SIZE;

    mutex.release();
    empty.release();
			
    return o;

}

五、参考书籍

1.《操作系统概念(第7版)--Java实现》

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值