Pergunta

class Downloader extends Thread {
    private InputStream in;
    private OutputStream out;
    private ArrayList<ProgressListener> listeners;
    public Downloader(URL url, String outputFilename) throws IOException {
        in = url.openConnection().getInputStream();
        out = new FileOutputStream(outputFilename);
        listeners = new ArrayList<ProgressListener>();
    }
    public synchronized void addListener(ProgressListener listener) {
        listeners.add(listener);
    }
    public synchronized void removeListener(ProgressListener listener) {
        listeners.remove(listener);
    }

    private synchronized void updateProgress(int n) {
        for (ProgressListener listener: listeners)
            listener.onProgress(n);
    }
    public void run() {
        int n = 0, total = 0;
        byte[] buffer = new byte[1024];
        try {
            while((n = in.read(buffer)) != -1) {
                out.write(buffer, 0, n);
                total += n;
                updateProgress(total);
            }
            out.flush();
        } catch (IOException e) { }
    }
}

O código acima é do livro “sete modelos de simultaneidade em sete semanas”.O livro diz que o código acima tem potencial para o impasse, pois o método sincronizado updateProgress chama um método alienígena [onProgress] que pode adquirir outro bloqueio.Como adquirimos dois bloqueios sem ordem correta, o impasse pode ocorrer.

Alguém pode explicar como o impasse acontece no cenário acima?

Desde já, obrigado.

Foi útil?

Solução

É melhor fazer com que os objetos que você usa synchronized privado.

Como você sincroniza no Downloader, você não sabe se outros threads são sincronizados no Downloader também.

O seguinte listener causa um deadlock:

MyProgressListener extends ProgressListener {

     public Downloader downloader;
     public void onProgress(int n) {
         Thread t = new Thread() {
             @Override
             public void run() {
                 synchronized(downloader) {
                     // do something ...
                 }
             }
         };
         t.start();
         t.join();
     }
}

Código que causa impasse:

Downloader d = new Downloader(...);
MyProgressListener l = new MyProgressListener();
l.downloader = d;
d.addListener(l);
d.run();

O seguinte acontecerá se você executar esse código:

  1. o fio principal chega ao updateProgress e adquire um bloqueio no Downloader
  2. o MyProgressListenerde onProgress método é chamado e o novo thread t começou
  3. o fio principal atinge t.join();

Nesta situação, o thread principal não pode prosseguir até t está terminado, mas para t para finalizar, o thread principal teria que liberar seu bloqueio no Downloader, mas isso não acontecerá porque o thread principal não pode prosseguir -> Impasse

Outras dicas

Primeiro, lembre-se que a palavra-chave synchronized, quando aplicado a uma classe, implica bloquear todo o objeto ao qual este método pertence.Agora, vamos esboçar outros objetos que acionam o impasse:

class DLlistener implements ProgressListener {

  private Downloader d;

  public DLlistener(Downloader d){
      this.d = d;
      // here we innocently register ourself to the downloader: this method is synchronized
      d.addListener(this);
  }

  public void onProgress(int n){
    // this method is invoked from a synchronized call in Downloader
    // all we have to do to create a dead lock is to call another synchronized method of that same object from a different thread *while holding the lock*
    DLthread thread = new DLThread(d);
    thread.start();
    thread.join();
  }
}

// this is the other thread which will produce the deadlock
class DLThread extends Thread {
   Downloader locked;
  DLThread(Downloader d){
    locked = d;
  }
  public void run(){
    // here we create a new listener, which will register itself and generate the dead lock
    DLlistener listener(locked);
    // ...
  }
}

Uma maneira de evitar o impasse é adiar o trabalho realizado em addListener por ter filas internas de ouvintes esperando para serem adicionados/removidos e ter Downloader cuidando deles sozinho periodicamente.Isto depende, em última análise, Downloader.run trabalho interno, é claro.

Provavelmente o problema neste código:

for (ProgressListener listener: listeners)
            listener.onProgress(n);

Quando um thread, que mantém um bloqueio, chama um método externo como este (OnProgress), você não pode garantir que a implementação desse método não tente obter outro bloqueio, que pode ser mantido por um encadeamento diferente.Isso pode causar um impasse.

Aqui está um exemplo clássico que mostra o tipo de problemas difíceis de depurar que o autor está tentando evitar.

A classe UseDownloader é criado e downloadSomething é chamado.

À medida que o download avança, o onProgress método é chamado.Como isso é chamado de dentro do bloco sincronizado, o Downloader motinor está bloqueado.Dentro do nosso onProgress método, precisamos bloquear nosso próprio recurso, neste caso lock.Então, quando estamos tentando sincronizar lock estamos segurando o Downloader monitor.

Se outro thread decidir que o download deve ser cancelado, ele chamará setCanceled.Este primeiro teste done então ele sincronizou no lock monitorar e depois ligar removeListener.Mas removeListener requer o Downloader trancar.

Esse tipo de impasse pode ser difícil de encontrar porque não acontece com muita frequência.

  public static final int END_DOWNLOAD = 100;

  class UseDownloader implements ProgressListener {
    Downloader d;
    Object lock = new Object();
    boolean done = false;

    public UseDownloader(Downloader d) {
      this.d = d;
    }
    public void onProgress(int n) {
      synchronized(lock) {
        if (!done) {
          // show some progress
        }
      }
    }

    public void downloadSomething() {
      d.addListener(this);
      d.start();
    }

    public boolean setCanceled() {
      synchronized(lock) {
        if (!done) {
          done = true;
          d.removeListener(this);
        }
      }
    }
  }

O exemplo a seguir leva a um impasse porque o MyProgressListener tenta adquirir o bloqueio do Downloader enquanto ele já foi adquirido.

class MyProgressListener extends ProgressListener {
    private Downloader myDownloader;

    public MyProgressListener(Downloader downloader) {
        myDownloader = downloader;
    }

    public void onProgress(int n) {
        // starts and waits for a thread that accesses myDownloader
    }
}

Downloader downloader = new Downloader(...);
downloader.addListener(new MyListener(downloader));
downloader.run();
Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top