O Problema dos Filósofos do Jantar em Java

O Problema dos Filósofos do Jantar em Java

1. Introdução

O problema dos Filósofos Jantar é um dos problemas clássicos usados ​​paradescribe synchronization issues in a multi-threaded environment and illustrate techniques for solving them. Dijkstra formulou esse problema pela primeira vez e apresentou-o em relação aos computadores que acessam periféricos de unidades de fita.

A presente formulação foi dada por Tony Hoare, que também é conhecido por inventar o algoritmo de classificação quicksort. Neste artigo, analisamos esse problema conhecido e codificamos uma solução popular.

2. O problema

image

O diagrama acima representa o problema. Há cinco filósofos silenciosos (P1 - P5) sentados ao redor de uma mesa circular, passando a vida comendo e pensando.

Existem cinco garfos para compartilhar (1 - 5) e para poder comer, um filósofo precisa ter garfos nas duas mãos. Depois de comer, ele coloca os dois no chão e eles podem ser escolhidos por outro filósofo que repete o mesmo ciclo.

O objetivo é criar um esquema / protocolo que ajude os filósofos a alcançar seu objetivo de comer e pensar sem morrer de fome.

3. Uma solução

Uma solução inicial seria fazer com que cada um dos filósofos seguisse o seguinte protocolo:

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!
}

Como o pseudo-código acima descreve, cada filósofo está pensando inicialmente. After a certain amount of time, the philosopher gets hungry and wishes to eat.

Neste ponto,he reaches for the forks on his either side and once he’s got both of them, proceeds to eat. Depois de comer, o filósofo abaixa os garfos para que fiquem à disposição de seu vizinho.

4. Implementação

Modelamos cada um de nossos filósofos como classes que implementam a interfaceRunnable para que possamos executá-los como threads separados. CadaPhilosopher tem acesso a dois garfos em seus lados esquerdo e direito:

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
    }

}

Também temos um método que instrui aPhilosopher a realizar uma ação - comer, pensar ou adquirir garfos para se preparar para comer:

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
}

Conforme mostrado no código acima, cada ação é simulada suspendendo o encadeamento de chamada por um período de tempo aleatório, de modo que a ordem de execução não seja aplicada apenas pelo tempo.

Agora, vamos implementar a lógica central de aPhilosopher.

Para simular a aquisição de um fork, precisamos travá-lo de modo que nenhum segmentoPhilosopher o adquira ao mesmo tempo. Para conseguir isso, usamos o

Para conseguir isso, usamos a palavra-chavesynchronized para adquirir o monitor interno do objeto fork e evitar que outros threads façam o mesmo. Um guia para a palavra-chavesynchronized em Java pode ser encontradohere. Prosseguimos com a implementação do métodorun() na classePhilosopher agora:

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;
        }
    }
}

Este esquema implementa exatamente o descrito anteriormente: aPhilosopher pensa um pouco e então decide comer.

Depois disso, ele adquire os garfos à esquerda e à direita e começa a comer. Quando terminar, ele coloca os garfos no chão. Também adicionamos carimbos de data e hora a cada ação, o que nos ajudaria a entender a ordem em que os eventos ocorrem.

Para iniciar todo o processo, escrevemos um cliente que cria 5Philosophers como threads e inicia todos eles:

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();
        }
    }
}

Modelamos cada um dos garfos como objetos Java genéricos e fazemos o maior número possível de filósofos. Passamos a cadaPhilosopher seus garfos esquerdo e direito que ele tenta bloquear usando a palavra-chavesynchronized.

A execução desse código resulta em uma saída semelhante à seguinte. Sua saída provavelmente será diferente da fornecida abaixo, principalmente porque o métodosleep() é invocado para um intervalo diferente:

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

Todos osPhilosophers inicialmente começam a pensar, e vemos quePhilosopher 1 passa a pegar a bifurcação esquerda e direita, então come e passa a colocá-las no chão, após o quePhilosopher 5 escolhe isso.

5. O problema com a solução: impasse

Embora pareça que a solução acima esteja correta, há um problema de conflito surgindo.

Um impasse é uma situação em que o progresso de um sistema é interrompido, pois cada processo aguarda a aquisição de um recurso mantido por outro processo.

Podemos confirmar o mesmo executando o código acima algumas vezes e verificando que, algumas vezes, o código simplesmente trava. Aqui está um exemplo de saída que demonstra o problema acima:

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

Nesta situação, cada um dosPhilosophers adquiriu seu garfo esquerdo, mas não pode adquirir seu garfo direito, porque seu vizinho já o adquiriu. Essa situação é comumente conhecida comocircular waite é uma das condições que resulta em um deadlock e impede o progresso do sistema.

6. Resolvendo o impasse

Como vimos acima, o principal motivo para um impasse é a condição de espera circular em que cada processo aguarda um recurso que está sendo mantido por algum outro processo. Portanto, para evitar uma situação de conflito, precisamos garantir que a condição de espera circular esteja quebrada. Existem várias maneiras de conseguir isso, sendo a mais simples:

Todos os filósofos pegam o garfo esquerdo primeiro, exceto aquele que primeiro pega o garfo direito.

Implementamos isso em nosso código existente, fazendo uma alteração relativamente pequena no código:

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();
        }
    }
}

A mudança ocorre nas linhas 17-19 do código acima, onde introduzimos a condição que faz com que o último filósofo alcance primeiro o garfo direito, em vez da esquerda. Isso interrompe a condição de espera circular e podemos evitar o impasse.

A saída a seguir mostra um dos casos em que todos osPhilosophers têm a chance de pensar e comer, sem causar um impasse:

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

Pode ser verificado executando o código várias vezes, se o sistema está livre da situação de conflito que ocorreu anteriormente.

7. Conclusão

Neste artigo, exploramos o famoso problema dos Dining Philosophers ethe concepts of circular wait and deadlock. Codificamos uma solução simples que causou um impasse e fizemos uma alteração simples para interromper a espera circular e evitar um impasse. Este é apenas o começo e existem soluções mais sofisticadas.

O código para este artigo pode ser encontradoover on GitHub.