Escreva um programa que certamente entrará em um impasse [fechado]


86

Recentemente, recebi essas perguntas em uma entrevista.

Respondi que o impasse ocorre se a intercalação der errado, mas o entrevistador insistiu que um programa que sempre entrará em impasse, independentemente da intercalação, pode ser escrito.

Podemos escrever esse programa? Você pode me indicar algum programa de exemplo como esse?


3
O entrevistador é definitivamente um sujeito tolo.
Leão

23
O entrevistador certamente não é um sujeito tolo. Compreensão total de um assunto significa que você deve ser capaz de explicar casos extremos polares: fazendo o programa nunca travar e sempre travar.
Yuriy Zubarev de

Respostas:


100

ATUALIZAÇÃO: Esta questão foi o assunto do meu blog em janeiro de 2013 . Obrigado pela ótima pergunta!


Como podemos escrever um programa que sempre entrará em impasse, não importa como os threads estão agendados?

Aqui está um exemplo em C #. Observe que o programa parece não conter bloqueios e dados compartilhados. Ele tem apenas uma única variável local e três instruções e, ainda assim, bloqueia com 100% de certeza. Seria difícil chegar a um programa mais simples que travasse com certeza.

Exercício para o leitor # 1: explique como isso bloqueia. (Uma resposta está nos comentários.)

Exercício para o leitor 2: demonstrar o mesmo impasse em Java. (Uma resposta está aqui: https://stackoverflow.com/a/9286697/88656 )

class MyClass
{
  static MyClass() 
  {
    // Let's run the initialization on another thread!
    var thread = new System.Threading.Thread(Initialize);
    thread.Start();
    thread.Join();
  }

  static void Initialize() 
  { /* TODO: Add initialization code */ }

  static void Main() 
  { }
}

4
Meu conhecimento de C # teórico é limitado, mas presumo que o carregador de classe garanta que o código seja executado em uma única thread, como acontece em Java. Tenho certeza de que há um exemplo semelhante em Java Puzzlers.
Voo

11
@Voo: Você tem boa memória. Neal Gafter - o coautor de "Java Puzzlers" - e eu apresentei uma versão um pouco mais ofuscada desse código em nossa palestra "C # Puzzlers" na Conferência de Desenvolvedores de Oslo alguns anos atrás.
Eric Lippert

41
@Lieven: O construtor estático deve ser executado no máximo uma vez e deve ser executado antes da primeira chamada para qualquer método estático na classe. Main é um método estático, então o thread principal chama o ctor estático. Para garantir que ele seja executado apenas uma vez, o CLR tira um bloqueio que não é liberado até que o ctor estático termine. Quando o ctor inicia um novo encadeamento, esse encadeamento também chama um método estático, então o CLR tenta pegar o bloqueio para ver se ele precisa executar o ctor. Enquanto isso, o encadeamento principal "se junta" ao encadeamento bloqueado e agora temos nosso impasse.
Eric Lippert

33
@artbristol: Nunca escrevi nem mesmo uma linha de código Java; Não vejo razão para começar agora.
Eric Lippert

4
Oh, presumi que você tivesse uma resposta para o seu Exercício # 2. Parabéns por obter tantos votos positivos para responder a uma pergunta sobre Java.
artbristol

27

A trava aqui garante que ambos os bloqueios sejam mantidos quando cada thread tenta bloquear o outro:

import java.util.concurrent.CountDownLatch;

public class Locker extends Thread {

   private final CountDownLatch latch;
   private final Object         obj1;
   private final Object         obj2;

   Locker(Object obj1, Object obj2, CountDownLatch latch) {
      this.obj1 = obj1;
      this.obj2 = obj2;
      this.latch = latch;
   }

   @Override
   public void run() {
      synchronized (obj1) {

         latch.countDown();
         try {
            latch.await();
         } catch (InterruptedException e) {
            throw new RuntimeException();
         }
         synchronized (obj2) {
            System.out.println("Thread finished");
         }
      }

   }

   public static void main(String[] args) {
      final Object obj1 = new Object();
      final Object obj2 = new Object();
      final CountDownLatch latch = new CountDownLatch(2);

      new Locker(obj1, obj2, latch).start();
      new Locker(obj2, obj1, latch).start();

   }

}

Interessante executar o jconsole, que mostrará corretamente o impasse na guia Threads.


3
É o melhor até agora, mas eu substituiria sleeppor uma trava adequada: teoricamente, temos uma condição de corrida aqui. Embora possamos ter quase certeza de que 0,5 segundo é o suficiente, não é muito bom para uma tarefa de entrevista.
alf

25

O deadlock acontece quando os threads (ou o que quer que sua plataforma chame de unidades de execução) adquirem recursos, onde cada recurso só pode ser mantido por um thread de cada vez e retém esses recursos de tal forma que as suspensões não podem ser impedidas, e existe algum relacionamento "circular" entre os threads de modo que cada thread no conflito está esperando para adquirir algum recurso mantido por outro thread.

Portanto, uma maneira fácil de evitar o conflito é dar uma ordem total aos recursos e impor uma regra de que os recursos só são adquiridos por threads em ordem . Por outro lado, um deadlock pode ser criado intencionalmente executando threads que adquirem recursos, mas não os adquirem em ordem. Por exemplo:

Dois fios, duas fechaduras. O primeiro thread executa um loop que tenta adquirir os bloqueios em uma determinada ordem, o segundo thread executa um loop que tenta adquirir os bloqueios na ordem oposta. Cada thread libera ambos os bloqueios após adquiri-los com sucesso.

public class HighlyLikelyDeadlock {
    static class Locker implements Runnable {
        private Object first, second;

        Locker(Object first, Object second) {
            this.first = first;
            this.second = second;
        }

        @Override
        public void run() {
            while (true) {
                synchronized (first) {
                    synchronized (second) {
                        System.out.println(Thread.currentThread().getName());
                    }
                }
            }
        }
    }

    public static void main(final String... args) {
        Object lock1 = new Object(), lock2 = new Object();
        new Thread(new Locker(lock1, lock2), "Thread 1").start();
        new Thread(new Locker(lock2, lock1), "Thread 2").start();
    }
}

Agora, houve alguns comentários nesta questão que apontam a diferença entre o probabilidade e a certeza de um impasse. Em certo sentido, a distinção é uma questão acadêmica. Do ponto de vista prático, certamente gostaria de ver um sistema em execução que não bloqueie com o código que escrevi acima :)

No entanto, as perguntas da entrevista podem ser acadêmicas às vezes, e essa pergunta do SO tem a palavra "certamente" no título, então o que se segue é um programa que certamente bloqueia. Dois Lockerobjetos são criados, cada um recebe dois bloqueios e um CountDownLatchusado para sincronizar entre os threads. Cada Lockertrava a primeira fechadura e, em seguida, conta uma vez. Quando os dois fios adquirem um bloqueio e fazem a contagem regressiva do trinco, passam pela barreira do trinco e tentam obter um segundo bloqueio, mas em cada caso o outro segmento já contém o bloqueio desejado. Essa situação resulta em um certo impasse.

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class CertainDeadlock {
    static class Locker implements Runnable {
        private CountDownLatch latch;
        private Lock first, second;

        Locker(CountDownLatch latch, Lock first, Lock second) {
            this.latch = latch;
            this.first = first;
            this.second = second;
        }

        @Override
        public void run() {
            String threadName = Thread.currentThread().getName();
            try {
                first.lock();
                latch.countDown();
                System.out.println(threadName + ": locked first lock");
                latch.await();
                System.out.println(threadName + ": attempting to lock second lock");
                second.lock();
                System.out.println(threadName + ": never reached");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }

    public static void main(final String... args) {
        CountDownLatch latch = new CountDownLatch(2);
        Lock lock1 = new ReentrantLock(), lock2 = new ReentrantLock();
        new Thread(new Locker(latch, lock1, lock2), "Thread 1").start();
        new Thread(new Locker(latch, lock2, lock1), "Thread 2").start();
    }
}

3
Desculpe por citar Linus, "Falar é barato. Mostre-me o código." - é uma boa tarefa e é surpreendentemente mais difícil do que parece.
alf

2
É possível executar este código sem deadlock
Vladimir Zhilyaev

1
Ok, vocês são brutais, mas acho que agora é uma resposta completa.
Greg Mattes

@GregMattes obrigado :) Não posso adicionar nada além de +1, e espero que você tenha se divertido :)
alf

15

Aqui está um exemplo de Java seguindo o de Eric Lippert:

public class Lock implements Runnable {

    static {
        System.out.println("Getting ready to greet the world");
        try {
            Thread t = new Thread(new Lock());
            t.start();
            t.join();
        } catch (InterruptedException ex) {
            System.out.println("won't see me");
        }
    }

    public static void main(String[] args) {
        System.out.println("Hello World!");
    }

    public void run() {           
        Lock lock = new Lock();      
    }

}

4
Acho que usar o método join in run é um pouco enganador. Ele sugere que essa outra junção além daquela no bloco estático é necessária para obter um deadlock enquanto o deadlock é causado pela instrução "new Lock ()". Minha reescrita, usando o método estático como no exemplo C #: stackoverflow.com/a/16203272/2098232
luke657

Você poderia explicar seu exemplo?
gstackoverflow de

de acordo com minhas experiências t.join (); O método run () interno é redundante
gstackoverflow de

Removi o código redundante que impede a compreensão
gstackoverflow

11

Aqui está um exemplo da documentação:

public class Deadlock {
    static class Friend {
        private final String name;
        public Friend(String name) {
            this.name = name;
        }
        public String getName() {
            return this.name;
        }
        public synchronized void bow(Friend bower) {
            System.out.format("%s: %s"
                + "  has bowed to me!%n", 
                this.name, bower.getName());
            bower.bowBack(this);
        }
        public synchronized void bowBack(Friend bower) {
            System.out.format("%s: %s"
                + " has bowed back to me!%n",
                this.name, bower.getName());
        }
    }

    public static void main(String[] args) {
        final Friend alphonse =
            new Friend("Alphonse");
        final Friend gaston =
            new Friend("Gaston");
        new Thread(new Runnable() {
            public void run() { alphonse.bow(gaston); }
        }).start();
        new Thread(new Runnable() {
            public void run() { gaston.bow(alphonse); }
        }).start();
    }
}

2
1 Para vincular o tutorial java.
mre

4
"é extremamente provável" não é bom o suficiente para "certamente entrará em um impasse"
alf

1
@alf Oh, mas a questão fundamental é demonstrada muito bem aqui. Pode-se escrever um agendador round robin que expõe um Object invokeAndWait(Callable task)método. Então tudo que Callable t1tem a fazer é invokeAndWait()para Callable t2durante o seu tempo de vida antes de retornar, e vice-versa.
user268396

2
@ user268396 bem, a questão fundamental é trivial e entediante :) O objetivo da tarefa é descobrir - ou provar que você entende - que é surpreendentemente difícil obter um deadlock garantido (bem como garantir qualquer coisa em um mundo assíncrono )
alf

4
@bezz sleepé chato. Embora eu acredite que nenhum tópico será iniciado por 5 segundos, é uma condição de corrida, de qualquer maneira. Você não quer contratar um programador que confiaria sleep()na resolução de condições de corrida :)
alf

9

Reescrevi a versão Java de Yuriy Zubarev do exemplo de deadlock postado por Eric Lippert: https://stackoverflow.com/a/9286697/2098232 para se parecer mais com a versão C #. Se o bloco de inicialização do Java funciona de forma semelhante ao construtor estático C # e primeiro adquire o bloqueio, não precisamos de outro thread para também invocar o método de junção para obter um deadlock, ele só precisa invocar algum método estático da classe Lock, como o C # original exemplo. O impasse resultante parece confirmar isso.

public class Lock {

    static {
        System.out.println("Getting ready to greet the world");
        try {
            Thread t = new Thread(new Runnable(){

                @Override
                public void run() {
                    Lock.initialize();
                }

            });
            t.start();
            t.join();
        } catch (InterruptedException ex) {
            System.out.println("won't see me");
        }
    }

    public static void main(String[] args) {
        System.out.println("Hello World!");
    }

    public static void initialize(){
        System.out.println("Initializing");
    }

}

por que quando eu comento Lock.initialize () no método run ele não bloqueia? o método de inicialização não faz nada ??
Aequitas

@Aequitas é apenas um palpite, mas o método pode estar sendo otimizado; não tenho certeza sobre como isso funcionaria com threads
Dave Cousineau

5

Não é a tarefa de entrevista mais simples que você pode conseguir: no meu projeto, paralisou o trabalho de uma equipe por um dia inteiro. É muito fácil fazer seu programa parar, mas é muito difícil levá-lo ao estado em que o despejo de thread grava algo como,

Found one Java-level deadlock:
=============================
"Thread-2":
  waiting to lock monitor 7f91c5802b58 (object 7fb291380, a java.lang.String),
  which is held by "Thread-1"
"Thread-1":
  waiting to lock monitor 7f91c6075308 (object 7fb2914a0, a java.lang.String),
  which is held by "Thread-2"

Java stack information for the threads listed above:
===================================================
"Thread-2":
    at uk.ac.ebi.Deadlock.run(Deadlock.java:54)
    - waiting to lock <7fb291380> (a java.lang.String)
    - locked <7fb2914a0> (a java.lang.String)
    - locked <7f32a0760> (a uk.ac.ebi.Deadlock)
    at java.lang.Thread.run(Thread.java:680)
"Thread-1":
    at uk.ac.ebi.Deadlock.run(Deadlock.java:54)
    - waiting to lock <7fb2914a0> (a java.lang.String)
    - locked <7fb291380> (a java.lang.String)
    - locked <7f32a0580> (a uk.ac.ebi.Deadlock)
    at java.lang.Thread.run(Thread.java:680)

Portanto, o objetivo seria obter um conflito que a JVM considerará um conflito. Obviamente, nenhuma solução como

synchronized (this) {
    wait();
}

funcionarão nesse sentido, embora realmente parem para sempre. Depender de uma condição de corrida também não é uma boa ideia, já que durante a entrevista você geralmente quer mostrar algo que provavelmente está funcionando, não algo que deveria funcionar na maior parte do tempo.

Agora, a sleep()solução está bem em um sentido, é difícil imaginar uma situação em que não funcione, mas não é justo (estamos em um esporte justo, não é?). A solução de @artbristol (a minha é a mesma, apenas objetos diferentes como monitores) é boa, mas longa e usa as novas primitivas de simultaneidade para colocar os threads no estado certo, o que não é muito divertido:

public class Deadlock implements Runnable {
    private final Object a;
    private final Object b;
    private final static CountDownLatch latch = new CountDownLatch(2);

    public Deadlock(Object a, Object b) {
        this.a = a;
        this.b = b;
    }

    public synchronized static void main(String[] args) throws InterruptedException {
        new Thread(new Deadlock("a", "b")).start();
        new Thread(new Deadlock("b", "a")).start();
    }

    @Override
    public void run() {
        synchronized (a) {
            latch.countDown();
            try {
                latch.await();
            } catch (InterruptedException ignored) {
            }
            synchronized (b) {
            }
        }
    }
}

Eu me lembro que o synchronized solução -only se encaixa 11..13 linhas de código (excluindo comentários e importações), mas ainda não me lembrei do truque real. Atualizará se eu fizer.

Atualização: aqui está uma solução feia em synchronized:

public class Deadlock implements Runnable {
    public synchronized static void main(String[] args) throws InterruptedException {
        synchronized ("a") {
            new Thread(new Deadlock()).start();
            "a".wait();
        }
        synchronized ("") {
        }
    }

    @Override
    public void run() {
        synchronized ("") {
            synchronized ("a") {
                "a".notifyAll();
            }
            synchronized (Deadlock.class) {
            }
        }
    }
}

Observe que substituímos uma trava por um monitor de objeto (usando "a"como um objeto).


Hum, acho que é uma tarefa de entrevista justa. Ele pede que você realmente entenda os bloqueios e bloqueios em Java. Eu não acho que a ideia geral seja tão difícil (certifique-se de que ambos os tópicos só possam continuar depois de ambos terem bloqueado seu primeiro recurso), você apenas deve se lembrar do CountdownLatch - mas como entrevistador, eu ajudaria o entrevistado nessa parte se ele pudesse explicar o que exatamente ele precisava (não é uma classe que a maioria dos desenvolvedores precisa e você não pode pesquisar no Google em uma entrevista). Eu adoraria receber perguntas tão interessantes para entrevistas!
Voo

@Voo na época em que estávamos brincando com ele, não havia travas no JDK, então era tudo à mão. E a diferença entre LOCKEDe waiting to locké sutil, não algo que você lê durante o café da manhã. Mas bem, você provavelmente está certo. Deixe-me reformular.
alf

4

Esta versão C #, acho que o java deve ser bastante semelhante.

static void Main(string[] args)
{
    var mainThread = Thread.CurrentThread;
    mainThread.Join();

    Console.WriteLine("Press Any key");
    Console.ReadKey();
}

2
Um bom! Realmente o programa C # mais curto para criar um deadlock se você remover as consoleinstruções. Você pode simplesmente escrever a Mainfunção inteira como Thread.CurrentThread.Join();.
RBT de

3
import java.util.concurrent.CountDownLatch;

public class SO8880286 {
    public static class BadRunnable implements Runnable {
        private CountDownLatch latch;

        public BadRunnable(CountDownLatch latch) {
            this.latch = latch;
        }

        public void run() {
            System.out.println("Thread " + Thread.currentThread().getId() + " starting");
            synchronized (BadRunnable.class) {
                System.out.println("Thread " + Thread.currentThread().getId() + " acquired the monitor on BadRunnable.class");
                latch.countDown();
                while (true) {
                    try {
                        latch.await();
                    } catch (InterruptedException ex) {
                        continue;
                    }
                    break;
                }
            }
            System.out.println("Thread " + Thread.currentThread().getId() + " released the monitor on BadRunnable.class");
            System.out.println("Thread " + Thread.currentThread().getId() + " ending");
        }
    }

    public static void main(String[] args) {
        Thread[] threads = new Thread[2];
        CountDownLatch latch = new CountDownLatch(threads.length);
        for (int i = 0; i < threads.length; ++i) {
            threads[i] = new Thread(new BadRunnable(latch));
            threads[i].start();
        }
    }
}

O programa sempre bloqueia porque cada thread está esperando na barreira pelos outros threads, mas para esperar a barreira, a thread deve estar segurando o monitor BadRunnable.class.


3
} catch (InterruptedException ex) { continue; }... linda
artbristol

2

Há um exemplo em Java aqui

http://baddotrobot.com/blog/2009/12/24/deadlock/

Onde um sequestrador entra em um impasse quando se recusa a entregar a vítima até conseguir o dinheiro, mas o negociador se recusa a entregar o dinheiro até pegar a vítima.


Essa implementação não é pertinente conforme fornecido. Algumas partes do código parecem estar faltando. No entanto, a ideia geral expressa está correta no que diz respeito à contenção de recursos que leva ao conflito.
Master Chief

o exemplo é pedagógico, então estou curioso para saber por que você o interpreta como não pertinente ... os códigos ausentes são métodos vazios onde os nomes dos métodos devem ser úteis (mas não mostrados por brevidade)
Toby

1

Uma simples pesquisa me deu o seguinte código:

public class Deadlock {
    static class Friend {
        private final String name;
        public Friend(String name) {
            this.name = name;
        }
        public String getName() {
            return this.name;
        }
        public synchronized void bow(Friend bower) {
            System.out.format("%s: %s"
                + "  has bowed to me!%n", 
                this.name, bower.getName());
            bower.bowBack(this);
        }
        public synchronized void bowBack(Friend bower) {
            System.out.format("%s: %s"
                + " has bowed back to me!%n",
                this.name, bower.getName());
        }
    }

    public static void main(String[] args) {
        final Friend alphonse =
            new Friend("Alphonse");
        final Friend gaston =
            new Friend("Gaston");
        new Thread(new Runnable() {
            public void run() { alphonse.bow(gaston); }
        }).start();
        new Thread(new Runnable() {
            public void run() { gaston.bow(alphonse); }
        }).start();
    }
}

Fonte: Deadlock


3
"é extremamente provável" não é bom o suficiente para "certamente entrará em um impasse"
alf

1

Aqui está um exemplo em que um thread segurando o bloqueio inicia outro thread que deseja o mesmo bloqueio e, em seguida, o iniciador espera até que o início termine ... para sempre:

class OuterTask implements Runnable {
    private final Object lock;

    public OuterTask(Object lock) {
        this.lock = lock;
    }

    public void run() {
        System.out.println("Outer launched");
        System.out.println("Obtaining lock");
        synchronized (lock) {
            Thread inner = new Thread(new InnerTask(lock), "inner");
            inner.start();
            try {
                inner.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

class InnerTask implements Runnable {
    private final Object lock;

    public InnerTask(Object lock) {
        this.lock = lock;
    }

    public void run() {
        System.out.println("Inner launched");
        System.out.println("Obtaining lock");
        synchronized (lock) {
            System.out.println("Obtained");
        }
    }
}

class Sample {
    public static void main(String[] args) throws InterruptedException {
        final Object outerLock = new Object();
        OuterTask outerTask = new OuterTask(outerLock);
        Thread outer = new Thread(outerTask, "outer");
        outer.start();
        outer.join();
    }
}

0

Aqui está um exemplo:

dois threads estão em execução, cada um esperando que o outro libere o bloqueio

public class ThreadClass extends Thread {

String obj1,obj2;
ThreadClass(String obj1,String obj2){
    this.obj1=obj1;
    this.obj2=obj2;
    start();
}

public void run(){
    synchronized (obj1) {
        System.out.println("lock on "+obj1+" acquired");

        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("waiting for "+obj2);
        synchronized (obj2) {
            System.out.println("lock on"+ obj2+" acquired");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }


}

}

Executar isso levaria a um impasse:

public class SureDeadlock {

public static void main(String[] args) {
    String obj1= new String("obj1");
    String obj2= new String("obj2");

    new ThreadClass(obj1,obj2);
    new ThreadClass(obj2,obj1);


}

}

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.