bug caused by improper operation of ThreadLocal


The project is a simple web project. The multi-user login merchant management system uses ThreadLocal to cache the login user's information (duid, user's unique id)

bug description

When the test environment has landed many times, the data detected by the query interface is sometimes absent.

Troubleshooting process

The log is uniquely identified by the merchant id and the user's duid (there are too many test environment logs) so that grep can find that the data and log are sometimes absent after troubleshooting, and duid is sometimes right and sometimes wrong during troubleshooting, so duid becomes a breakthrough. I found the duid data cached by the interceptor, but I found that there was no problem with the cache of the interceptor. Compared with the interceptors of other projects, a problem is found. One method of the interceptor is not rewritten and the data of the local thread is not remove d

    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        super.afterCompletion(request, response, handler, ex);

Add this and the bug will be solved.


Why are threadlocal data disordered (overwritten)?

A diagram is drawn to represent the internal structure of ThreadLocal.

ThreadLocalMap is actually used inside ThreadLocal to cache data.

An entry is an object, which can be understood as a key value pair.

ThreadLocalMap uses Entry [] internally to store objects.

So far, we have not analyzed the source code, but it does not prevent us from deriving the cause of the problem according to the results and bold text.

If we simply understand ThreadLocalMap as HashMap, is the problem obvious?

Take the current thread as the key and the login user data as the value. When the thread remains unchanged, the user data changes. Is this possible?

be on the cards.

There should be a theory (personal): the server only recognizes the request thread and does not recognize the request data

Why do you say that?

For example, if you log in two accounts before and after on the same browser, you must log in to the later account. The server recognizes the request thread rather than the account password.

Code simulation bug process

public class TestMain {
    public void test() {

        final ThreadLocal<UserCacheVO> local = new ThreadLocal<>();
        final UserCacheVO vo1 = new UserCacheVO();
        UserCacheVO vo2 = new UserCacheVO();
UserCacheVO(phone=yyygyjbjh, duid=xxxx, userInfoMap=null)
Process finished with exit code 0

Code flow: the original business requirement is to use vo1 data to db query results, and the results can be found normally. At this time, I use vo2 data to query again, and I can't find the results (the data has been overwritten)

Corresponding page process: page login, interceptor cache data, query results, normal page display; After logging in with a different account, the interceptor caches the data and overwrites the data of the previous request thread, resulting in duid coverage of the data. At this time, the query result is not the business result we want. If you use merchant ID + duid to query the data in the server, you will find that without this log, there will be an inexplicable bug.

Code flow after bug modification: page login, interceptor cache data, query results, interceptor remove cache, and normal page display.

Note: the login module is a separate service, and the login service is called directly by the front end. If you log in correctly, you will get a ticket for business call

Source code analysis

private void set(ThreadLocal<?> key, Object value) {

    // We don't use a fast path as with get() because it is at
    // least as common to use set() to create new entries as
    // it is to replace existing ones, in which case, a fast
    // path would fail more often than not.

    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);

    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();
				// a key
        if (k == key) {
            e.value = value;

        if (k == null) {
            replaceStaleEntry(key, value, i);

    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)

If ThreadLocal is the same, the Entry is directly overwritten.


org.springframework.web.servlet.handler.HandlerInterceptorAdapter has four methods:


Execute before entering the controller interface


Before calling the DispatcherServlet rendering view (ModelAndView), it seems that there is no view in the front and rear ends.


Callback after request processing is completed, that is, after rendering the view. After executing the controller interface, you can clean up resources.


Called during concurrent execution, it is generally not used

The focus of this bug is that the local thread does not clean up after running out of data, that is, it does not call afterCompletion and DataUserHolder.clear()

Tags: Java bug ThreadLocal

Posted on Wed, 03 Nov 2021 19:39:23 -0400 by slobodnium