Java线程同步问题--哲学家就餐

目录
  • 1.场景
  • 2.解决方案
    • 方法一:限制吃饭的哲学家人数
    • 方法二:找到一个左撇子哲学家

1.场景

有五位沉默的哲学家围坐在一张圆桌旁,他们一生都在吃东西和思考。

有五只筷子供他们使用,哲学家需要双手拿到一双筷子之后才能吃饭;吃完后会将筷子放下继续思考。

那么现在有一个问题,我们需要想出一种方案,如何保证哲学家们可以交替吃饭和思考,而不会被饿死。

上面这个问题是由Dijkstra提出的一个经典的线程同步问题。

2.解决方案

我们在开始想如何解决问题之前,可以先将这个场景通过代码还原,在程序中进行建模。

每一只筷子可以看做是一个资源数据,都可以被它两边的哲学家尝试去获取,并且同一时间只能由其中一人持有,这可以通过我们JUC包中的信号量Semaphore来表示。

然后,每个哲学家可以看做是一个线程,每个线程中的run方法内容都是先进行思考,然后试图获取左右两边的筷子吃饭,吃完饭后继续思考。

通过上面的分析,我们的代码实现如下:

/**
 * @author 小黑说Java
 * @ClassName DiningPhilosophers
 * @Description 哲学家就餐问题
 * @date 2022/2/6
 **/
@Slf4j
public class DiningPhilosophers implements Runnable {

    private final int id;

    public DiningPhilosophers(int id) {
        this.id = id;
    }

    private static final Random random = new Random(System.currentTimeMillis());

    private static final Semaphore[] forks = new Semaphore[5];

    // 初始化信号量,每个信号量为1,代表1只筷子
    static {
        forks[0] = new Semaphore(1);
        forks[1] = new Semaphore(1);
        forks[2] = new Semaphore(1);
        forks[3] = new Semaphore(1);
        forks[4] = new Semaphore(1);
    }

    @Override
    public void run() {
        try {
            while (true) {
                think();
                eat(id);
            }
        } catch (InterruptedException e) {
            log.error("异常中断", e);
        }
    }

    /**
     * 哲学家思考随机时间
     */
    private void think() throws InterruptedException {
        TimeUnit.MILLISECONDS.sleep(random.nextInt(100));
    }

    private void eat(int id) {
        // TODO
    }

}

接下来,我们思考一下,如何实现哲学家吃饭的逻辑。

当一个哲学家需要吃饭时,他要拿起左右两边的筷子。

所以:

  • 哲学家 A(0) 需要筷子0 和 4
  • 哲学家 B(1) 需要筷子 1 和 0
  • 哲学家 C(2) 需要筷子 2 和 1
  • 哲学家 D(3) 需要筷子 3 和 2
  • 哲学家 E(4) 需要筷子 4 和 3

所以每个哲学家线程都应该有个编号,所以我在DiningPhilosophers中定义了属性id表示哲学家的编号。

在吃饭方法中,需要根据id来决定获取哪只筷子。

左手边的筷子可以有用fork[id]表示;

右手边的筷子用fork[(id+4)%5]表示。

那么我们的eat方法的实现如下:

private void eat(int id) throws InterruptedException {
    // 先拿左边的筷子
    forks[id].acquire();

    // 然后拿右边的筷子
    forks[(id + 4) % 5].acquire();

    // 吃一口饭
    log.info("哲学家{}正在吃饭~", id);

    // 依次放下左边的筷子和右边的筷子
    forks[id].release();
    forks[(id + 4) % 5].release();
}

我们接着来测试我们的完整代码。

/**
 * @author 小黑说Java
 * @ClassName DiningPhilosophers
 * @Description 哲学家就餐问题
 * @date 2022/2/6
 **/
@Slf4j
public class DiningPhilosophers implements Runnable {

    private final int id;

    public DiningPhilosophers(int id) {
        this.id = id;
    }

    private static final Random random = new Random(System.currentTimeMillis());

    private static final Semaphore[] forks = new Semaphore[5];

    // 初始化信号量,每个信号量为1,代表1只筷子
    static {
        forks[0] = new Semaphore(1);
        forks[1] = new Semaphore(1);
        forks[2] = new Semaphore(1);
        forks[3] = new Semaphore(1);
        forks[4] = new Semaphore(1);
    }

    @Override
    public void run() {
        try {
            while (true) {
                think();
                eat(id);
            }
        } catch (InterruptedException e) {
            log.error("异常中断", e);
        }
    }

    /**
     * 哲学家思考随机时间
     */
    private void think() throws InterruptedException {
        TimeUnit.MILLISECONDS.sleep(random.nextInt(100));
    }

    private void eat(int id) throws InterruptedException {
        // 先拿左边的筷子
        forks[id].acquire();

        // 然后拿右边的筷子
        forks[(id + 4) % 5].acquire();

        // 吃一口饭
        log.info("哲学家{}正在吃饭~", id);

        // 依次放下左边的筷子和右边的筷子
        forks[id].release();
        forks[(id + 4) % 5].release();
    }

    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new Thread(new DiningPhilosophers(i)).start();
        }
    }
}

运行上面的代码后,会发现程序在运行一段时间后会进入死锁状态。

这种情况是因为,在某一时刻,所有的哲学家都获取到了左手边的筷子,而无法获取到右手边的筷子,导致没有人可以到东西,陷入僵局。

该如何避免出现这种死锁问题呢?

方法一:限制吃饭的哲学家人数

很简单的一种方法,就是在一个时间点,只能有最多4个哲学家开始吃饭。4个哲学家分5只筷子,则永远不会发生死锁。

要实现这种方法,我们可以再定义一个许可数为4的信号量Semaphore,表示剩余可以吃饭的哲学家名额。

代码实现如下:

/**
 * @author 小黑说Java
 * @ClassName DiningPhilosophers
 * @Description 哲学家就餐问题
 * @date 2022/2/6
 **/
@Slf4j
public class DiningPhilosophers implements Runnable {

    private final int id;

    public DiningPhilosophers(int id) {
        this.id = id;
    }

    private static final Random random = new Random(System.currentTimeMillis());

    private static final Semaphore[] forks = new Semaphore[5];

    private static final Semaphore maxDiners = new Semaphore(4);

    // 初始化信号量,每个信号量为1,代表1只筷子
    static {
        forks[0] = new Semaphore(1);
        forks[1] = new Semaphore(1);
        forks[2] = new Semaphore(1);
        forks[3] = new Semaphore(1);
        forks[4] = new Semaphore(1);
    }

    @Override
    public void run() {
        try {
            while (true) {
                think();
                eat(id);
            }
        } catch (InterruptedException e) {
            log.error("异常中断", e);
        }
    }

    /**
     * 哲学家思考随机时间
     */
    private void think() throws InterruptedException {
        TimeUnit.MILLISECONDS.sleep(random.nextInt(100));
    }

    private void eat(int id) throws InterruptedException {
        // 先获得吃饭名额
        maxDiners.acquire();

        // 先拿左边的筷子
        forks[id].acquire();

        // 然后拿右边的筷子
        forks[(id + 4) % 5].acquire();

        // 吃一口饭
        log.info("哲学家{}正在吃饭~", id);

        // 依次放下左边的筷子和右边的筷子
        forks[id].release();
        forks[(id + 4) % 5].release();

        // 吃完之后归还吃饭名额
        maxDiners.release();
    }

    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new Thread(new DiningPhilosophers(i)).start();
        }
    }
}

方法二:找到一个左撇子哲学家

这种方法是让其中一个哲学家和其他哲学家拿筷子的顺序和其他哲学家不一样。

比如其他人都是先拿右手边再拿左手边,而这个左撇子哲学家则先拿左手边再拿右手边。

而哪一位哲学家被选为左撇子并不重要,因为桌子是圆的,所以我们就选择0号哲学家为左撇子。

代码实现如下:

/**
 * @ClassName DiningPhilosophers
 * @Description 哲学家就餐问题
 * @date 2022/2/6
 **/
@Slf4j
public class DiningPhilosophers implements Runnable {

    private final int id;

    public DiningPhilosophers(int id) {
        this.id = id;
    }

    private static final Random random = new Random(System.currentTimeMillis());

    private static final Semaphore[] forks = new Semaphore[5];

    // 初始化信号量,每个信号量为1,代表1只筷子
    static {
        forks[0] = new Semaphore(1);
        forks[1] = new Semaphore(1);
        forks[2] = new Semaphore(1);
        forks[3] = new Semaphore(1);
        forks[4] = new Semaphore(1);
    }

    @Override
    public void run() {
        try {
            while (true) {
                think();
                eat(id);
            }
        } catch (InterruptedException e) {
            log.error("异常中断", e);
        }
    }

    /**
     * 哲学家思考随机时间
     */
    private void think() throws InterruptedException {
        TimeUnit.MILLISECONDS.sleep(random.nextInt(100));
    }

    private void eat(int id) throws InterruptedException {
        if (id == 0) {
            hanleLeftFirst(id);
        } else {
            hanldRightFirst(id);
        }
        // 吃一口饭
        log.info("哲学家{}正在吃饭~", id);
        forks[id].release();
        forks[(id + 4) % 5].release();
    }

    private void hanleLeftFirst(int id) throws InterruptedException {
        forks[id].acquire();
        forks[(id + 4) % 5].acquire();
    }

    private void hanldRightFirst(int id) throws InterruptedException {
        forks[(id + 4) % 5].acquire();
        forks[id].acquire();
    }

    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new Thread(new DiningPhilosophers(i)).start();
        }
    }
}

到此这篇关于Java线程同步问题的文章就介绍到这了,更多相关Java线程同步内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Java并发编程之详解CyclicBarrier线程同步

    CyclicBarrier线程同步 java.util.concurrent.CyclicBarrier提供了一种多线程彼此等待的同步机制,可以把它理解成一个障碍,所有先到达这个障碍的线程都将将处于等待状态,直到所有线程都到达这个障碍处,所有线程才能继续执行. 举个例子:CyclicBarrier的同步方式有点像朋友们约好了去旅游,在景点入口处集合,这个景点入口就是一个Barrier障碍,等待大家都到了才一起进入景点游览参观. 进入景点后大家去爬山,有的人爬得快,有的人爬的慢,大家约好了山顶集合

  • Java 多线程的同步代码块详解

    目录 synchronized 同步代码块 同步方法(this锁) 静态同步方法 死锁问题 lock 总结 火车站抢票问题 由于现实中买票也不会是零延迟的,为了真实性加入了延迟机制,也就是线程休眠语句 package test.MyThread.ticketDemo; public class RunnableThread implements Runnable{ private int ticket = 100; @Override public void run(){ while(true)

  • java并发编程JUC CountDownLatch线程同步

    目录 java并发编程JUC CountDownLatch线程同步 1.CountDownLatch是什么? 2.CountDownLatch 如何工作 3.CountDownLatch 代码例子 java并发编程JUC CountDownLatch线程同步 CountDownLatch是一种线程同步辅助工具,它允许一个或多个线程等待其他线程正在执行的一组操作完成.CountDownLatch的概念在java并发编程中非常常见,面试也会经常被问到,所以一定要好好理解掌握. CountDownLa

  • Java多线程之同步工具类CountDownLatch

    前言: CountDownLatch是一个同步工具类,它允许一个或多个线程一直等待,直到其他线程执行完后再执行.例如,应用程序的主线程希望在负责启动框架服务的线程已经启动所有框架服务之后执行. 1 CountDownLatch主要方法 void await():如果当前count大于0,当前线程将会wait,直到count等于0或者中断. PS:当count等于0的时候,再去调用await() , 线程将不会阻塞,而是立即运行.后面可以通过源码分析得到. boolean await(long t

  • Java多线程之同步工具类CyclicBarrier

    目录 1 CyclicBarrier方法说明 2 CyclicBarrier实例 3 CyclicBarrier源码解析 CyclicBarrier构造函数 await方法 nextGeneration的源码 breakBarrier源码 isBroken方法 reset方法 getNumberWaiting方法 前言: CyclicBarrier是一个同步工具类,它允许一组线程互相等待,直到达到某个公共屏障点.与CountDownLatch不同的是该barrier在释放线程等待后可以重用,所以

  • Java多线程之同步工具类Exchanger

    目录 1 Exchanger 介绍 2 Exchanger 实例 exchange等待超时 3 实现原理 1 Exchanger 介绍 前面分别介绍了CyclicBarrier.CountDownLatch.Semaphore,现在介绍并发工具类中的最后一个Exchange. Exchanger 是一个用于线程间协作的工具类,Exchanger用于进行线程间的数据交换,它提供一个同步点,在这个同步点,两个线程可以交换彼此的数据.这两个线程通过exchange 方法交换数据,如果第一个线程先执行e

  • Java线程同步问题--哲学家就餐

    目录 1.场景 2.解决方案 方法一:限制吃饭的哲学家人数 方法二:找到一个左撇子哲学家 1.场景 有五位沉默的哲学家围坐在一张圆桌旁,他们一生都在吃东西和思考. 有五只筷子供他们使用,哲学家需要双手拿到一双筷子之后才能吃饭:吃完后会将筷子放下继续思考. 那么现在有一个问题,我们需要想出一种方案,如何保证哲学家们可以交替吃饭和思考,而不会被饿死. 上面这个问题是由Dijkstra提出的一个经典的线程同步问题. 2.解决方案 我们在开始想如何解决问题之前,可以先将这个场景通过代码还原,在程序中进行

  • Java线程同步实例分析

    本文实例讲述了Java线程同步的用法.分享给大家供大家参考.具体分析如下: 多线程的使用为我们的程序提供了众多的方便,同时它也给我们带来了以往没有考虑过的麻烦.当我们使用多线程处理共享资源时意外将会发生:比如我们一起外出就餐,每个人都是一个线程,餐桌上的食物则是共享资源,当我看到红烧鸡腿上桌后立即拿起筷子直奔目标,眼看着就得手的时候,突然---鸡腿消失了,一个距离盘子更近的线程正在得意地啃着. 为了避免上述问题的发生,Java为我们提供了"synchronized(同步化)修饰符"来避

  • java 线程同步详细介绍及实例代码

    java 线程同步 概要: 为了加快代码的运行速度,我们采用了多线程的方法.并行的执行确实让代码变得更加高效,但随之而来的问题是,有很多个线程在程序中同时运行,如果它们同时的去修改一个对象,很可能会造成讹误的情况,这个时候我们需要用一种同步的机制来管理这些线程. (一)竞争条件 记得操作系统中,让我印象很深的有一张图.上面画的是一块块进程,在这些进程里面分了几个线程,所有这些线程齐刷刷统一的指向进程的资源.Java中也是如此,资源会在线程间共享而不是每个线程都有一份独立的资源.在这种共享的情况下

  • Java 线程同步详解

    Java 线程同步根本上是要符合一个逻辑:加锁------>修改------>释放锁 1.同步代码块 示例如下: public class SyncBlock { static class DataWrap { int i; } static class SyncBlockThread extends Thread { private DataWrap date; public SyncBlockThread(DataWrap dataWrap) { this.date = dataWrap;

  • Java线程同步机制_动力节点Java学院整理

    在之前,已经学习到了线程的创建和状态控制,但是每个线程之间几乎都没有什么太大的联系.可是有的时候,可能存在多个线程多同一个数据进行操作,这样,可能就会引用各种奇怪的问题.现在就来学习多线程对数据访问的控制吧. 由于同一进程的多个线程共享同一片存储空间,在带来方便的同时,也带来了访问冲突这个严重的问题.Java语言提供了专门机制以解决这种冲突,有效避免了同一个数据对象被多个线程同时访问. 一.多线程引起的数据访问安全问题 下面看一个经典的问题,银行取钱的问题: 1).你有一张银行卡,里面有5000

  • Java线程同步Lock同步锁代码示例

    java线程同步原理 java会为每个object对象分配一个monitor,当某个对象的同步方法(synchronizedmethods)被多个线程调用时,该对象的monitor将负责处理这些访问的并发独占要求. 当一个线程调用一个对象的同步方法时,JVM会检查该对象的monitor.如果monitor没有被占用,那么这个线程就得到了monitor的占有权,可以继续执行该对象的同步方法:如果monitor被其他线程所占用,那么该线程将被挂起,直到monitor被释放. 当线程退出同步方法调用时

  • Java线程同步、同步方法实例详解

    线程的同步是保证多线程安全访问竞争资源的一种手段. 线程的同步是Java多线程编程的难点,往往开发者搞不清楚什么是竞争资源.什么时候需要考虑同步,怎么同步等等问题,当然,这些问题没有很明确的答案,但有些原则问题需要考虑,是否有竞争资源被同时改动的问题? 对于同步,在具体的Java代码中需要完成一下两个操作: 把竞争访问的资源标识为private: 同步哪些修改变量的代码,使用synchronized关键字同步方法或代码. 当然这不是唯一控制并发安全的途径. synchronized关键字使用说明

  • JAVA线程同步实例教程

    线程是Java程序设计里非常重要的概念,本文就以实例形式对此加以详细解读.具体分析如下: 首先,线程加锁有什么用处呢?举个例子:比如你现在有30000块大洋在银行存着,现在你到银行取钱,当你输入密码完成后,已经输入取款金额,比如你输入的是20000,就是在银行给你拿钱这个时刻,你老婆也去银行取这笔钱,你老婆同样取20000,因为此时你的账上仍然是30000,所以银行同样的操作在你老婆那端又进行了一遍,这样当你们两个完成各自操作后,银行记录的你账上还应该有10000块存款,这样是不是很爽.解决这个

  • 解析Java线程同步锁的选择方法

    在需要线程同步的时候如何选择合适的线程锁?例:选择可以存入到常量池当中的对象,String对象等 复制代码 代码如下: public class SyncTest{    private String name = "name";public void method(String flag)    {        synchronized (name)        {            System.out.println(flag + ", invoke metho

  • 深入解析Java的线程同步以及线程间通信

    Java线程同步 当两个或两个以上的线程需要共享资源,它们需要某种方法来确定资源在某一刻仅被一个线程占用.达到此目的的过程叫做同步(synchronization).像你所看到的,Java为此提供了独特的,语言水平上的支持. 同步的关键是管程(也叫信号量semaphore)的概念.管程是一个互斥独占锁定的对象,或称互斥体(mutex).在给定的时间,仅有一个线程可以获得管程.当一个线程需要锁定,它必须进入管程.所有其他的试图进入已经锁定的管程的线程必须挂起直到第一个线程退出管程.这些其他的线程被

随机推荐