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

StackOverflow https://stackoverflow.com//questions/25050574

Вопрос

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 вызывает чужой метод [onProgress], который может получить еще одну блокировку.Поскольку мы получаем две блокировки в неправильном порядке, может возникнуть взаимоблокировка.

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

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

Это было полезно?

Решение

Лучше всего создавать объекты, которые вы используете, с 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 и получает блокировку на Downloader
  2. тот MyProgressListener's onProgress вызывается метод и новый поток t запускается
  3. основная нить достигает t.join();

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

Другие советы

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

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 внутренняя работа, конечно.

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

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

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

Here's a classic example that shows the kind of hard-to-debug problems the author is trying to avoid.

The class UseDownloader is created and downloadSomething is called.

As the download progresses, the onProgress method is called. Since this is called from within the synchronized block, the Downloader motinor is locked. Inside our onProgress method, we need to lock our own resource, in this case lock. So when we are trying to synchronize on lock we are holding the Downloader monitor.

If another thread has decided that the download should be canceled, it will call setCanceled. This first tests done so it synchronized on the lock monitor and then calls removeListener. But removeListener requires the Downloader lock.

This kind of deadlock can be hard to find because it doesn't happen very often.

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

The following example leads to a deadlock because the MyProgressListener tries to acquire the Downloader lock while it's already acquired.

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();
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top