This article is participating in “Java Theme Month – Java Swipe Card”, see the activity link for details

ThreadLocal is a private variable in a thread. Each thread can only operate on its own private variable, so it does not cause thread safety problems.

Thread unsafe means that when multiple threads write to the same global variable at the same time (read operations do not involve thread unsafe issues), the result of execution is called thread unsafe if it is inconsistent with the expected result; otherwise, it is called thread safe.

Thread insecurity in the Java language is usually addressed in two ways:

  1. Use a Lock (use synchronized or Lock);
  2. Using ThreadLocal.

The lock is implemented by queuing all global variables one at a time to avoid thread insecurity. For example, when we use the thread-unsafe SimpleDateFormat to format time, if we use locks to solve the thread-unsafe problem, the implementation process would look like this:As you can see from the above picture, locking can solve the problem of thread insecurity, but at the same time, it brings a new problem: the thread needs to be queued to execute when using locks, so it brings a certain performance overhead. However,If you use the ThreadLocal approach, you create a SimpleDateFormat object for each thread to avoid queuing problems, its implementation process is shown in the figure below:

PS: Creating a SimpleDateFormat will also take time and space. If the thread reuse the SimpleDateFormat frequently, using a ThreadLocal will be advantageous. Otherwise, you can consider using a lock.

However, it’s easy to run out of memory when using ThreadLocal, as in the following example.

What is a memory overflow?

Out Of Memory (OOM) refers to useless objects (objects that are no longer used) continue to occupy Memory, or the Memory Of useless objects can not be released in time, resulting in Memory space waste behavior is called Memory leak.

Memory overflow code demonstration

Idea (-xmx50m) : Idea (-xmx50m) : Idea (-xmx50m) : Idea (-xmx50m)The final result looks like this:

PS: Because THE Idea I’m using is community edition, so it may be different from your interface, you just need to click on “Edit Configurations…” Find the “VM Options” option and set the “-xmx50m” parameter.

With Idea configured, let’s implement the business code. In the code we create a large object with an array of 10 MB in it. We store this large object in a ThreadLocal and use the thread pool to perform more than 5 additions. Since we set the maximum running memory to 50 MB, the ideal situation is that after 5 additions, Memory overflow will occur, the implementation code is as follows:

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ThreadLocalOOMExample {
    
    /** * define a class */
    static class MyTask {
        // Create an array of 10 MB (1 MB -> 1024KB -> 1024*1024B)
        private byte[] bytes = new byte[10 * 1024 * 1024];
    }
    
    / / define ThreadLocal
    private static ThreadLocal<MyTask> taskThreadLocal = new ThreadLocal<>();

    // The main test code
    public static void main(String[] args) throws InterruptedException {
        // Create a thread pool
        ThreadPoolExecutor threadPoolExecutor =
                new ThreadPoolExecutor(5.5.60,
                        TimeUnit.SECONDS, new LinkedBlockingQueue<>(100));
        // Make 10 calls
        for (int i = 0; i < 10; i++) {
            // Execute the task
            executeTask(threadPoolExecutor);
            Thread.sleep(1000); }}/** * Thread pool to execute tasks *@paramThreadPoolExecutor Thread pool */
    private static void executeTask(ThreadPoolExecutor threadPoolExecutor) {
        // Execute the task
        threadPoolExecutor.execute(new Runnable() {
            @Override
            public void run(a) {
                System.out.println("Create object");
                // Create object (10M)
                MyTask myTask = new MyTask();
                / / store ThreadLocal
                taskThreadLocal.set(myTask);
                // Set the object to NULL, indicating that the object is no longer in use
                myTask = null; }}); }}Copy the code

The execution results of the above procedures are as follows:You can see from the picture above, when the program execution to fifth add object of memory problems, this is because the set up the operation of the largest memory is 50 m, each cycle will take up 10 MB of memory, plus program can take up a certain amount of memory, so when performing to the fifth add tasks, can appear memory leak problem.

Cause analysis,

The problem and the solution to the memory overflow are relatively simple, but the focus is on “cause analysis,” where we use the memory overflow problem to figure out why ThreadLocal behaves like this. What causes the memory overflow?

To figure this out, we need to start with the ThreadLocal source code, so we first open the source code for the set method (which is used in our example), as follows:

public void set(T value) {
    // Get the current thread
    Thread t = Thread.currentThread();
    // Get the ThreadMap variable according to the thread
    ThreadLocalMap map = getMap(t);
    if(map ! =null)
        map.set(this, value); // Store the content in the map
    else
        createMap(t, value); // Create a map and store the values in the map
}
Copy the code

Thread, ThreadLocalMap, and set ** Each Thread has its own data store, ThreadLocalMap. When threadLocal. set executes, it stores the values in the ThreadLocalMap container. So let’s look at the source code of ThreadLocalMap again:

static class ThreadLocalMap {
    // The array that actually stores the data
    private Entry[] table;
    // The method of storing data
    private void set(ThreadLocal
        key, Object value) {
        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();// Update the value if there is a corresponding key
            if (k == key) {
                e.value = value;
                return;
            }
            // 发现空位插入 value
            if (k == null) {
                replaceStaleEntry(key, value, i);
                return; }}// Insert a new Entry into the array
        tab[i] = new Entry(key, value);
        int sz = ++size;
        // Determine whether to expand the capacity
        if(! cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }/ /... Ignore other source code
}
Copy the code

From the above source code we can see: An Entry[] array is used to store all data in a ThreadMap. An Entry is a pair of keys and values, where the key is the ThreadLocal itself. Value is the value to be stored in a ThreadLocal.

From the above, we can draw a diagram of ThreadLocal related objects that looks like this:That is to say,The referential relationship between them looks like this: Thread – > ThreadLocalMap – > Entry – > Key, Value, so when we use the Thread pool to store objects, because the Thread pool has a long life cycle, so the Thread pool will always hold the Value Value, then the garbage collector can’t recycling Value, As a result, memory is occupied all the time, leading to memory overflow problems.

The solution

The solution to a ThreadLocal memory overflow is simple. We simply need to execute the remove method after using the ThreadLocal to avoid the memory overflow problem, such as the following code:

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class App {

    /** * define a class */
    static class MyTask {
        // Create an array of 10 MB (1 MB -> 1024KB -> 1024*1024B)
        private byte[] bytes = new byte[10 * 1024 * 1024];
    }

    / / define ThreadLocal
    private static ThreadLocal<MyTask> taskThreadLocal = new ThreadLocal<>();

    // Test the code
    public static void main(String[] args) throws InterruptedException {
        // Create a thread pool
        ThreadPoolExecutor threadPoolExecutor =
                new ThreadPoolExecutor(5.5.60,
                        TimeUnit.SECONDS, new LinkedBlockingQueue<>(100));
        // Execute n calls
        for (int i = 0; i < 10; i++) {
            // Execute the task
            executeTask(threadPoolExecutor);
            Thread.sleep(1000); }}/** * Thread pool to execute tasks *@paramThreadPoolExecutor Thread pool */
    private static void executeTask(ThreadPoolExecutor threadPoolExecutor) {
        // Execute the task
        threadPoolExecutor.execute(new Runnable() {
            @Override
            public void run(a) {
                System.out.println("Create object");
                try {
                    // Create object (10M)
                    MyTask myTask = new MyTask();
                    / / store ThreadLocal
                    taskThreadLocal.set(myTask);
                    // Other business code...
                } finally {
                    // Free memorytaskThreadLocal.remove(); }}}); }}Copy the code

The execution results of the above procedures are as follows:From the above results, we can see that we only need to execute the remove method of ThreadLocal in the finally to avoid the memory overflow problem.

Remove the secret

So why is the remove method so powerful? Let’s open the source of remove and see:

public void remove(a) {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if(m ! =null)
        m.remove(this);
}
Copy the code

Remove removes the ThreadLocalMap object from the Thread, so the Thread no longer holds the ThreadLocalMap object. There are no memory overflow problems due to memory usage of ThreadLocalMap.

conclusion

In this article, we use code to demonstrate the memory overflow problem with a ThreadLocal. Memory overflow is not strictly a problem with a ThreadLocal, but rather a problem caused by improper use of a ThreadLocal. To avoid the problem of ThreadLocal memory overruns, simply call the remove method after using the ThreadLocal. However, through the memory overflow problem of ThreadLocal, let us understand the specific implementation of ThreadLocal, so that we can better use ThreadLocal in the future, and better deal with the interview.

Check out the Java Chinese Community for more interesting and informative concurrent programming articles.