Question

How Java Servlets Work: Instantiation, Sessions, Instance Variables, and Multithreading

javamultithreadingservletssession-variablesinstance-variables

Question

Suppose I have a web server that hosts multiple Java servlets. To pass information between those servlets, I am using session attributes and servlet instance variables.

If two or more users send requests to the server, what happens to the session data?

  • Is the session data shared by all users?
  • Or does each user get a different session?
  • If each user gets a different session, how does the server distinguish one user from another?

I also have a related question about servlet instantiation.

If n users access the same servlet, is that servlet instantiated only once when the first request arrives, or is a separate servlet object created for each user?

In other words, what happens to servlet instance variables when multiple users access the same servlet at the same time?

Short Answer

By the end of this page, you will understand that a Java servlet is typically instantiated once by the servlet container and then used to serve many requests concurrently. You will also learn that session data is usually separate for each user, while instance variables belong to the shared servlet object and can therefore be accessed by multiple threads at the same time. This distinction is essential for writing correct, thread-safe servlet code.

Concept

In Java web applications, a servlet is a class managed by a servlet container such as Tomcat or Jetty. The container is responsible for:

  • creating the servlet object
  • initializing it
  • routing HTTP requests to it
  • managing sessions
  • handling concurrency

One servlet instance, many requests

For a standard servlet, the container usually creates one instance of the servlet class for a given servlet definition. That single object is then reused for many incoming requests.

That means this is the common lifecycle:

  1. The container creates the servlet instance.
  2. It calls init() once.
  3. For each request, it calls service(), which dispatches to doGet(), doPost(), and so on.
  4. When the application shuts down, it may call destroy().

Because the same servlet object handles many requests, servlet instance variables are shared across requests and users.

Sessions are per user

A session is different. Session data is not stored in the servlet object itself. It is stored by the container in an HttpSession object.

In most cases:

  • each user gets a separate session
  • session attributes belong to that user’s session
  • one user cannot automatically see another user’s session data

For example:

HttpSession session = request.getSession();
session.setAttribute("username", "alice");

That username value is stored in Alice's session, not globally for all users.

How the server knows which session belongs to which user

The container usually tracks the session using a session ID:

  • commonly stored in a cookie such as JSESSIONID
  • sometimes passed in the URL if cookies are disabled

When the browser sends the next request, it includes the session ID. The server uses that ID to look up the correct HttpSession object.

Why this matters

This distinction is critical:

  • local variables inside doGet() or doPost() are usually safe because each thread has its own stack
  • session attributes are user-specific, but can still require care if the same user sends concurrent requests
  • instance variables are shared and can cause race conditions if mutated without proper synchronization

A lot of beginner servlet bugs come from putting per-request or per-user data into servlet instance fields.

Mental Model

Think of a servlet like a single restaurant kitchen station.

  • The servlet object is the station itself.
  • The instance variables are items placed on the shared counter at that station.
  • The requests are food orders arriving from many customers.
  • The threads are cooks working at the same station at the same time.
  • The session is each customer’s personal order ticket.

If a cook writes customer-specific information on the shared counter, another cook may accidentally read or overwrite it. That is what happens when you store request-specific or user-specific data in servlet instance variables.

But if each customer has their own ticket stored separately, the kitchen can keep orders apart. That is similar to using HttpSession for per-user data.

So:

  • instance variables = shared space
  • session attributes = customer-specific storage
  • method local variables = temporary notes used by one cook for one order
Take Quiz

Syntax and Examples

The core servlet concepts usually look like this:

import java.io.IOException;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;

public class VisitServlet extends HttpServlet {

    private int totalVisits = 0; // shared across all users and requests

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

        HttpSession session = request.getSession();

        Integer userVisits = (Integer) session.getAttribute("userVisits");
        if (userVisits == null) {
            userVisits = 0;
        }
        userVisits++;
        session.setAttribute("userVisits", userVisits);

        totalVisits++;

        response.setContentType("text/plain");
        response.getWriter().println("Your visits: " + userVisits);
        response.getWriter().println("Total visits: " + totalVisits);
    }
}

What this example shows

  • userVisits is stored in the session, so it is usually different for each user.
  • totalVisits is an instance variable, so it is shared by everyone.
  • totalVisits++ is not thread-safe. Multiple requests at the same time can corrupt the value.

Safer approach

Keep request-specific values in local variables and user-specific values in the session:

@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {

    String productId = request.getParameter("productId"); // local to this request

    HttpSession session = request.getSession();
    session.setAttribute("lastViewedProduct", productId); // per user session

    response.getWriter().println("Last viewed product saved in your session.");
}

Avoid this pattern

public class BadServlet extends HttpServlet {
    private String currentUser;

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws IOException {
        currentUser = request.getParameter("username");
        response.getWriter().println("Hello " + currentUser);
    }
}

This is broken because currentUser is shared by all requests. Two users hitting the servlet at nearly the same time can overwrite each other’s data.

Step by Step Execution

Consider this servlet:

public class CounterServlet extends HttpServlet {
    private int sharedCounter = 0;

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws IOException {

        HttpSession session = request.getSession();

        Integer sessionCounter = (Integer) session.getAttribute("sessionCounter");
        if (sessionCounter == null) {
            sessionCounter = 0;
        }

        sessionCounter++;
        session.setAttribute("sessionCounter", sessionCounter);

        sharedCounter++;

        response.getWriter().println(
            "Session counter = " + sessionCounter + ", shared counter = " + sharedCounter
        );
    }
}

Now imagine this sequence:

Request 1: User A visits

  • The container creates the servlet instance if it does not already exist.
  • sharedCounter starts at 0.
  • User A gets a session, for example with ID ABC123.
  • sessionCounter for A is null, so it becomes 0.
  • sessionCounter++ makes it 1.
  • sharedCounter++ makes it 1.
  • Output might be:
Session counter = 1, shared counter = 1

Request 2: User A visits again

  • Same servlet instance is used.
  • Browser sends session ID ABC123.
  • Server finds User A’s existing session.
  • sessionCounter is now 1 for A.
  • It becomes 2.
  • sharedCounter becomes 2.
  • Output might be:
Session counter = 2, shared counter = 2

Request 3: User B visits

  • Same servlet instance is still used.
  • User B has a different session ID, maybe XYZ999.
  • User B’s sessionCounter starts as null.
  • It becomes 1.
  • sharedCounter becomes 3.
  • Output might be:
Session counter = 1, shared counter = 3

Key observation

  • User A and User B have different session counters.
  • Both users share the same sharedCounter because it is an instance field on the single servlet object.

Concurrency warning

If User A and User B hit the servlet at exactly the same time, the increments to sharedCounter can overlap. Without synchronization or atomic types, the result may be wrong.

Real World Use Cases

Where sessions are used

Sessions are commonly used for per-user state, such as:

  • login status
  • shopping cart contents
  • multi-step form progress
  • user preferences like theme or locale
  • temporary data between pages

Example:

session.setAttribute("cartItemCount", 3);
session.setAttribute("loggedInUser", user);

Where servlet shared state may appear

Shared servlet state is less common for mutable data, but you may see it used for:

  • read-only configuration loaded in init()
  • references to shared services
  • caches managed safely through dedicated components
  • counters or metrics handled using thread-safe classes

Example of a safer shared object:

private final java.util.concurrent.atomic.AtomicInteger totalRequests = new AtomicInteger();

In real applications

Developers usually avoid storing changing business data directly in servlet instance fields. Instead, they use:

  • databases
  • services managed by dependency injection frameworks
  • session storage for per-user temporary state
  • application-wide context only for shared configuration
Take Quiz

Real Codebase Usage

In real servlet-based applications, developers typically follow a few practical rules.

1. Use local variables for request data

Anything derived from the current request should usually stay inside the method:

String email = request.getParameter("email");

This avoids shared-state bugs.

2. Use session attributes for user-specific state

A logged-in user, a cart, or a wizard step often belongs in the session:

request.getSession().setAttribute("loggedInUserId", userId);

3. Use guard clauses for validation

Servlets often reject invalid input early:

String id = request.getParameter("id");
if (id == null || id.isBlank()) {
    response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Missing id");
    return;
}

4. Avoid mutable servlet fields

Instead of this:

private String currentOrderId;

developers prefer:

  • request parameters
  • session attributes
  • database lookups
  • service-layer state management

5. Use thread-safe helpers for shared counters or caches

If shared mutable state is absolutely necessary, use tools like:

  • AtomicInteger
  • concurrent collections
  • synchronization where appropriate

Example:

private final java.util.concurrent.atomic.AtomicInteger totalRequests = new java.util.concurrent.atomic.AtomicInteger();

@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
        throws IOException {
    int current = totalRequests.incrementAndGet();
    response.getWriter().println("Total requests: " + current);
}

6. Keep servlets thin

In larger codebases, servlets often only:

  • read request data
  • validate input
  • call a service class
  • write the response

This reduces the chance of mixing request state, session state, and shared application state in unsafe ways.

Common Mistakes

1. Storing user-specific data in instance variables

Broken example:

public class ProfileServlet extends HttpServlet {
    private String username;

    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws IOException {
        username = request.getParameter("username");
        response.getWriter().println(username);
    }
}

Why it is wrong

username is shared by all requests. One user can overwrite another user’s value.

Better approach

String username = request.getParameter("username");

or, if it must persist for a user:

request.getSession().setAttribute("username", username);

2. Assuming sessions are shared across all users

Beginners sometimes think HttpSession is global. It is not. It is generally tied to one client session.

3. Assuming one servlet object per user

A servlet is typically not created once per user. One instance usually serves many users.

4. Forgetting thread safety

Broken example:

private int count = 0;

protected void doGet(HttpServletRequest request, HttpServletResponse response)
        throws IOException {
    count++;
    response.getWriter().println(count);
}

Problem

count++ is not atomic. Concurrent requests can produce incorrect values.

Safer version

private final java.util.concurrent.atomic.AtomicInteger count = new java.util.concurrent.atomic.AtomicInteger();

protected void doGet(HttpServletRequest request, HttpServletResponse response)
        throws IOException {
    response.getWriter().println(count.incrementAndGet());
}

5. Putting large or sensitive data in the session without care

Sessions consume memory. Also, sensitive data should be minimized and handled carefully.

6. Ignoring that one user can make concurrent requests too

Even session data may need care. A single browser can trigger multiple parallel requests, so session attributes are not automatically safe from all race conditions.

Comparisons

ConceptScopeShared Between Users?LifetimeThread-Safety Concerns
Local variable in doGet() / doPost()Current method callNoOne requestUsually safe
Instance variable in servletServlet objectYesAs long as servlet livesYes
HttpSession attributeOne user sessionUsually noUntil session expires or is invalidatedPossible with concurrent requests from same user
ServletContext attributeWhole applicationYesApplication lifetimeYes

Servlet instance vs session

FeatureServlet Instance VariableSession Attribute
Stored where?In the servlet objectIn HttpSession
Per user?NoYes, usually
Shared globally?Yes, for that servlet instanceNo
Good for user cart/login state?NoYes
Needs thread-safety?YesSometimes

init() vs doGet() / doPost()

MethodCalled WhenTypical Use
init()Once when servlet is initializedLoad config, create shared resources
doGet() / doPost()On every requestHandle request-specific logic

Session vs cookie

FeatureSessionCookie
Stored on server?YesNo
Stored in browser?Session ID usually isYes
Good for larger user state?YesUsually no
Easy to tamper with directly?Session data no, cookie contents yesYes
Take Quiz

Cheat Sheet

Quick rules

  • A servlet is usually instantiated once per servlet definition.
  • The container uses the same servlet instance for many requests.
  • doGet() and doPost() may run concurrently in different threads.
  • Instance variables are shared across users and requests.
  • Local variables are per request.
  • Session attributes are usually per user.
  • The server identifies sessions using a session ID such as JSESSIONID.

Safe places to store data

Per request

String id = request.getParameter("id");

Per user session

request.getSession().setAttribute("cart", cart);

Shared read-only config

private String apiBaseUrl;

@Override
public void init() {
    apiBaseUrl = getServletConfig().getInitParameter("apiBaseUrl");
}

Avoid

private String currentUser;
private int requestCount;

unless you truly want shared state and handle thread safety correctly.

Thread-safe counter example

private final java.util.concurrent.atomic.AtomicInteger requestCount = new java.util.concurrent.atomic.AtomicInteger();

Session basics

HttpSession session = request.getSession();
session.setAttribute("name", "Alice");
String name = (String) session.getAttribute("name");

Important edge case

A session is per user, but the same user can still send multiple requests at the same time. Do not assume session updates are automatically synchronized.

FAQ

Is a Java servlet created once or once per user?

Usually once per servlet definition, not once per user. The same servlet object typically handles many users.

Are servlet instance variables shared between users?

Yes. Instance variables belong to the shared servlet object, so all requests can access them.

Is HttpSession shared by all users?

No. Each user normally gets a separate session identified by a session ID.

How does the server know which session belongs to which user?

Usually through a session ID sent by the browser, often in a JSESSIONID cookie.

Are local variables inside doGet() thread-safe?

They are usually safe because each request has its own method call stack.

Can session attributes have concurrency issues?

Yes. If the same user sends multiple requests at the same time, session data can still be updated concurrently.

Should I store counters in servlet instance fields?

Only if you intentionally want shared state and use thread-safe tools like AtomicInteger.

Where should I store logged-in user data?

Usually in the session, or in a security framework designed for authentication state.

Take Quiz

Mini Project

Description

Build a small servlet that tracks two kinds of counts: how many times the current user has visited the page and how many total requests the servlet has handled across all users. This project demonstrates the difference between session-scoped data and shared servlet state, and shows how to make shared counters thread-safe.

Goal

Create a servlet where each user sees their own visit count, while the application also tracks a safe global request count.

Requirements

  • Create a servlet that responds to GET requests.
  • Store a per-user visit count in the session.
  • Store a global request count using a thread-safe shared variable.
  • Return both counts in the response.
  • Ensure the global counter is safe under concurrent access.
Take Quiz

Keep learning

Related questions

View more