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], который может получить другую блокировку. Поскольку мы приобретаем два замка без правильного порядка, может возникнуть взаимоблокировка.
Может ли кто-нибудь объяснить, как тупик происходит в вышеуказанном сценарии?
Заранее спасибо.
Лучше всего использовать объекты, которые вы используете, с 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();
Если вы запустите этот код, произойдет следующее:
updateProgress
и aquires замок на Downloader
onProgress
метод onProgress
MyProgressListener
onProgress
новый поток t
t.join();
В этой ситуации основной поток не может обработать до тех пор, пока t
будет завершен, но для завершения t
основной поток должен будет освободить его блокировку на Downloader
, но этого не произойдет, поскольку основной поток не может обработать → Deadlock
Во-первых, напомните, что ключевое слово 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
.
Вероятно, проблема в этом коде:
for (ProgressListener listener: listeners)
listener.onProgress(n);
Когда один поток, который содержит блокировку, вызывает внешний метод, подобный этому (onProgress), тогда вы не можете гарантировать, что реализация этого метода не будет пытаться получить другую блокировку, которая может храниться в разных потоках. Это может вызвать тупик.
Вот классический пример, который показывает, какие трудно отлаживаемые проблемы пытается избежать автор.
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);
}
}
}
}
Следующий пример приводит к тупиковой ситуации, потому что 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();