Java多线程:哲学家就餐问题和生产者消费者问题

在Java多线程中有两个经典问题被提及最多,一个关于死锁的哲学家就餐问题还有一个就是关于多个线程协作并发的生产者消费者问题

哲学家就餐问题

哲学家就餐问题是1965年由Dijkstra提出的一种线程同步问题。

问题描述:一圆桌前坐着几名哲学家,两个人中间有一只筷子,桌子中央有食物。哲学家可以进行思考和吃饭,思考时,不获取筷子。吃饭时,必须同时获得左右两只筷子才能吃饭(这里我们规定先获得右边的,再获得左边的)。当所有哲学家都拿着自己右手边的筷子,准备去拿左手边的筷子时发现右边的筷子都在右边的哲学家手中,而所有哲学家都不愿放弃自己手中的筷子,这就导致所有的哲学家都不能吃饭,僵持在这样一个状态。

而我们在使用java中的sychronized或者显示锁来进行互斥操作时,就可能会出现跟哲学家一样的情况:即任务一获得A资源,等待B资源。任务二获得B资源等待C资源。任务三获得C资源等待D资源。而任务四获得D资源等待A资源,这样就造成一个连续的循环等待,没有哪个线程能够继续下去,称为死锁。 

我们用代码来描述

 1 /**
 2  * 满足:
 3  * 每根筷子同时只能被一个哲学家获得,若有另外一个哲学家请求获得该筷子,则需要等待
 4  * 哲学家使用完筷子之后就放回并通知其他哲学家使用
 5  *  6  *
 7  */
 8 public class Chopstick {
 9     private int index;
10     private boolean use = false;
11 
12     public Chopstick(int index) {
13      14         this.index = index;
15     }
16 
17     @Override
18     public String toString() {
19         return "Chopstick [index=" + index + "]";
20     }
21 
22     /*
23      * 获得筷子,先判断当前筷子是否被占用
24      * 该筷子被获得之后,当有其他哲学家线程来请求获得时,都需要等待
25      */
26     public synchronized void take() throws InterruptedException{
27         while(use)
28            wait();
29         use =true;
30     }
31 
32     /*
33      * 归还筷子,更新状态
34      * 当持有该筷子的哲学家使用完毕之后,就将其归还,并通知其他在等待该筷子资源的哲学家
35      */
36     public synchronized void drop(){
37         use = false;
38         notifyAll();
39     }
40 }

Philosopher类: 
哲学家先获得右边的筷子,再获得左边的筷子。在获得左边的筷子时,若左边的筷子已经被相邻哲学家获得,则需要等待直到其释放该资源为止,即left.take()会阻塞,直到被通知。

 3 /**
 4  * 每个哲学家可以进行思考或者吃饭,吃饭时需要先后获得右边和左边的筷子
 5  * 若没有同时获得右边和左边的筷子,则等待,
 6  * 若使用完之后就返回。
 7  */
 8 public class Philosopher implements Runnable{
 9     private Chopstick right ; 
10     private Chopstick left;
11     private int index;
12     private int thinkTime;
13     public Philosopher(Chopstick right, Chopstick left, int index, int thinkingTime) {
14         super();
15         this.right = right;
16         this.left = left;
17         this.index = index;
18         this.thinkTime = thinkingTime;
19     }
20 
21     @Override
22     public void run() {
23         try {
24             while (!Thread.interrupted()) {
25                 System.out.println(this + " thinking .......");
26                 thinking();
27                 System.out.println(this + " start to eat and take right stick");
28                 right.take();
29                 System.out.println(this + " take left stick");
30                 left.take();
31                 System.out.println(this + " eating");
32                 thinking();//吃饭
33                 right.drop();
34                 left.drop();
35             }
36         } catch (InterruptedException e) {
37             System.out.println(this+"InterruptedException");
38         }
39     }
40 
41     /**
42      * 哲学家思考时间,由thinkingTime因子决定
43      * @throws InterruptedException
44      */
45     private void thinking() throws InterruptedException{
46         Thread.sleep(thinkTime*100);
47     }
48 
49     @Override
50     public String toString() {
51         return "Philosopher [index=" + index + "]";
52     }
53 }

产生死锁的版本: 
测试类中有5个哲学家和5只筷子,为求方便将思考时间定为0s,就会很快出现死锁的情况。

 7 /**
 8  * 会产生死锁的版本
 9  * 5个哲学家,5只筷子,每个哲学家吃饭之前需要先拿到右边的筷子,然后再拿到左边的筷子
10  * 之后才能吃饭
11  * @author lecky
12  *
13  */
14 public class DeadlockPhilosopher {
15 
16     @Test
17     public void test() throws InterruptedException {
18         ExecutorService executor = Executors.newCachedThreadPool();
19         int size=5;
20         int thinkingTime=0;
21         Chopstick[] chopstick = new Chopstick[size];
22         for(int i=0;i<size;i++)
23             chopstick[i] = new Chopstick(i);
24         for(int i=0;i<size;i++)
25             executor.execute(new Philosopher(chopstick[i], chopstick[(i+1)%size], i, thinkingTime));
26         Thread.sleep(4*1000);
27         executor.shutdownNow();
28     }
29 }

运行结果: 
分析运行结果会发现, 
Philosopher [index=0]获得right stick[0],请求left stick 
Philosopher [index=1]获得right stick[1],请求left stick 
Philosopher [index=2]获得right stick[2],请求left stick 
Philosopher [index=3]获得right stick[3],请求left stick 
Philosopher [index=4]获得right stick[4],请求left stick 
而没有一个能够同时获得两只筷子吃饭的哲学家。

打印结果为

 1 Philosopher [index=0] thinking .......
 2 Philosopher [index=3] thinking .......
 3 Philosopher [index=4] thinking .......
 4 Philosopher [index=1] thinking .......
 5 Philosopher [index=4] start to eat and take right stick
 6 Philosopher [index=3] start to eat and take right stick
 7 Philosopher [index=2] thinking .......
 8 Philosopher [index=0] start to eat and take right stick
 9 Philosopher [index=2] start to eat and take right stick
10 Philosopher [index=3] take left stick
11 Philosopher [index=4] take left stick
12 Philosopher [index=1] start to eat and take right stick
13 Philosopher [index=2] take left stick
14 Philosopher [index=0] take left stick
15 Philosopher [index=1] take left stick
16 Philosopher [index=1]InterruptedException
17 Philosopher [index=0]InterruptedException
18 Philosopher [index=2]InterruptedException
19 Philosopher [index=3]InterruptedException
20 Philosopher [index=4]InterruptedException

在本问题中,同时满足以下四个条件时,就会发生死锁

1. 互斥条件:线程使用的资源中至少有一个是不能共享的。这里,一根筷子一次就只能被一个哲学家使用。

2.至少有一个线程持有一个资源,并且它在等待获取一个当前被别的线程持有的资源。也就是说,要放生死锁,哲学家必须拿着一根筷子并且等待另一根。

3. 资源不能被进程(线程)抢占。所有的进程必须把资源释放作为普通条件.哲学家很有礼貌,不会从其他哲学家那里抢筷子。

4. 必须有循环等待,这时,一个进程等待其他进程持有的资源,后者又在等待另外一个进程持有的资源,这样一直下去,直到有一个进程在等待第一个进程(线程)持有的资源,使得大家都被锁住。在这里,以为每个哲学家都试图先得到左边的筷子,然后得到右边的筷子,所以发生了循环等待。

在下面的例子中,针对最后一个哲学家,通过交换构造器中的初始化顺序,打破了死锁条件,使得最后一个哲学家先拿右边的筷子,再拿左边的筷子。这样将使得不再出现死锁。

1 for(int i=0;i<size-1;i++)
2       executor.execute(new Philosopher(chopsticks[i], chopsticks[i+1], i, thinkingTime));
3  executor.execute(new Philosopher(chopsticks[0], chopsticks[size-1], size, thinkingTime));//更改第五个哲学家获得筷子的顺序

生产者消费者问题

生产者-消费者(producer-consumer)问题,也称作有界缓冲区(bounded-buffer)问题,两个进程共享一个公共的固定大小的缓冲区。其中一个是生产者,用于将消息放入缓冲区;另外一个是消费者,用于从缓冲区中取出消息。问题出现在当缓冲区已经满了,而此时生产者还想向其中放入一个新的数据项的情形,其解决方法是让生产者此时进行休眠,等待消费者从缓冲区中取走了一个或者多个数据后再去唤醒它。同样地,当缓冲区已经空了,而消费者还想去取消息,此时也可以让消费者进行休眠,等待生产者放入一个或者多个数据时再唤醒它。 

对于此模型,应该明确一下几点:
1、生产者仅仅在仓储未满时候生产,仓满则停止生产。
2、消费者仅仅在仓储有产品时候才能消费,仓空则等待。
3、当消费者发现仓储没产品可消费时候会通知生产者生产。
4、生产者在生产出可消费产品时候,应该通知等待的消费者去消费。

涉及问题

  • 同步问题:如何保证同一资源被多个线程并发访问时的完整性。常用的同步方法是采用标记或加锁机制
  • wait() / nofity() 方法是基类Object的两个方法,也就意味着所有Java类都会拥有这两个方法,这样,我们就可以为任何对象实现同步机制。
  • wait()方法:当缓冲区已满/空时,生产者/消费者线程停止自己的执行,放弃锁,使自己处于等等状态,让其他线程执行。
  • notify()方法:当生产者/消费者向缓冲区放入/取出一个产品时,向其他等待的线程发出可执行的通知,同时放弃锁,使自己处于等待状态。

公共资源类

//公共资源类
public class PublicResource
{
    //公共资源
    private int resource;

    public synchronized void increase()
    { 
        try
        {
            while(resource != 0)
            {
                wait();
            }
        }
        catch(Exception e)
        {
            e.printStackTrace();
        }
        resource ++;
        System.out.println(resource);
        notify();
    }

    public synchronized void decrease()
    {
        try
        {
            while(resource != 1)
            {
                wait();
            }
        }
        catch(Exception e)
        {
            e.printStackTrace();
        }
        resource --;
        System.out.println(resource);
        notify();
    }
}

生产者

 1 //生产者线程类用于生产者线程的产生
 2 public class ProducerThread implements Runnable
 3 {
 4     public PublicResource resource;
 5     public Producer(PublicResource resource)
 6     {
 7         this.resource = resource;
 8     }
 9 
10     @Override
11     public void run()
12     {
13         for(int i = 0; i < 10; i++)
14         {
15             try
16             {
17                 Thread.sleep((long)(Math.random()*1000));
18             }
19             catch(Exception e)
20             {
21                 e.printStackTrace();
22             }
23             resource.increase();
24         }
25     }
26 }

消费者

 1 //消费者线程类用于消费者线程的产生
 2 public class ConsumerThread implements Runnable
 3 {
 4     public PublicResource resource;
 5     public Producer(PublicResource resource)
 6     {
 7         this.resource = resource;
 8     }
 9 
10     @Override
11     public void run()
12     {
13         for(int i = 0; i < 10; i++)
14         {
15             try
16             {
17                 Thread.sleep((long)(Math.random()*1000));
18             }
19             catch(Exception e)
20             {
21                 e.printStackTrace();
22             }
23             resource.decrease();
24         }
25     }
26 }

测试类

 1 public class ProducerConsumerTest {    
 2     public static void main(String[] args) {    
 3         PublicResource resource = new PublicResource();    
 4         new Thread(new ProducerThread(resource)).start();    
 5         new Thread(new ConsumerThread(resource)).start();    
 6         new Thread(new ProducerThread(resource)).start();    
 7         new Thread(new ConsumerThread(resource)).start();    
 8         new Thread(new ProducerThread(resource)).start();    
 9         new Thread(new ConsumerThread(resource)).start();    
10     }    
11 }   
原文地址:https://www.cnblogs.com/2015110615L/p/6735394.html