暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

Java面向对象程序设计|模拟生产者-消费者问题

原创 TiAmoZhang 2022-10-21
214

 “同步”实际上是相对于“异步”而言的。异步是指多个线程的运行相互独立,彼此间无依赖性。例如,在输出2的倍数时,不会影响3的倍数的输出。从某种意义上说,并发设计就是从顺序程序中分离出可异步执行的代码段,让其并发执行,以提高整体的执行效率。

同步则是指线程间执行需要遵循某种约定。例如组团外出旅游,约定:各人先自由赶赴某地集合,然后集体观光。这样,团友之间的行动实际上是相关的。

Java实现同步的策略是:互斥+通信。互斥,需要线程间有临界资源,由该资源的状态决定线程是否能执行。当然,各线程要以互斥方式更改资源的状态。例如,集合地就是团友间的临界资源,团友到达集合地后,集合地的状态发生改变(即人数增加)。通信,实际上就是对临界资源的wait()、notify()/notifyAll()的调用。如张三到达集合地后,若人尚未来齐,则执行集合地的wait()方法,让张三等待;若来齐,则执行集合地的notifyAll(),以唤醒所有处在等待状态的人(notify()只能唤醒一个线程),开始执行下一步动作。

注意

wait()、notify()、notifyAll()均为Object类的final方法。这些方法必须直接或间接地用于临界区中,否则,将会产生非法监控锁状态异常(IllegalMonitorStateException)。另外,执行wait()方法的线程会释放锁(这点与sleep()不同)。

01、线程的同步机制-示例

【例1】 有一生产者P、消费者C和缓冲区D,D中只能存放一个产品(int型数据),P、C每次只能生产/消费一个产品,见图2。利用线程同步机制,模拟实现多轮生产-消费。

编辑

 ■ 图2 生产者-消费者问题

目的:掌握同步机制的实现框架,掌握对线程同步执行过程的分析方法。

设计:本例主要设计了三个类:缓冲区、生产者、消费者

BufferArea:包含属性d和状态标记isEmpty,以及放/取产品的方法put(i)/get()方法。这两个方法是本例的设计重点,设计相似,要考虑三项重要操作何时执行:wait()、notify()、修改缓冲区状态(即isEmpty的值)。另外就是放/取(即返回)数据。

put(i):若缓冲区不空则wait(),否则执行{ d=i;  isEmpty=false;  notify(); }。

get() :若缓冲区空则wait(),否则执行{ x=d; isEmpty=true;  notify(); return x; }。

Producer:生产者类,线程体run()会依次产生数据1~6,执行put(i)--输出i;

Consumer:消费者类,线程体run()会执行6次:输出get()信息。

本例展示了两种互斥实现策略,策略1:put/get为synchronized方法,生产者/消费者直接调用put/get,未达到预期效果;策略2:put/get为普通方法,生产者/消费者采用synchronized块方式,此策略完美实现生产-消费同步。注意对两种策略的执行分析。

class BufferArea{ private int d; //用于存放产品
  private boolean isEmpty=true; //缓冲区状态。注:此处定要有初值true,why?
  public  synchronized  void  put(int i){ //策略1:将put方法体设为原子操作序列
  //public void put(int i){ //策略2:在run中设置原子操作序列
      //注:因线程可能会伪唤醒(即自动醒来、小概率事件),故用循环
    while(!isEmpty) try{ wait();}catch(InterruptedException e){;}
    d=i; isEmpty=false; notify();//注意要修改状态标记,并发通知
  }
  public  synchronized  int  get(){ //策略1:将get方法体设为原子操作序列
  //public int get(){ // 策略2:在run中设置原子操作序列
    while(isEmpty)try{ wait(); }catch(InterruptedException e){;}
    isEmpty=true; notify(); return d; //注意要修改状态标记,并发通知
  }
}
class Producer extends Thread{ private BufferArea ba;
  public Producer(BufferArea b){ ba=b; }
  public void run(){ //生产过程
    for (int i = 1; i<6; i++){
      //synchronized(ba){ //使用策略1时需注释此句
        ba.put(i); System.out.print("Producer put: "+i);
        try{sleep(1);}catch(InterruptedException e){;}
      //} //end synchronized:使用策略1时需注释此句
    } //end for
  }
}
class Consumer extends Thread{ private BufferArea ba;
  public Consumer(BufferArea b){ ba=b; }
  public void run(){ //消费过程
    for (int i = 1; i<6; i++){
      //synchronized(ba){ //使用策略1时需注释此句
        System.out.print("\t Consumer get: "+ba.get()+"\n");
        try{sleep(1);}catch(InterruptedException e){;}
      //}//end synchronized:使用策略1时需注释此句
    }
  }
}
public class Ch_5_8{
  public static void main (String[] args) {
    BufferArea b=new BufferArea();
    (new Consumer(b)).start(); (new Producer(b)).start();
  }
}
复制

02、输出结果

(为便于对比,下面列出了两种策略的运行结果。)

按策略1方式的运行结果:        

 Consumer get: 1

Producer put: 1Producer put: 2   Consumer get: 2

Producer put: 3  Consumer get: 3

         Consumer get: 4

Producer put: 4  Consumer get: 5

Producer put: 5
复制

按策略2方式的运行结果:

Producer put: 1  Consumer get: 1

Producer put: 2  Consumer get: 2

Producer put: 3  Consumer get: 3

Producer put: 4  Consumer get: 4

Producer put: 5  Consumer get: 5
复制

03、示例剖析

实现框架:互斥+通信,互斥,是指生产者、消费者线程要有缓冲区作为临界资源,作用为:1. 传递产品数据;2. 借助缓冲区状态决定生产者/消费者能否执行;通信,是指互斥操作中调用wait()或是notify(),从而避免发生“死等”。

JDK说明文档中提醒,线程有时会被“伪唤醒”,即线程未用notify()或notifyAll()也会苏醒(小概率事件)。因此为安全起见,put/get中用while对isEmpty实施检测,这样即使伪唤醒,也会被wait语句再次阻塞。

对两种策略执行过程的分析。其中生产者线程执行6次“put(i)--打印”,消费者线程执行6次“打印get()的数据”。注意,因缓冲区初始为空,决定生产者定会先执行(若消费者先执行会因缓冲区状态而wait)。

策略1执行结果分析:

put(i)/get()方法体(粗线框定部分)以原子方式执行,见图2。生产者put(1)执行①后更改了缓冲区状态,继而执行②,消费者线程被唤醒,put(1)执行结束,生产者释放资源。若此时生产者线程的时间片恰好用完,get()操作将执行(注:此时生产信息尚未输出)。get()执行到③,更改缓冲区状态。get()操作结束,释放资源。若此时消费者时间片尚未用完,则执行④,输出。消费线程继续执行下一轮get()。执行到⑤,因缓冲区状态不符而wait(),切换到生产者线程,执行第一轮的输出,即⑥。之后开始消费者的第二轮生产(注:此时消费者已wait()),执行put(2),至⑦,改缓冲区状态,并唤醒消费者,由于时间片未用完,继续执行⑧。之后若开启第三轮生产,执行put(3),会因状态不符而wait()。故此后必定会转至消费者线程,接续⑤之后的操作,执行⑨,改状态并唤醒生产者,此后因时间片尚未用完,执行⑩。持续上述过程,直至结束。当然,策略1的执行结果是不唯一的。如生产者执行⑧之前,若时间片用完,会切换到消费者线程,输出“get 2”,之后才会输出“put 2”。

编辑

■ 图2 对策略1的执行分析

策略2执行结果分析

见图3,临界区是run中的方法体(粗线框定部分),生产者put(1)执行①后更改了缓冲区状态,继而唤醒消费者线程(注:此时尚未出临界区),并执行②,输出“put -1”。之后有两种可能。(1)生产者时间片用完,消费者执行,即get执行到③,继而执行④,输出“get-1”;(2)生产者时间片未用完,继续执行(3-a),会因状态不符而wait();之后消费者依次执行③、④,输出“get-1”。显然,无论哪种情形,都是先输出“put-1”,再输出“get-1”。

编辑

■ 图3 对策略2的执行分析

思考

若将run()写成:run(){ synchronized(ba){ for(…){…}  } },输出结果会怎样?

小结:同步框架=互斥+通信,要点有三:(1)临界资源的状态决定线程执行的总体次序;(2)线程通过对临界资源的互斥存取,更改临界资源的状态;(3)通过临界资源的发出wait()/notify()等消息,阻塞/唤醒线程,即确保不该执行时不执行(即wait()),该执行时不会死等(即被notify()唤醒)。注意,wait()消息由临界资源X发出,相应地,被阻塞的线程会加到X的等待队列中;类似地,X发出的notify()/notifyAll()消息,仅会唤醒自己的等待队列中的线程。

思考

若缓冲区中用容量为n的数组存储数据,生产者每次生产x个产品(x<n),若当前缓冲区空余不足x个则停止生产;消费者每次消费至多为y个,若产品数为0则停止消费,若产品数大于等于y,则消费y个,否则将消费所有剩余产品。如何模拟上述生产-消费过程?如果存在多个生产者、多个消费者呢?

编辑

「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论