Session Management in Java Servlets

14 Jan 2020

Table of Contents


Inspired by this Brian Goetz article: Are all stateful Web applications broken?

The Servlets API

Core classes: HttpServlet, ServletContext

ServletContext defines a set of methods that a servlet uses to communicate with its servlet container, for example, to get the MIME type of a file, dispatch requests, or write to a log file.

The ServletContext object is contained within the ServletConfig object, which the Web server provides to the servlet when the servlet is initialized.

Session management:

Core class: HttpSession

The server can maintain a session in many ways such as using cookies or rewriting URLs.

Session information is scoped only to the current web application (ServletContext), so information stored in one context will not be directly visible in another.

The HttpSession interface acts as a value store for (key, value) data items relevant to that session.

HttpServletRequest

This extends the ServletRequest interface to provide request information for HTTP servlets.

The servlet container creates an HttpServletRequest object and passes it as an argument to the servlet’s service methods (doGet, doPost, etc).

Scoped Containers

The ServletContext, HttpSession, and HttpRequest objects in the Servlet specification are referred to as scoped containers.

Lifetime of scoped containers:

Container Scope
HttpRequest lifetime of the request
HttpSession lifetime of a session between a user and the application
ServletContext lifetime of the application

For the last 2, getAttribute() and setAttribute() methods may be called at any time by different threads.

When an HTTP request arrives at the servlet container, HttpRequest and HttpResponse objects are created and passed to the service() method of a servlet, in the context of a thread managed by the servlet container.

Servlet containers maintain no affinity between threads and sessions.

How Not to Store Data in a Scope

This is a broken approach for storing related items in a scoped container:

(The code assumes there is already a high score in the session.)

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public PlayerScore getHighScore() {
    ServletContext ctx = getServletConfig().getServletContext();
    PlayerScore hs = (PlayerScore) ctx.getAttribute("highScore");
    PlayerScore result = new PlayerScore();
    result.setName(hs.getName());
    result.setScore(hs.getScore());
    return result;
}

public void updateHighScore(PlayerScore newScore) {
    ServletContext ctx = getServletConfig().getServletContext();
    PlayerScore hs = (PlayerScore) ctx.getAttribute("highScore");
    if (newScore.getScore() > hs.getScore()) {
        hs.setName(newScore.getName());
        hs.setScore(newScore.getScore());
    }
}

This is not thread safe. Two different users may try to get and set the high score at the same time, with unpredictable results: the visibility problem.

Set After Write

One improvement - the “set-after-write” technique:

Java
1
2
3
4
5
6
7
8
9
public void updateHighScore(PlayerScore newScore) {
    ServletContext ctx = getServletConfig().getServletContext();
    PlayerScore hs = (PlayerScore) ctx.getAttribute("highScore");
    if (newScore.getScore() > hs.getScore()) {
        hs.setName(newScore.getName());
        hs.setScore(newScore.getScore());
        ctx.setAttribute("highScore", hs);
    }
}

One of the motivations for this technique is to hint to the container that the value has been changed, so that the session or application state can be resynchronized across instances in a distributed Web application.

It is enough to mitigate visibility problems (i.e. that another player might never see the values updated in updateHighScore()), but it is not enough to address the multiple potential atomicity problems.

Question: Why don’t we just do this:

Java
1
2
3
4
5
6
7
public void updateHighScore(PlayerScore newScore) {
    ServletContext ctx = getServletConfig().getServletContext();
    PlayerScore hs = (PlayerScore) ctx.getAttribute("highScore");
    if (newScore.getScore() > hs.getScore()) {
        ctx.setAttribute("highScore", newScore); // just use newScore!
    }
}

Maybe there are other attributes in PlayerScore that we don’t want to change.

Immutability

The next step is to make HighScore immutable:

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
Public class HighScore {
    public final String name;
    public final int score;

    public HighScore(String name, int score) {
        this.name = name;
        this.score = score;
    }
}

public PlayerScore getHighScore() {
    ServletContext ctx = getServletConfig().getServletContext();
    return (PlayerScore) ctx.getAttribute("highScore");
}

public void updateHighScore(PlayerScore newScore) {
    ServletContext ctx = getServletConfig().getServletContext();
    PlayerScore hs = (PlayerScore) ctx.getAttribute("highScore");
    if (newScore.score > hs.score)
        ctx.setAttribute("highScore", newScore);
}

Atomic References

The final step - use an AtomicReference:

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public PlayerScore getHighScore() {
    ServletContext ctx = getServletConfig().getServletContext();
    AtomicReference<PlayerScore> holder
        = (AtomicReference<PlayerScore>) ctx.getAttribute("highScore");
    return holder.get();
}

public void updateHighScore(PlayerScore newScore) {
    ServletContext ctx = getServletConfig().getServletContext();
    AtomicReference<PlayerScore> holder
        = (AtomicReference<PlayerScore>) ctx.getAttribute("highScore");
    while (true) {
        HighScore old = holder.get();
        if (old.score >= newScore.score)
            break;
        else if (holder.compareAndSet(old, newScore))
            break;
    }
}

Even with a session-scoped container, HttpSession, it is possible for multiple simultaneous requests to happen on the same session (especially with asynchronous calls via ajax, etc).

Implementations

Tomcat

Tomcat’s implementation of ServletContext is org.apache.catalina.core.ApplicationContext.

It uses ConcurrentHashMap for its attributes store.

The source code is here.

Regarding ConcurrentHashMap:

“Retrieval operations (including get) generally do not block, so may overlap with update

operations (including put and remove). Retrievals reflect the results of the most recently

completed update operations holding upon their onset. (More formally, an update operation

for a given key bears a happens-before relation with any (non-null) retrieval for that key

reporting the updated value.)”

Jetty

In Jetty, it appears to also be a ConcurrentHashMap, but wrapped in AtomicReference see here

Perhaps because their use of ConcurrentHashMap predated the addition of atomic operations such as computeIfAbsent()…?

ConcurrentHashMap:

ConcurrentHashMap - since Java 5
putIfAbsent() - since Java 5

computeIfAbsent(), compute(), merge() - since Java 8

Concurrency Clarification

See this SO post:

If a thread puts a value in concurrent hash map then some other thread that retrieves the value for the map is guaranteed to see the values inserted by the previous thread.

This issue has been clarified in “Java Concurrency in Practice” by Joshua Bloch.

Quoting from the text :-

The thread-safe library collections offer the following safe publication guarantees, even if the javadoc is less than clear on the subject:

Placing a key or value in a Hashtable, synchronizedMap or Concurrent-Map safely publishes it to any other thread that retrieves it from the Map (whether directly or via an iterator)…

SO comment:

ConcurrentHashMap.get() is guaranteed to see any previous put(), where “previous” means there is a happens-before relationship between the two as specified by the Java Memory Model. (For the ConcurrentHashMap in particular, this is essentially what you’d expect, with the caveat that you may not be able to tell which happens first if both threads execute at “exactly the same time” on different cores.

Useful article: Understanding the Java Memory Model

Also worth a closer look: Java Memory Model FAQ