Обеденная философская проблема на Яве

Обеденная философская проблема на Яве

1. Вступление

Задача «Обеденные философы» - одна из классических задач, используемых дляdescribe synchronization issues in a multi-threaded environment and illustrate techniques for solving them. Дейкстра впервые сформулировал эту проблему и представил ее в отношении компьютеров, имеющих доступ к периферийным устройствам ленточных накопителей.

Настоящая формулировка была дана Тони Хоаром, который также известен как изобретатель алгоритма быстрой сортировки. В этой статье мы анализируем эту известную проблему и кодируем популярное решение.

2. Эта проблема

image

Диаграмма выше представляет проблему. Вокруг круглого стола сидят пять безмолвных философов (P1 - P5), которые всю жизнь едят и думают.

Для них есть пять вилок (1 - 5), и для того, чтобы они могли есть, философ должен иметь вилы в обеих руках. После еды он кладет их обоих, и тогда их может выбрать другой философ, который повторяет тот же цикл.

Цель состоит в том, чтобы придумать схему / протокол, который поможет философам достичь своей цели - есть и думать, не голодая до смерти.

3. Решение

Первоначальным решением было бы заставить каждого из философов следовать следующему протоколу:

while(true) {
    // Initially, thinking about life, universe, and everything
    think();

    // Take a break from thinking, hungry now
    pick_up_left_fork();
    pick_up_right_fork();
    eat();
    put_down_right_fork();
    put_down_left_fork();

    // Not hungry anymore. Back to thinking!
}

Как описывает приведенный выше псевдокод, каждый философ изначально думает. After a certain amount of time, the philosopher gets hungry and wishes to eat.с

В этот моментhe reaches for the forks on his either side and once he’s got both of them, proceeds to eat. Когда еда закончена, философ кладет вилки, чтобы они были доступны его соседу.

4. Реализация

Мы моделируем каждого из наших философов как классы, реализующие интерфейсRunnable, чтобы мы могли запускать их как отдельные потоки. У каждогоPhilosopher есть доступ к двум вилкам слева и справа:

public class Philosopher implements Runnable {

    // The forks on either side of this Philosopher
    private Object leftFork;
    private Object rightFork;

    public Philosopher(Object leftFork, Object rightFork) {
        this.leftFork = leftFork;
        this.rightFork = rightFork;
    }

    @Override
    public void run() {
        // Yet to populate this method
    }

}

У нас также есть метод, который инструктируетPhilosopher выполнить действие - поесть, подумать или приобрести вилки для подготовки к еде:

public class Philosopher implements Runnable {

    // Member variables, standard constructor

    private void doAction(String action) throws InterruptedException {
        System.out.println(
          Thread.currentThread().getName() + " " + action);
        Thread.sleep(((int) (Math.random() * 100)));
    }

    // Rest of the methods written earlier
}

Как показано в приведенном выше коде, каждое действие моделируется путем приостановки вызывающего потока на случайное время, так что порядок выполнения не определяется только временем.

Теперь давайте реализуем основную логикуPhilosopher.

Чтобы смоделировать получение вилки, нам нужно заблокировать ее, чтобы никакие два потокаPhilosopher не захватили ее одновременно. Для достижения этого мы используем

Для этого мы используем ключевое словоsynchronized, чтобы получить внутренний монитор объекта fork и предотвратить то же самое другими потоками. Руководство по ключевому словуsynchronized в Java можно найтиhere. Теперь приступим к реализации методаrun() в классеPhilosopher:

public class Philosopher implements Runnable {

   // Member variables, methods defined earlier

    @Override
    public void run() {
        try {
            while (true) {

                // thinking
                doAction(System.nanoTime() + ": Thinking");
                synchronized (leftFork) {
                    doAction(
                      System.nanoTime()
                        + ": Picked up left fork");
                    synchronized (rightFork) {
                        // eating
                        doAction(
                          System.nanoTime()
                            + ": Picked up right fork - eating");

                        doAction(
                          System.nanoTime()
                            + ": Put down right fork");
                    }

                    // Back to thinking
                    doAction(
                      System.nanoTime()
                        + ": Put down left fork. Back to thinking");
                }
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return;
        }
    }
}

Эта схема в точности реализует описанную ранее: aPhilosopher немного думает, а затем решает поесть.

После этого он приобретает вилки слева и справа и начинает есть. Закончив, он кладет вилки вниз. Мы также добавляем временные метки к каждому действию, что поможет нам понять порядок, в котором происходят события.

Чтобы запустить весь процесс, мы пишем клиента, который создает 5Philosophers в виде потоков и запускает их все:

public class DiningPhilosophers {

    public static void main(String[] args) throws Exception {

        Philosopher[] philosophers = new Philosopher[5];
        Object[] forks = new Object[philosophers.length];

        for (int i = 0; i < forks.length; i++) {
            forks[i] = new Object();
        }

        for (int i = 0; i < philosophers.length; i++) {
            Object leftFork = forks[i];
            Object rightFork = forks[(i + 1) % forks.length];

            philosophers[i] = new Philosopher(leftFork, rightFork);

            Thread t
              = new Thread(philosophers[i], "Philosopher " + (i + 1));
            t.start();
        }
    }
}

Мы моделируем каждую из вилок как общие объекты Java и создаем столько их, сколько есть философов. Мы передаем каждомуPhilosopher его левую и правую вилки, которые он пытается заблокировать с помощью ключевого словаsynchronized.

Выполнение этого кода приводит к выводу, подобному следующему. Ваш результат, скорее всего, будет отличаться от приведенного ниже, в основном потому, что методsleep() вызывается для другого интервала:

Philosopher 1 8038014601251: Thinking
Philosopher 2 8038014828862: Thinking
Philosopher 3 8038015066722: Thinking
Philosopher 4 8038015284511: Thinking
Philosopher 5 8038015468564: Thinking
Philosopher 1 8038016857288: Picked up left fork
Philosopher 1 8038022332758: Picked up right fork - eating
Philosopher 3 8038028886069: Picked up left fork
Philosopher 4 8038063952219: Picked up left fork
Philosopher 1 8038067505168: Put down right fork
Philosopher 2 8038089505264: Picked up left fork
Philosopher 1 8038089505264: Put down left fork. Back to thinking
Philosopher 5 8038111040317: Picked up left fork

ВсеPhilosopher изначально начинают думать, и мы видим, чтоPhilosopher 1 продолжает брать левую и правую вилки, затем ест и кладет их обе вниз, после чегоPhilosopher 5 выбирает это вверх.

5. Проблема с решением: тупик

Хотя кажется, что приведенное выше решение является правильным, возникает проблема тупиковой ситуации.

Тупиковая ситуация - это ситуация, когда развитие системы останавливается, поскольку каждый процесс ожидает получения ресурса, удерживаемого каким-либо другим процессом.

Мы можем подтвердить то же самое, запустив приведенный выше код несколько раз и проверив, что код просто зависает. Вот пример вывода, демонстрирующий указанную выше проблему:

Philosopher 1 8487540546530: Thinking
Philosopher 2 8487542012975: Thinking
Philosopher 3 8487543057508: Thinking
Philosopher 4 8487543318428: Thinking
Philosopher 5 8487544590144: Thinking
Philosopher 3 8487589069046: Picked up left fork
Philosopher 1 8487596641267: Picked up left fork
Philosopher 5 8487597646086: Picked up left fork
Philosopher 4 8487617680958: Picked up left fork
Philosopher 2 8487631148853: Picked up left fork

В этой ситуации каждый изPhilosophers получил свою левую вилку, но не может получить свою правую вилку, потому что его сосед уже приобрел ее. Эта ситуация обычно известна какcircular wait и является одним из условий, которое приводит к тупиковой ситуации и препятствует продвижению системы.

6. Выход из тупика

Как мы видели выше, основной причиной тупика является условие циклического ожидания, когда каждый процесс ожидает ресурса, который удерживается каким-либо другим процессом. Следовательно, чтобы избежать ситуации тупика, мы должны убедиться, что условие циклического ожидания нарушено. Есть несколько способов достичь этого, самым простым из которых является следующее:

Все Философы тянутся к своей левой развилке первыми, кроме того, кто первым тянется к своей правой развилке.

Мы реализуем это в нашем существующем коде, внеся относительно небольшие изменения в код:

public class DiningPhilosophers {

    public static void main(String[] args) throws Exception {

        final Philosopher[] philosophers = new Philosopher[5];
        Object[] forks = new Object[philosophers.length];

        for (int i = 0; i < forks.length; i++) {
            forks[i] = new Object();
        }

        for (int i = 0; i < philosophers.length; i++) {
            Object leftFork = forks[i];
            Object rightFork = forks[(i + 1) % forks.length];

            if (i == philosophers.length - 1) {

                // The last philosopher picks up the right fork first
                philosophers[i] = new Philosopher(rightFork, leftFork);
            } else {
                philosophers[i] = new Philosopher(leftFork, rightFork);
            }

            Thread t
              = new Thread(philosophers[i], "Philosopher " + (i + 1));
            t.start();
        }
    }
}

Изменение происходит в строках 17-19 вышеприведенного кода, где мы вводим условие, которое заставляет последнего философа сначала добраться до своей правой вилки, а не левой. Это нарушает условие кругового ожидания, и мы можем предотвратить тупик.

Следующий вывод показывает один из случаев, когда всеPhilosopher получают возможность думать и есть, не вызывая тупиковой ситуации:

Philosopher 1 88519839556188: Thinking
Philosopher 2 88519840186495: Thinking
Philosopher 3 88519840647695: Thinking
Philosopher 4 88519840870182: Thinking
Philosopher 5 88519840956443: Thinking
Philosopher 3 88519864404195: Picked up left fork
Philosopher 5 88519871990082: Picked up left fork
Philosopher 4 88519874059504: Picked up left fork
Philosopher 5 88519876989405: Picked up right fork - eating
Philosopher 2 88519935045524: Picked up left fork
Philosopher 5 88519951109805: Put down right fork
Philosopher 4 88519997119634: Picked up right fork - eating
Philosopher 5 88519997113229: Put down left fork. Back to thinking
Philosopher 5 88520011135846: Thinking
Philosopher 1 88520011129013: Picked up left fork
Philosopher 4 88520028194269: Put down right fork
Philosopher 4 88520057160194: Put down left fork. Back to thinking
Philosopher 3 88520067162257: Picked up right fork - eating
Philosopher 4 88520067158414: Thinking
Philosopher 3 88520160247801: Put down right fork
Philosopher 4 88520249049308: Picked up left fork
Philosopher 3 88520249119769: Put down left fork. Back to thinking

Это можно проверить, выполнив код несколько раз, что система свободна от тупиковой ситуации, которая произошла ранее.

7. Заключение

В этой статье мы исследовали знаменитую задачу «Обеденные философы» иthe concepts of circular wait and deadlock. Мы разработали простое решение, которое привело к тупику и внесло простое изменение, чтобы прервать круговое ожидание и избежать тупика. Это только начало, и существуют более сложные решения.

Код для этой статьи можно найтиover on GitHub.