background

  1. When I view a tool class, I find that instances of the tool class are frequently created and reclaimed
  2. Although this class is very light, we want to minimize the impact of this tool on the system, considering that it is a basic tool class and this feature needs to be called frequently
  3. The optimization goal is to pool class objects for reuse on a thread-safe basis

Thus, the initial scenario is to use ThreadLocal to hold one object per thread.

However, after refactoring the utility class, the Ali protocol plug-in indicates that “the remove() method should be called at least once” and that it may cause a memory leak.

When I looked at WeakReference, I clearly saw that ThreadLocal was useful for weak references, which should be automatically collected when GC is not used. And this was written by Doug Lea.

The source code to explore

Analyze the source code with the following questions:

  1. How does ThreadLocal allow each thread to hold a copy of its own variables
  2. ThreadLocal uses WeakReference. Why does ali protocol suggest that the remove method should be called at least once? Does it really cause memory leak

Implementation of ThreadLocal

The implementation of ThreadLocal is clever enough to add a unique “hashMap-like structure” of ThreadLocalMap to each thread, where all ThreadLocal variables are stored.

ThreadLocalMap is designed like this:

  1. ThreadLocalMap objects are stored in Thread objects. According to the Java memory model, each Thread has its own working memory. Threads cannot access the working memory of other threads
  2. The ThreadLocalMap structure is similar to the HashMap structure, with an array of entries, which will also be expanded in threshold, hash collisions and solutions
  3. The biggest difference from a HashMap is that this Map Entry does not normally contain both key and value attributes
    • Entry to inheritWeakReference<ThreadLocal<? >>Weak references are used to treat ThreadLocal, the real reference object of a weak reference, as the key of a normal Entry. This means that when used, the object to which the weak reference refers is obtained via ‘entry.get(), and the result of equals is computed
    • Entry contains oneObject valueProperty to save the corresponding variable

ThreadLocal wraps the ThreadLocalMap to create a variable storage area for threads, isolating variables between threads, and reclaiming “Entry keys” during GC. In this case, only the key is reclaimed, but neither entry nor value is.

Several key methods

The hash algorithm

The hash algorithm of ThreadLocalMap is modular hash, that is, the hash value of key (ThreadLocal) is modular of capacity, where capacity is guaranteed to be a power of 2. The solution to the conflict is linear detection, which looks at the entry of the next adjacent position and assigns the value if it is “writable”.

When are available locations?

  1. Entry is null. This entry is not yet in use and can obviously be written
  2. If the key of an entry is null, it indicates that the entry has expired. The key has been collected by GC and can be replaced with its key and value

Note that ThreadLocalMap does not use zip/red-black tree conflict resolution.

ThreadLocal.nextHashCode()

Because ThreadLocal is used as a key and uses a special hash algorithm, the hash generation method is rewritten.

The hash value of each ThreadLocal is accumulated by the step 0x61C88647. Why this number? My personal opinion is that this is a prime number (1640531527), and there is less conflict when modulo powers of 2, even by summation. Some sources explain the dispersion of this value pair hash, although I’m not a big fan of the “golden ratio” theory.

ThreadLocalMap. ExpungeStaleEntry (int staleSlot) on an expired empty entry for operation, this is a private methods, cannot be called directly.

Since linear detection is used to resolve conflicts, subsequent entries may be inserted into the current slot due to hash conflicts. This entry is expired, but if the entry is cleared and not processed, a batch of consecutive slots with the same hash result generated due to hash conflict may be “broken”. When searching for these entries through hash, the corresponding result cannot be found in linear detection, and the size pair is not good.

Therefore, after emptying the data at that particular location, all subsequent consecutive entries are rehashed, which, in plain English, might be like removing elements from an array and moving the subsequent consecutive elements forward to ensure no logical error.

However, I personally think that the processing of this part is not enough. I did not check whether the entries that need rehash are expired. The expired entries could have been cleared directly. In extreme cases, multiple subsequent entries are expired, and you have to rehash multiple times, just like in the extreme case of bubble sort. Fortunately, the hash algorithm is simple enough (fast calculation), the number of entries roughly corresponds to the number of threads (the array is not particularly large), and the hash algorithm is evenly distributed (it is difficult to have very long consecutive non-empty entries). Such extreme cases should be ignored.

In the get, set, and remove methods, this method is invoked directly or indirectly when an entry key that has expired and been reclaimed is encountered. This ensures that many expired entries and entry values can be cleaned regularly even if the key is reclaimed without the remove operation. Of course, there are special cases that cannot be cleaned, such as expired entries before the current one, which the rehash procedure may not check.

conclusion

ThreadLocal simply contains a Map key of type int and encapsulates a tool that uses the key to look up values from the respective threads.

Look back at the problem

The first question

How to implement per-thread variable isolation

Since the first step of the GET method is to get ThreadLocalMap from Thread.currentThread() and value from ThreadLocalMap, isolation is obviously guaranteed (with exceptions).

Will the use of WeakReference still cause memory leaks

Only the key in the entry is a weak reference, but the entry itself and its value are still strong references. If the reference is not released, memory leakage may occur.

The specific causes of memory leaks are analyzed in the following sections.

The new problem

On searching for information, it turned out that the initial problem led to some other problems.

What are the effects of the untuned remove() method other than memory leaks

Since ThreadLocalMap is stored in Thread objects, and Thread pools are widely used in many mainstream frameworks, reusing Thread objects will also reuse their bound ThreadLocalMap. The following code may cause problems:

    Object v = threadLocal.get();
    // Because the thread is overused, the data in the thread was not cleaned up in the last execution process
    if (v == null) {
        v = genFromSomePlace();
        threadLocal.set(v);
    }
Copy the code

Also, be careful to use threadLocal. withInitial(Supplier
supplier) factory method creates a ThreadLocal object. Once the ThreadLocal of different threads uses the same supplier object, there is no need for isolation, as in this case:

// ...
// Counter example, this is actually different threads sharing the same variable
private static ThreadLocal<Obj> threadLocal = ThreadLocal.withIntitial(() -> obj);
// ...
Copy the code

To do this:

// ...
private static ThreadLocal<Obj> threadLocal = ThreadLocal.withIntitial(Obj::new);
// ...
Copy the code

Why not define Entry or value as weak references

ThreadLocal is referenced in memory

Entry is defined as a weak reference: when the GC collects, it is impossible to tell whether it was never written or collected in the first place, and subsequent patching of linear probes cannot be completed.

Value is defined as a weak reference: seems like a good way to do this, why not? Since this is basically the same as defining a key as a weak reference, we can still rely on the weak reference mechanism to clean up, but usually we do not hold a strong reference to a value, only a strong reference to a ThreadLocal object, and a value without a strong reference will be collected by GC, which is not what we expect.

Let’s change the question: why does key use a weak reference instead of a strong reference?

  1. It is generally possible to hold both ThreadLocal and Thread strong references
  2. In some cases, the strong reference to the key breaks, and only weak references are left for the key, which will be reclaimed in the next GC
  3. After the key is reclaimed, methods such as set and GET may trigger the expungeStaleEntry method to clear the entry

Generally, this is the end of online materials, but I want to further explore: under what circumstances will the strong reference of key break?

A strong reference to a key is held by an object in the corresponding child thread or main thread. When the object’s life cycle ends or the object replaces the reference to the key, the strong reference to the key is broken.

Let’s take a comprehensive look at this overdue recycling process:

  1. The child thread uses an object of class A that contains A non-static ThreadLocal variable called A key
public class A {
    private ThreadLocal<Context> local = new ThreadLocal<>();

    public void doSth(a) {
        // Context ctx = ...local.set(ctx); }}Copy the code
  1. The child thread terminates, or the next child thread uses the object A ‘of class A, where the ThreadLocal of A’ also uses the new hash value as key’.
  2. The original object A is unreachable and is collected by GC
  3. The key is reclaimed, but the entry and value corresponding to the key are strongly referenced by Thread.threadLocalMap
  4. There may be cases where both the entry and the value are cleared for recycling through the expungeStaleEntry method

In this case, if weak references are used, it is also possible to clean up ThreadLocalMap through the expungeStaleEntry mechanism;

With strong references, there is no way to clean it up, because ThreadLocalMap alone cannot know whether key holder A is alive or not, and the key itself is strongly referenced by Entry.

What are the best practices for ThreadLocal

As mentioned above, when an intermediate class A is used to hold A non-static ThreadLocal object (key), some invalid entries will be automatically cleaned up through the weak reference mechanism and its own policy.

However, as mentioned in the comment documentation for the ThreadLocal class, ThreadLocal should normally be declared as a private static variable.

I personally think ThreadLocal’s weak reference collection mechanism is just a precautionary measure taken by Josh Bloch and Doug Lea to avoid misuse. If ThreadLocal is declared private static, then there is almost no need for weak reference collection, right?

But declaring them static introduces new problems.

Static ThreadLocal static ThreadLocal static ThreadLocal static ThreadLocal

A threadLocal is essentially a key that fetches a value from different threads

Once a ThreadLocal is declared static, multiple threads will use the same ThreadLocal object as the key, and it is possible that the values of these keys will be present in multiple threads.

Imagine a memory leak when some thread no longer needs to update/use some threadLocal: Many of the values in its threadLocalMap are already in a state that is not needed and can be cleaned up. However, because the corresponding threadLocal (key) is still in use by some threads, these expired values cannot be reclaimed, even if weak references are used, such problems cannot be solved.

Take the picture above for example:

  1. Thread 1 and thread 2 both use threadLocal1 and threadLocal2 and set value
  2. Thread 1 returns the thread pool after using it, but does not call threadlocal1.remove ().
  3. Thread 1 will no longer use threadLocal1, only threadLocal2
  4. Obj1 is still stored in thread 1’s threadLocalMap
  5. Because the static variable threadLocal1 reference is still reachable and will not be reclaimed, thread 1 cannot trigger expungeStaleEntry, and the corresponding entry and value of threadLocal1 cannot be reclaimed, causing a memory leak

The advantage of using the private static modifier is that only a limited number of ThreadLocal objects are used to save the creation of objects and subsequent automatic collection. The disadvantage is that we need to manually call remove to clean up the used slot, otherwise we will have memory leaks.

With weak references, will data stored in ThreadLocal be reclaimed during GC, resulting in NPE during subsequent use?

If you use the static modifier, as long as the static reference does not change, it will definitely not be recycled and can be used safely.

If you don’t use the static modifier, then you have to do your own analysis, and normal use (holding a threadLocal strong reference) will not be reclaimed.

Ps. Using private static final embellishments may be a better choice.

conclusion

In general, improper use of ThreadLocal does run the risk of memory leaks. Routine use should follow the following points:

  1. Use private Static to decorate ThreadLocal objects
  2. Be careful not to pass in the same object to create false isolation when calling threadLocal. withInitial
  3. Save the context into threadLocal before the process starts
  4. It is best not to modify ThreadLocal references
  5. Remove is called at the end of the process to remove data from threadLocal, avoiding memory leaks and thread reuse

Much of the information on the web is not clear about the ThreadLocal memory leak problem and its solution, and most of it is missing the point or even wrong.

Although Josh Bloch and Doug Lea have added a lot of precautions to the ThreadLocal memory leak problem, it’s unfortunately unavoidable for a number of reasons.

supplement

When is it appropriate to use ThreadLocal

  1. Some context information that is needed throughout the process, such as RpcContext, is stored in ThreadLocal in many frameworks
  2. Some thread-unsafe objects, such as SimpleDateFormat and JDBC connections, are expensive to create each time they are stored in ThreadLocal

The resources

ThreadLocal hash algorithm (about 0x61C88647) – Digging gold

Why use 0x61C88647 – Nuggets

What are the benefits of setting ThreadLocal variables to private static? – zhihu

The best practice ThreadLocal | Xu Jingfeng | personal blog

This article moves my blog, welcome to visit!