Estou relendo o livro Java Concurrency in Practice e implementando os exemplos de código enquanto estudo. Estou no capítulo sobre cancelamento de tarefas, e encontrei um problema no código de exemplo.
Esse é o código que eu implementei e aonde estou fazendo algumas modificações para experimentar e entender melhor o funcionamento das ideias.
private static final ScheduledExecutorService cancelExec = Executors.newScheduledThreadPool(1);
public static void timedRun(Runnable r, long timeout, TimeUnit unit) throws InterruptedException {
class RethrowableTask implements Runnable {
private volatile Throwable t;
@Override
public void run() {
try {
r.run();
} catch (Throwable t) {
this.t = t;
}
}
void rethrow() {
if (t != null)
throw new RuntimeException(t);
}
}
RethrowableTask task = new RethrowableTask();
final Thread taskThread = new Thread(task);
taskThread.start();
cancelExec.schedule(taskThread::interrupt, timeout, unit);
taskThread.join(unit.toMillis(timeout));
task.rethrow();
}
public static void main(String[] args) throws InterruptedException {
Runnable task = () -> {
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
throw new RuntimeException("Oi");
}
};
try {
timedRun(task, 5, TimeUnit.SECONDS);
} catch (Throwable t) {
t.printStackTrace();
}
TimeUnit.SECONDS.sleep(15);
}
Esse código faz o seguinte:
- A thread principal cria um runnable (que tenta dormir 10 segundos e no final causa uma exceção proposital);
- A thread principal cria uma segunda thread (com o runnable criado no item 1) e dá start nela. Existe uma espécie de embrulho ao redor do runnable original, que tem como único intuito capturar exceptions eventuais e salvar para um throw futuro;
- A thread principal agenda um novo runnable em um serviço de agendamento (
ScheduledExecutorService
) para dar umThread.interrupt()
na thread criada no item 2, que deve ser executado após o timeout estabelecido pelo usuário; - A thread principal dá um
join()
na thread criada no item 2 para aguardar seu término até no máximo o timeout estabelecido pelo usuário; - A thread principal verifica se alguma exception aconteceu no runnable do usuário e a causa novamente, para que o usuário possa saber de sua existência;
Os timers estão definidos como:
- Timeout do usuário para a execução cronometrada: 5 segundos
- Tempo de sleep do runnable para a execução cronometrada: 10 segundos
Logicamente, como o sleep da tarefa a ser executada é maior do que o timeout (10 > 5), o timeout vai acontecer primeiro e a tarefa vai ser interrompida. O método sleep
vai causar uma InterruptedException
, que vai ser capturada pela task. Em seguida (no finally
) uma RuntimeException
é causada propositalmente. Essa exception é capturada pelo catch
da RethrowableTask
, e armazenada em uma variável volatile
para garantir a visibilidade por todas as outras threads.
Acontece que, na hora que a thread principal chama o método task.rethrow()
, a variável t
ainda é null
e a exception não é propagada para o método main. Eu fiz um teste colocando um sleep
entre o join
e o rethrow
, e funcionou.
Parece que essa pequena janela de tempo entre o taskThread.interrupt()
(executado pela thread do pool de agendamento) e o task.rethrow()
é suficiente para causar um mal comportamento do programa. Existe portanto, um erro nessa solução do autor?
Listing original do livro: http://jcip.net/listings/TimedRun2.java