Architecture Singleton et Multithread des Servlets
Par défaut, les conteneurs de servlets traitent les requêtes HTTP entrantes en utilisant une architecture à instance unique et multithread. Lorsqu'un serveur web démarre ou reçoit la première requête pour une servlet spécifique, le conteneur charge la classe et n'instancie qu'un seul objet de cette servlet. Ensuite, un pool de threads est utilisé pour gérer les requêtes simultanées.
Mécanisme de distribution des requêtes
Le conteneur s'appuie sur un thread de distribution (Dispatcher Thread) et un pool de threads de travail (Worker Threads). Lorsqu'une requête arrive, le thread de distribution assigne un thread de travail disponible pour exécuter la méthode service() de la servlet. Une fois la requête traitée, le thread de travail est remis dans le pool. Cette approche réduit la surcharge de création d'objets et améliore les temps de réponse. Le conteneur ne se préoccupe pas de savoir si les requêtes ciblent la même servlet ou une autre ; il alloue simplement un thread disponible. Par conséquent, la méthode service() d'une même servlet peut être exécutée de manière concurrente par plusieurs threads.
Modèle de Mémoire Java (JMM) et Sécurité des Threads
Le Modèle de Mémoire Java (JMM) définit comment les threads interagissent avec la mémoire. La mémoire principale contient les variables d'instance partagées, tandis que chaque thread possède sa propre mémoire de travail (cache et pile). Les variables locales sont stockées dans la pile de chaque thread, ce qui les rend intrinsèquement sûres pour le multithreading. En revanche, les variables d'instance (champs de la classe Servlet) sont partagées entre tous les threads de travail, ce qui pose des risques de concurrence si elles sont modifiées.
Analyse des variables et attributs
- Variables locales : Allouées dans la pile du thread, elles sont totalement isolées et sûres.
- Variables d'instance : Partagées dans le tas (heap), elles ne sont pas thread-safe si elles sont modifiées.
- Attributs ServletContext : Partagés à l'échelle de l'application, ils nécessitent une synchronisation stricte lors des opérations de lecture/écriture.
- Attributs HttpSession : Bien que liés à une session utilisateur, plusieurs requêtes d'un même utilisateur (ex: pluseiurs onglets du navigateur) peuvent accéder simultanément à la même session, nécessitant une protection contre les accès concurrents.
- Attributs ServletRequest : Créés à chaque requête, ils sont locaux au thread et donc thread-safe. Il ne faut jamais conserver de référence à cet objet en dehors de la portée de la méthode
service().
Stratégies pour des Servlets Thread-Safe
1. Privilégier les variables locales
La méthode la plus efficace et performante pour garantir la sécurité des threads est d'éviter complètement les variables d'instance pour stocker l'état de la requête. Utilisez des variables locales dans les méthodes de traitement.
@WebServlet("/secure-authentication")
public class AuthenticationServlet extends HttpServlet {
// Aucune variable d'instance pour les données spécifiques à la requête
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
// Variables locales, intrinsèquement thread-safe
String clientId = req.getParameter("clientId");
String sessionToken = generateSecureToken(clientId);
resp.setContentType("text/html;charset=UTF-8");
try (PrintWriter writer = resp.getWriter()) {
writer.println("<h2>Jeton de session : " + sessionToken + "</h2>");
}
}
private String generateSecureToken(String client) {
return UUID.randomUUID().toString() + "-" + client;
}
}
2. Synchronisation des ressources partagées
Si l'utilisation de variables d'instance ou de ressources externes est inévitable, utilisez des blocs synchronisés. Il est crucial de minimiser la portée de la synchronisation pour ne pas bloquer inutilement les autres threads et dégrader le débit de l'application.
@WebServlet("/metrics-tracker")
public class MetricsServlet extends HttpServlet {
private long totalHits = 0;
private final Object metricLock = new Object();
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
long currentHits;
// Synchronisation ciblée uniquement sur la section critique
synchronized (metricLock) {
totalHits++;
currentHits = totalHits;
}
resp.getWriter().println("Nombre total de visites : " + currentHits);
}
}
3. L'interface SingleThreadModel (Obsolète)
Historiquement, l'implémentation de l'interface SingleThreadModel forçait le conteneur à garantir qu'un seul thread exécute la méthode service() à la fois. Cette approche a été dépréciée dans la spécification Servlet 2.4 et finalement supprimée, car elle entraîne une dégradation sévère des performances et une consommation mémoire excessive en créant de multiples instances ou en sérialisatn les requêtes.
Comparaison avec les Frameworks MVC
La gestion du cycle de vie des contrôleurs varie selon les frameworks Java :
- Struts 1 et Spring MVC : Les classes Action ou Controller sont des singletons par défaut. Tout comme les servlets, elles doivent être conçues sans état (stateless) pour être thread-safe.
- Struts 2 : Un nouvel objet Action est instancié pour chaque requête (scope prototype). Cela élimine les problèmes de concurrence au niveau de l'instance, bien que les ressources partagées externes nécessitent toujours une attention particulière.
Cycle de Vie et Conteneurs
Le cycle de vie d'une servlet est entièrement orchestré par le contaneur de servlets :
- Initialisation : Le conteneur instancie la servlet et appelle la méthode
init(). - Service : Pour chaque requête, la méthode
service()est invoquée, déléguant ensuite aux méthodesdoGet(),doPost(), etc. - Destruction : Lors de l'arrêt de l'application, la méthode
destroy()est appelée pour libérer les ressources.
Il est important de distinguer les différents niveaux de conteneurs dans l'écosystème Java :
- Conteneur de Servlets : Gère spécifiquement le cycle de vie des servlets, le mappage des URL et l'exécution multithread.
- Conteneur Web : Fournit les services HTTP de base, gère les connexions réseau et héberge les applications web.
- Serveur d'Application : Offre des fonctionnalités avancées pour l'ensemble de l'écosystème Jakarta EE, incluant la gestion des transactions distribuées, les Enterprise JavaBeans (EJB), et les services de messagerie.