Тупик при вызове двух синхронизированных методов

1
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) { }
    }
}

Вышеприведенный код из книги "Семь параллельных моделей за семь недель". В книге говорится, что вышеуказанный код имеет потенциал для тупиковой ситуации, поскольку синхронизированный метод updateProgress вызывает метод alien [onProgress], который может получить другую блокировку. Поскольку мы приобретаем два замка без правильного порядка, может возникнуть взаимоблокировка.

Может ли кто-нибудь объяснить, как тупик происходит в вышеуказанном сценарии?

Заранее спасибо.

  • 0
    С этим кодом не возникнет тупиковая ситуация. Также потребуется другой код, чтобы вызвать тупик.
  • 0
    Если это вызывалось из нескольких потоков, есть ли вероятность тупика?
Показать ещё 5 комментариев
Теги:
multithreading
concurrency
java.util.concurrent
dining-philosopher

5 ответов

3
Лучший ответ

Лучше всего использовать объекты, которые вы используете, с synchronized частями.

Поскольку вы синхронизируете Downloader, вы не знаете, синхронизируются ли другие потоки в Downloader.

Следующий прослушиватель вызывает тупик:

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

Код, который блокирует:

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

Если вы запустите этот код, произойдет следующее:

  1. основной поток достигает updateProgress и aquires замок на Downloader
  2. onProgress метод onProgress MyProgressListener onProgress новый поток t
  3. основной поток достигает t.join();

В этой ситуации основной поток не может обработать до тех пор, пока t будет завершен, но для завершения t основной поток должен будет освободить его блокировку на Downloader, но этого не произойдет, поскольку основной поток не может обработать → Deadlock

  • 0
    Спасибо, Фабиан. Получил ответ
2

Во-первых, напомните, что ключевое слово synchronized, применяемое к классу aa, подразумевает блокировку всего объекта, к которому принадлежит этот метод. Теперь давайте набросим еще одну пару объектов, запускающих тупик:

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);
    // ...
  }
}

Один из способов избежать мертвой блокировки - отложить работу, выполняемую в addListener счет наличия внутренних очередей слушателей, ожидающих добавления/удаления, и периодически Downloader самостоятельно. Это, в конечном счете, зависит от внутренней работы Downloader.run.

1

Вероятно, проблема в этом коде:

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

Когда один поток, который содержит блокировку, вызывает внешний метод, подобный этому (onProgress), тогда вы не можете гарантировать, что реализация этого метода не будет пытаться получить другую блокировку, которая может храниться в разных потоках. Это может вызвать тупик.

0

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

UseDownloader класс UseDownloader вызывается downloadSomething.

По мере продвижения загрузки onProgress метод onProgress. Поскольку это вызвано из синхронизированного блока, мотив Downloader заблокирован. В нашем методе onProgress нам нужно заблокировать наш собственный ресурс, в этом случае lock. Поэтому, когда мы пытаемся синхронизироваться при lock мы держим монитор Downloader.

Если другой поток решил, что загрузка должна быть отменена, она вызовет setCanceled. Эти первые тесты done так, чтобы они синхронизировались на мониторе lock а затем removeListener. Но removeListener требует блокировки Downloader.

Такой тупик трудно найти, потому что это происходит не очень часто.

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

Следующий пример приводит к тупиковой ситуации, потому что MyProgressListener пытается приобрести блокировку Downloader, пока она уже приобретена.

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();
  • 1
    Мониторы Java являются реентерабельными. Получение блокировки дважды на одном и том же потоке работает.

Ещё вопросы

Сообщество Overcoder
Наверх
Меню