¿Por qué los servlets siguen asustando y cómo vivir con ello?
\n\nSi alguna vez has mirado en Stack Overflow buscando una respuesta a la pregunta «¿Cómo funcionan los servlets?», seguramente te topaste con el legendario hilo de miles de visitas. La pregunta que se hace una y otra vez: «¿Cuántas instancias de un servlet se crean? ¿Cómo compartir datos entre peticiones? ¿Y por qué mi variable se comporta como si estuviera loca?»
\n\nSeamos honestos: los servlets no son ciencia espacial, pero hay suficientes trampas. Especialmente cuando se trata de concurrencia y variables compartidas. En este artículo no solo repetiremos la documentación. Tomaremos un ejemplo real de la vida, lo analizaremos en detalle y mostraremos cómo no pisar el rastrillo. ¡Allá vamos!
\n\nCómo llega un servlet a la memoria: instanciación y ciclo de vida
\n\nEmpecemos con lo simple: ¿cuántas instancias de un servlet se crean? Respuesta: una. Sí, no oíste mal. El contenedor de servlets (Tomcat, Jetty y otros) crea exactamente una instancia de cada servlet para toda la aplicación. Y esa instancia vive hasta que la aplicación se descarga.
\n\nAsí es como se ve un ciclo de vida típico:
\n\n- \n
init()— se llama una vez al cargar el servlet. Aquí inicializas conexiones a la BD, configuraciones y demás. \n service()— se llama por cada petición entrante. Aquí es donde ocurre el procesamiento. \n destroy()— se llama antes de descargar el servlet. Limpias recursos. \n
Punto clave: service() (y sus derivados doGet, doPost) se ejecuta en un hilo separado para cada petición. Es decir, la misma instancia del servlet procesa cientos de peticiones simultáneamente. Y aquí es donde empieza lo interesante.
Ejemplo: un servlet, muchos hilos
\n\n@WebServlet("/counter")\npublic class CounterServlet extends HttpServlet {\n private int counter = 0; // shared variable!\n\n @Override\n protected void doGet(HttpServletRequest req, HttpServletResponse resp) \n throws ServletException, IOException {\n counter++;\n resp.getWriter().write("Counter: " + counter);\n }\n}\n\nSi ejecutas este código y envías 10 peticiones paralelas, no obtendrás los números del 1 al 10 en orden. En su lugar, verás algo como: 2, 5, 3, 8, 1... O incluso counter puede saltarse valores. ¿Por qué? Porque counter++ no es una operación atómica. Dos hilos pueden leer el mismo valor, incrementarlo y escribirlo de vuelta, perdiendo un incremento.
Conclusión: los campos de instancia del servlet son variables compartidas. Son visibles para todos los hilos. Y si no sincronizas el acceso a ellos, obtienes una condición de carrera.
\n\nSesiones: cómo no confundir a los usuarios
\n\nAhora imagina: quieres almacenar datos únicos para cada usuario (carrito de compras, inicio de sesión, configuraciones). Para esto, en Java Servlets existe HttpSession. La sesión se crea para cada nuevo usuario y vive hasta que cierre el navegador o expire el tiempo de espera.
\n\nLa sesión no es memoria compartida. Cada usuario obtiene su propia instancia de sesión. Pero dentro de la sesión puedes almacenar cualquier objeto, y estos serán accesibles solo para ese usuario.
\n\nEjemplo: trabajar con sesión
\n\n@WebServlet("/session-demo")\npublic class SessionServlet extends HttpServlet {\n @Override\n protected void doGet(HttpServletRequest req, HttpServletResponse resp) \n throws ServletException, IOException {\n HttpSession session = req.getSession();\n Integer visitCount = (Integer) session.getAttribute("visitCount");\n if (visitCount == null) {\n visitCount = 0;\n