Java多线程-------生产者消费者(转载)

我们这里的生产者和消费者模型为:

    生产者Producer 生产某个对象(共享资源),放在缓冲池中,然后消费者从缓冲池中取出这个对象。也就是生产者生产一个,消费者取出一个。这样进行循环。

 

  第一步:我们先创建共享资源的类 Person,它有两个方法,一个生产对象,一个消费对象

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

public class Person {

    private String name;

    private int age;

     

    /**

     * 生产数据

     * @param name

     * @param age

     */

    public void push(String name,int age){

        this.name = name;

        this.age = age;

    }

    /**

     * 取数据,消费数据

     * @return

     */

    public void pop(){

        System.out.println(this.name+"---"+this.age);

    }

}

  第二步:创建生产者线程,并在 run() 方法中生产50个对象

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

public class Producer implements Runnable{

    //共享资源对象

    Person p = null;

    public Producer(Person p){

        this.p = p;

    }

    @Override

    public void run() {

        //生产对象

        for(int i = 0 ; i < 50 ; i++){

            //如果是偶数,那么生产对象 Tom--11;如果是奇数,则生产对象 Marry--21

            if(i%2==0){

                p.push("Tom"11);

            }else{

                p.push("Marry"21);

            }

        }

    }

}

  第三步:创建消费者线程,并在 run() 方法中消费50个对象

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

public class Consumer implements Runnable{

    //共享资源对象

    Person p = null;

    public Consumer(Person p) {

        this.p = p;

    }

     

    @Override

    public void run() {

        for(int i = 0 ; i < 50 ; i++){

            //消费对象

            p.pop();

        }

    }

}

  由于我们的模型是生产一个,马上消费一个,那期望的结果便是 Tom---11,Marry--21,Tom---11,Mary---21......   连续这样交替出现50次

但是结果却是:

1

2

3

4

5

6

7

8

9

10

11

Marry---21

Marry---21

Marry---21

Marry---21

Marry---21

......

Marry---21

Marry---21

Marry---21

Marry---21

Marry---21

 

为了让结果产生的更加明显,我们在共享资源的 pop() 和 push() 方法中添加一段延时代码

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

/**

     * 生产数据

     * @param name

     * @param age

     */

    public void push(String name,int age){

        this.name = name;

        try {

            //这段延时代码的作用是可能只生产了 name,age为nul,消费者就拿去消费了

            Thread.sleep(10);

        catch (InterruptedException e) {

            e.printStackTrace();

        }

        this.age = age;

    }

    /**

     * 取数据,消费数据

     * @return

     */

    public void pop(){

        try {

            Thread.sleep(10);

        catch (InterruptedException e) {

            e.printStackTrace();

        }

        System.out.println(this.name+"---"+this.age);

    }  

  

这个时候,结果如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

Marry---11

Tom---21

Marry---11

Tom---21

Marry---11

Tom---21

Marry---11

Tom---21

......

Tom---11

Tom---21

Marry---11

Tom---21

Marry---11

Marry---21

  

结果分析:这时候我们发现结果全乱套了,Marry--21是固定的,Tom--11是固定的,但是上面的结果全部乱了,那这又是为什么呢?而且有很多重复的数据连续出现,那这又是为什么呢?

原因1:出现错乱数据,是因为先生产出Tom--11,但是消费者没有消费,然后生产者继续生产出name为Marry,但是age还没有生产,而消费者这个时候拿去消费了,那么便出现 Marry--11。同理也会出现 Tom--21

原因2:出现重复数据,是因为生产者生产一份数据了,消费者拿去消费了,但是第二次生产者生产数据了,但是消费者没有去消费;而第三次生产者继续生产数据,消费者才开始消费,这便会产生重复

解决办法1:生产者生产name和age必须要是一个整体一起完成,即同步。生产的中间不能让消费者来消费即可。便不会产生错乱的数据。如何同步可以参考:

        Java 多线程详解(三)------线程的同步:http://www.cnblogs.com/ysocean/p/6883729.html

     这里我们选择同步方法(在方法前面加上 synchronized)

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

public class Person {

    private String name;

    private int age;

     

    /**

     * 生产数据

     * @param name

     * @param age

     */

    public synchronized void push(String name,int age){

        this.name = name;

        try {

            //这段延时代码的作用是可能只生产了 name,age为nul,消费者就拿去消费了

            Thread.sleep(10);

        catch (InterruptedException e) {

            e.printStackTrace();

        }

        this.age = age;

    }

    /**

     * 取数据,消费数据

     * @return

     */

    public synchronized void pop(){

        try {

            Thread.sleep(10);

        catch (InterruptedException e) {

            e.printStackTrace();

        }

        System.out.println(this.name+"---"+this.age);

    }

}

  结果如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

Marry---21

Marry---21

Marry---21

Marry---21

Marry---21

Tom---11

Tom---11

......

Tom---11

Tom---11

Tom---11

Tom---11

Tom---11

问题:还是没有解决上面的问题2,出现重复的问题。期望的结果是 Tom---11,Marry--21,Tom---11,Mary---21......   连续这样交替出现50次。那如何解决呢?

解决办法:生产者生产一次数据了,就暂停生产者线程,等待消费者消费;消费者消费完了,消费者线程暂停,等待生产者生产数据,这样来进行。

 

 

这里我们介绍一个同步锁池的概念:

  同步锁池:同步锁必须选择多个线程共同的资源对象,而一个线程获得锁的时候,别的线程都在同步锁池等待获取锁;当那个线程释放同步锁了,其他线程便开始由CPU调度分配锁

 

关于让线程等待和唤醒线程的方法,如下:(这是 Object 类中的方法)

  

 

  

 

wait():执行该方法的线程对象,释放同步锁,JVM会把该线程放到等待池中,等待其他线程唤醒该线程

notify():执行该方法的线程唤醒在等待池中等待的任意一个线程,把线程转到锁池中等待(注意锁池和等待池的区别)

notifyAll():执行该方法的线程唤醒在等待池中等待的所有线程,把线程转到锁池中等待。

注意:上述方法只能被同步监听锁对象来调用,这也是为啥wait() 和 notify()方法都在 Object 对象中,因为同步监听锁可以是任意对象,只不过必须是需要同步线程的共同对象即可,否则别的对象调用会报错:        java.lang.IllegalMonitorStateException

 

假设 A 线程和 B 线程同时操作一个 X 对象,A,B 线程可以通过 X 对象的 wait() 和 notify() 方法来进行通信,流程如下:

①、当线程 A 执行 X 对象的同步方法时,A 线程持有 X 对象的 锁,B线程在 X 对象的锁池中等待

②、A线程在同步方法中执行 X.wait() 方法时,A线程释放 X 对象的锁,进入 X 对象的等待池中

③、在 X 对象的锁池中等待锁的 B 线程获得 X 对象的锁,执行 X 的另一个同步方法

④、B 线程在同步方法中执行 X.notify() 方法,JVM 把 A 线程从等待池中移动到 X 对象的锁池中,等待获取锁

⑤、B 线程执行完同步方法,释放锁,等待获取锁的 A 线程获得锁,继续执行同步方法

 

那么为了解决上面重复的问题,修改代码如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

public class Person {

    private String name;

    private int age;

     

    //表示共享资源对象是否为空,如果为 true,表示需要生产,如果为 false,则有数据了,不要生产

    private boolean isEmpty = true;

    /**

     * 生产数据

     * @param name

     * @param age

     */

    public synchronized void push(String name,int age){

        try {

            //不能用 if,因为可能有多个线程

            while(!isEmpty){//进入到while语句内,说明 isEmpty==false,那么表示有数据了,不能生产,必须要等待消费者消费

                this.wait();//导致当前线程等待,进入等待池中,只能被其他线程唤醒

            }

             

            //-------生产数据开始-------

            this.name = name;

            //延时代码

            Thread.sleep(10);

            this.age = age;

            //-------生产数据结束-------

            isEmpty = false;//设置 isEmpty 为 false,表示已经有数据了

            this.notifyAll();//生产完毕,唤醒所有消费者

        catch (Exception e) {

            e.printStackTrace();

        }

         

    }

    /**

     * 取数据,消费数据

     * @return

     */

    public synchronized void pop(){

        try {

            //不能用 if,因为可能有多个线程

            while(isEmpty){//进入 while 代码块,表示 isEmpty==true,表示为空,等待生产者生产数据,消费者要进入等待池中

                this.wait();//消费者线程等待

            }

            //-------消费开始-------

            Thread.sleep(10);

            System.out.println(this.name+"---"+this.age);

            //-------消费结束------

            isEmpty = true;//设置 isEmpty为true,表示需要生产者生产对象

            this.notifyAll();//消费完毕,唤醒所有生产者

        catch (InterruptedException e) {

            e.printStackTrace();

        }

    }

}

  

结果:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

Tom---11

Marry---21

Tom---11

Marry---21

Tom---11

Marry---21

Tom---11

......

Marry---21

Tom---11

Marry---21

Tom---11

Marry---21

Tom---11

Marry---21  

 

那么这便是我们期待的结果,交替出现。

 

 

死锁:

①、多线程通信的时候,很容易造成死锁,死锁无法解决,只能避免

②、当 A 线程等待由 B 线程持有的锁,而 B 线程正在等待由 A 线程持有的锁时发生死锁现象(比如A拿着铅笔,B拿着圆珠笔,A说你先给我圆珠笔,我就把铅笔给你,而B说你先给我铅笔,我就把圆珠笔给你,这就造成了死锁,A和B永远不能进行交换)

③、JVM 既不检测也不避免这种现象,所以程序员必须保证不能出现这样的情况

 

Thread 类中容易造成死锁的方法(这两个方法都已经过时了,不建议使用):

suspend():使正在运行的线程放弃 CPU,暂停运行(不释放锁)

resume():使暂停的线程恢复运行

情景:A 线程获得对象锁,正在执行一个同步方法,如果 B线程调用 A 线程的 suspend() 方法,此时A 暂停运行,放弃 CPU 资源,但是不放弃同步锁,那么B也不能获得锁,A又暂停,那么便造成死锁。

 

解决死锁法则:当多个线程需要访问 共同的资源A,B,C时,必须保证每一个线程按照一定的顺序去访问,比如都先访问A,然后B,最后C。就像我们这里的生产者---消费者模型,制定了必须生产者先生产一个对象,然后消费者去消费,消费完毕,生产者才能在开始生产,然后消费者在消费。这样的顺序便不会造成死锁。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
这个是很经典的问题 实验题目: 生产者与消费者(综合性实验) 实验环境: C语言编译器 实验内容: ① 由用户指定要产生的进程及其类别,存入进入就绪队列。    ② 调度程序从就绪队列中提取一个就绪进程运行。如果申请的资源被阻塞则进入相应的等待队列,调度程序调度就绪队列中的下一个进程。进程运行结束时,会检查对应的等待队列,激活队列中的进程进入就绪队列。运行结束的进程进入over链表。重复这一过程直至就绪队列为空。    ③ 程序询问是否要继续?如果要转直①开始执行,否则退出程序。 实验目的: 通过实验模拟生产者与消费者之间的关系,了解并掌握他们之间的关系及其原理。由此增加对进程同步的问题的了解。 实验要求: 每个进程有一个进程控制块(PCB)表示。进程控制块可以包含如下信息:进程类型标号、进程系统号、进程状态、进程产品(字符)、进程链指针等等。 系统开辟了一个缓冲区,大小由buffersize指定。 程序中有三个链队列,一个链表。一个就绪队列(ready),两个等待队列:生产者等待队列(producer);消费者队列(consumer)。一个链表(over),用于收集已经运行结束的进程 本程序通过函数模拟信号量的操作。 参考书目: 1)徐甲同等编,计算机操作系统教程,西安电子科技大学出版社 2)Andrew S. Tanenbaum著,陈向群,马红兵译. 现代操作系统(第2版). 机械工业出版社 3)Abranham Silberschatz, Peter Baer Galvin, Greg Gagne著. 郑扣根译. 操作系统概念(第2版). 高等教育出版社 4)张尧学编著. 计算机操作系统教程(第2版)习题解答与实验指导. 清华大学出版社 实验报告要求: (1) 每位同学交一份电子版本的实验报告,上传到202.204.125.21服务器中。 (2) 文件名格式为班级、学号加上个人姓名,例如: 电子04-1-040824101**.doc   表示电子04-1班学号为040824101号的**同学的实验报告。 (3) 实验报告内容的开始处要列出实验的目的,实验环境、实验内容等的说明,报告中要附上程序代码,并对实验过程进行说明。 基本数据结构: PCB* readyhead=NULL, * readytail=NULL; // 就绪队列 PCB* consumerhead=NULL, * consumertail=NULL; // 消费者队列 PCB* producerhead=NULL, * producertail=NULL; // 生产者队列 over=(PCB*)malloc(sizeof(PCB)); // over链表 int productnum=0; //产品数量 int full=0, empty=buffersize; // semaphore char buffer[buffersize]; // 缓冲区 int bufferpoint=0; // 缓冲区指针 struct pcb { /* 定义进程控制块PCB */ int flag; // flag=1 denote producer; flag=2 denote consumer; int numlabel; char product; char state; struct pcb * processlink; …… }; processproc( )--- 给PCB分配内存。产生相应的的进程:输入1为生产者进程;输入2为消费者进程,并把这些进程放入就绪队列中。 waitempty( )--- 如果缓冲区满,该进程进入生产者等待队列;linkqueue(exe,&producertail); // 把就绪队列里的进程放入生产者队列的尾部 void signalempty() bool waitfull() void signalfull() void producerrun() void comsuerrun() void main() { processproc(); element=hasElement(readyhead); while(element){ exe=getq(readyhead,&readytail); printf("进程%d申请运行,它是一个",exe->numlabel); exe->flag==1? printf("生产者\n"):printf("消费者\n"); if(exe->flag==1) producerrun(); else comsuerrun(); element=hasElement(readyhead); } printf("就绪队列没有进程\n"); if(hasElement(consumerhead)) { printf("消费者等待队列中有进程:\n"); display(consumerhead); } else { printf("消费者等待队列中没有进程\n"); } if(hasElement(producerhead)) { printf("生产者等待队列中有进程:\n"); display(producerhead); } else { printf("生产者等待队列中没有进程\n"); } }
(1)创建生产者和消费者线程 在Windows2000环境下,创建一个控制台进程,在此进程中创建n个线程来模拟生产者或者消费者。这些线程的信息由本程序定义的“测试用例文件”中予以指定。 该文件的格式和含义如下: 3 1 P 3 2 P 4 3 C 4 1 4 P 2 5 C 3 1 2 4 第一行说明程序中设置几个临界区,其余每行分别描述了一个生产者或者消费者线程的信息。每一行的各字段间用Tab键隔开。不管是消费者还是生产者,都有一个对应的线程号,即每一行开始字段那个整数。第二个字段用字母P或者C区分是生产者还是消费者。第三个字段表示在进入相应线程后,在进行生产和消费动作前的休眠时间,以秒计时;这样做的目的是可以通过调整这一列参数,控制开始进行生产和消费动作的时间。如果是代表生产者,则该行只有三个字段。如果代表消费者,则该行后边还有若干字段,代表要求消费的产品所对应的生产者的线程号。所以务必确认这些对应的线程号存在并且该线程代表一个生产者。 (2)生产和消费的规则 在按照上述要求创建线程进行相应的读写操作时,还需要符合以下要求: ①共享缓冲区存在空闲空间时,生产者即可使用共享缓冲区。 ②从上边的测试数据文件例子可以看出,某一生产者生产一个产品后,可能不止一个消费者,或者一个消费者多次地请求消费该产品。此时,只有当所有的消费需求都被满足以后,该产品所在的共享缓冲区才可以被释放,并作为空闲空间允许新的生产者使用。 ③每个消费者线程的各个消费需求之间存在先后顺序。例如上述测试用例文件包含一行信息“5 C 3 l 2 4”,可知这代表一个消费者线程,该线程请求消费1,2,4号生产者线程生产的产品。而这种消费是有严格顺序的,消费1号线程产品的请求得到满足后才能继续往下请求2号生产者线程的产品。 ④要求在每个线程发出读写操作申请、开始读写操作和结束读写操作时分别显示提示信息。 (3)相关基础知识 本实验所使用的生产者和消费者模型具有如下特点: 本实验的多个缓冲区不是环形循环的,也不要求按顺序访问。生产者可以把产品放到目前某一个空缓冲区中。 消费者只消费指定生产者的产品。 在测试用例文件中指定了所有的生产和消费的需求,只有当共享缓冲区的数据满足了所有关于它的消费需求后,此共享缓冲区才可以作为空闲空间允许新的生产者使用。 本实验在为生产者分配缓冲区时各生产者间必须互斥,此后各个生产者的具体生产活动可以并发。而消费者之间只有在对同一产品进行消费时才需要互斥,同时它们在消费过程结束时需要判断该消费对象是否已经消费完毕并清除该产品。 Windows用来实现同步和互斥的实体。在Windows中,常见的同步对象有:信号量(Semaphore)、互斥量(Mutex)、临界段(CriticalSection)等。使用这些对象都分为三个步骤,一是创建或者初始化:接着请求该同步对象,随即进入临界区,这一步对应于互斥量的上锁;最后释放该同步对象,这对应于互斥量的解锁。这些同步对象在一个线程中创建,在其他线程中都可以使用,从而实现同步互斥。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值