一、生产者消费者问题
生产者-消费者问题,简单描述就是,生产者向仓库中存入生产的产品,消费者从仓库中取走产品消费。
需要满足的三个条件:
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实现》