Ok, non sono riuscito prima per via di miei impegni vari ma ecco un esempio più valido.
Innanzitutto un @Service con un metodo @Async che esegue un task.
package xyz;
import java.math.BigInteger;
import java.util.concurrent.Future;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.AsyncResult;
import org.springframework.stereotype.Service;
@Service
public class MyService {
private static final Logger logger = LoggerFactory.getLogger(MyService.class);
@Async
public Future<Void> myAsyncTask(String user) {
logger.info("User {}: INIZIO TASK", user);
try {
for (int i = 0; i < 10; i++) {
logger.info("User {}: sleep 4sec", user);
Thread.sleep(4000);
logger.info("User {}: calcoli", user);
BigInteger bi = BigInteger.TEN;
for (int n = 0; n < 1000000; n++) {
if (n % 10000 == 0 && Thread.interrupted()) {
throw new InterruptedException("Calcolo interrotto");
}
bi = bi.multiply(BigInteger.TWO);
}
logger.info("User {}: Calcolato valore {} bit", user, bi.bitLength());
}
} catch (InterruptedException e) {
logger.info("User {}: il task è stato interrotto!", user);
} finally {
logger.info("User {}: FINE TASK", user);
}
return new AsyncResult<>(null);
}
}
Quello sopra è solo un ESEMPIO. Esegue 10 volte due parti, una di sleep, una di calcoli puri. Lo sleep è perfettamente "interrompibile", è ben documentato nel javadoc di sleep. Le computazioni "pure" NO, non sono automaticamente interrompibili. Sei tu che devi testare "ogni tanto" se c'è la interruzione. Ogni quanto ... dipende. Ragionevolmente, che il test del interrupt non sia troppo pesante rispetto al resto del ciclo ma che sia comunque sufficientemente responsivo alla interruzione.
Sulla mia macchina quel ciclo di calcolo impiega in tutto circa 30 secondi. Testando ogni 10000 cicli, risulta quindi che è indicativamente responsivo sui 0,3 secondi, quindi ragionevole.
Ora una classe UserTask, che è altamente riutilizzabile, NON è legata ad un task specifico.
package xyz;
import java.util.concurrent.Future;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.function.Supplier;
public class UserTask<T> {
private final String user;
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private Future<T> future;
public UserTask(String user) {
this.user = user;
}
public String getUser() {
return user;
}
public void startTask(Supplier<Future<T>> futureSupplier) {
if (lock.writeLock().tryLock()) {
try {
if (future != null && !future.isDone()) {
throw new IllegalStateException("Il task è già in esecuzione");
}
future = futureSupplier.get();
} finally {
lock.writeLock().unlock();
}
} else {
throw new IllegalStateException("Il task non può essere avviato in questo momento");
}
}
public void stopTask(boolean mayInterruptIfRunning) {
if (lock.writeLock().tryLock()) {
try {
if (future != null) {
future.cancel(mayInterruptIfRunning);
}
} finally {
lock.writeLock().unlock();
}
} else {
throw new IllegalStateException("Il task non può essere stoppato in questo momento");
}
}
}
Nota che UserTask "incapsula" il Future e come puoi vedere non è esposto all'esterno. Questa è una buona cosa in generale.
Ho usato anche io un ReentrantReadWriteLock ma per un motivo più specifico: perché ha il tryLock().
La questione importante è: se un thread A sta facendo lo startTask e un secondo thread B nel medesimo istante vuole fare la stessa cosa per lo stesso user, cosa dovrebbe succedere?
Ci sono due (almeno) soluzioni: il thread B sta bloccato in attesa di acquisire il lock, oppure la richiesta fallisce subito.
La mia scelta è stata la seconda. Se il lock è già acquisito, tryLock() restituisce false e quindi si lancia l'eccezione per dire che non puoi fare l'azione in quel preciso momento.
Nota che l'interno del try è "atomico" ... DEVE essere atomico. Il concetto "se non è (già) in esecuzione, lo avvio" è una operazione "composta", va fatta atomicamente. Per questo serve comunque un lock. Idem per il try nel stopTask.
Il "difetto" purtroppo di Future è che se fai cancel, NON puoi attendere la terminazione del task. Il task potrebbe non essere (subito o affatto) responsivo, quindi potrebbe metterci del tempo a terminare materialmente.
Con il solo Future purtroppo non è risolvibile, servirebbe un ulteriore meccanismo di sincronizzazione che però dovrebbe essere più "invasivo" sul task.
Ora l'ultima parte, un @Service che gestisce i task per i vari user.
package xyz;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class MyTasksService {
@Autowired MyService myService;
private final Map<String, UserTask<Void>> userTasks = new ConcurrentHashMap<>();
public void startMyAsyncTask(String user) throws InterruptedException {
UserTask<Void> userTask = getUserTask(user);
userTask.startTask(() -> myService.myAsyncTask(user));
}
public void stopMyAsyncTask(String user) throws InterruptedException {
UserTask<Void> userTask = getUserTask(user);
userTask.stopTask(true);
}
private UserTask<Void> getUserTask(String user) {
return userTasks.computeIfAbsent(user, UserTask::new);
}
}
Qui ho usato un ConcurrentHashMap, che è già di suo altamente "concorrente". Grazie al computeIfAbsent, non c'è bisogno di sincronizzazione esplicita. In pratica mantengo una mappa di user->UserTask. Ogni user ha il SUO personale oggetto UserTask, che inizialmente viene creato con il Future null, ovviamente.
Il computeIfAbsent è atomico, per uno user X: o dà un UserTask già esistente, o ne crea uno nuovo. Non può fare casini del tipo creare 2 UserTask per lo stesso user se ci sono due richieste concorrenti.
Nota che myAsyncTask() è invocato sul MyService "iniettato". Quello che è iniettato è il proxy (verso il bean MyService) che ha la macchineria per gestire l'@Async, quindi funziona asincrono come ci si aspetta (se abilitato nella applicazione con @EnableAsync, ovviamente).
Osserva come sia tutto semplice, minimale e pulito. Non ci sono if, else, if else if ecc... come hai fatto tu nel tuo esegui. Quello è "fumoso", come dicevo prima.
Ah, ho usato <Void> per evitare alcune grane con i wildcard <?> che non sto ora a spiegare. Di fatto non cambia niente, il null passato al AsyncResult comunque non viene usato.
P.S. ti prego, concludi questa esercitazione. Dopodiché prendi:
"Il Nuovo Java" - Claudio De Sio Cesari - HOEPLI. O uno dei precedenti sempre di De Sio Cesari.