Deadlock ao chamar dois métodos sincronizados
-
21-12-2019 - |
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.
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:
- o fio principal chega ao
updateProgress
e adquire um bloqueio noDownloader
- o
MyProgressListener
deonProgress
método é chamado e o novo threadt
começou - 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();