Rôle des pools de threads en environnement concurrent
Les pools de threads optimisent l'exécution de tâches asynchrones en réutliisant des threads existants, réduisant ainsi la surcharge liée à leur création et destruction. Deux implémentations courantes sont ThreadPoolExecutor du JDK et ThreadPoolTaskExecutor du framework Spring, chacune avec des spécificités d'usage.
ThreadPoolExecutor : implémentation native de Java
Cette classe du package java.util.concurrent implémente l'interface ExecutorService, qui étend Executor pour gérer le cycle de vie des pools de threads. Sa hiérarchie inclut AbstractExecutorService comme classe abstraite intermédiaire.
La création peut se faire via la classe utilitaire Executors, qui offre des méthodes pratiques :
public class FabriquePersonnalisee {
/**
* Génère un pool avec un nombre fixe de threads pour des charges constantes.
* @param capaciteFixe Nombre de threads à maintenir activement
* @return Instance du service d'exécution
*/
public static ExecutorService creerPoolFixe(int capaciteFixe) {
return new ThreadPoolExecutor(capaciteFixe, capaciteFixe,
0L, TimeUnit.SECONDS,
new LinkedBlockingQueue<runnable>());
}
/**
* Crée un pool dynamique adaptant le nombre de threads selon la charge.
* @param seuilParallélisme Niveau de parallélisme cible
* @return Service d'exécution avec ajustement automatique
* @since Java 8
*/
public static ExecutorService creerPoolAdaptatif(int seuilParallélisme) {
return new ForkJoinPool(seuilParallélisme,
ForkJoinPool.defaultForkJoinWorkerThreadFactory,
null, true);
}
}
</runnable>
Alternativement, on peut instancier directement ThreadPoolExecutor pour un contrôle précis des paramètres comme les tailles de pool, les délais d'attente et les files d'attente bloquantes.
ThreadPoolTaskExecutor : intégration Spring
Spring propose cette classe dans le module spring-context pour une gestion simplifiée via l'injection de dépendances. Sa configuration peut être réalisée en XML ou en Java.
Exemple de configuration XML avec des paramètres typiques :
<bean id="executeurTaches" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
<property name="corePoolSize" value="8"/>
<property name="maxPoolSize" value="160"/>
<property name="queueCapacity" value="15"/>
<property name="keepAliveSeconds" value="30"/>
<property name="rejectedExecutionHandler">
<bean class="java.util.concurrent.ThreadPoolExecutor.CallerRunsPolicy"/>
</property>
</bean>
Pour l'injection dans une classe Spring, on utilise les annotations :
@Autowired
@Qualifier("executeurTaches")
private ThreadPoolTaskExecutor executeurPersonnalise;
En configuration Java, la classe peut être définie comme suit :
@Configuration
public class ConfigExecuteur {
@Bean("executeurSpring")
public Executor configurerExecuteur() {
ThreadPoolTaskExecutor executeur = new ThreadPoolTaskExecutor();
int processeursDisponibles = Runtime.getRuntime().availableProcessors();
executeur.setCorePoolSize(processeursDisponibles * 2);
executeur.setMaxPoolSize(processeursDisponibles * 4);
executeur.setQueueCapacity(500);
executeur.setKeepAliveSeconds(60);
executeur.setThreadNamePrefix("spring-worker-");
executeur.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
executeur.initialize();
return executeur;
}
}
Stratégies de rejet et mécanisme d'exécution
Les stratégies de rejet déterminent le comportement lorsque toutes les tâches ne peuvent être traitées. Les options courantes sont :
- AbortPolicy : Lève une exception
RejectedExecutionException. - CallerRunsPolicy : Exécute la tâche dans le thread appelant.
- DiscardOldestPolicy : Abandonne la tâche la plus ancienne en attente.
- DiscardPolicy : Silencieusement ignore la tâche soumise.
Le flux d'exécution des tâches suit un ordre séquentiel :
- Si le nombre de threads actifs est inférieur à la taille du noyau, un nouveau thread est créé.
- Si la file d'attente n'est pas saturée, la tâche y est placée pour exécution future.
- Si le pool n'atteint pas sa capacité maximale, un thread supplémentaire est lancé.
- Sinon, la stratégie de rejet configurée est appliquée.