In-depth understanding of ThreadLocal's "memory overflow"

background

The actual usage scenarios for ThreadLocal have been a bit vague. In the code review, everyone has different opinions on whether ThreadLocal will have a memory leak problem. Therefore, I went online to find out, but found that there are different opinions on the Internet, some say it will lead to memory leaks, some say it will not, it is difficult to find the crystallization of actual combat.

analyze

structure

The internal structure of a concise ThreadLocal class is as follows

public class ThreadLocal<T> {
       static class ThreadLocalMap {
              static class Entry extends WeakReference<ThreadLocal> {
                     Object value;
                     Entry(ThreadLocal k, Object v) {
                           super(k);
                           value = v;
                     }
                     private ThreadLocal.ThreadLocalMap.Entry[] table;
              }
       }
}

 A static inner class ThreadLocalMap is defined in the ThreadLocal class. ThreadLocalMap does not implement the Map interface, but "implements" a Map by itself. A static inner class Entry is defined inside ThreadLocalMap to inherit from WeakReference, and look for the memory of WeakReference - when all When the referenced object no longer has a strong reference in the JVM, the weak reference will be automatically reclaimed after GC.

process

Then, let's take a look at the created process

    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
When the thread calls the set method for the first time, the ThreadLocalMap cannot be obtained, so the ThreadLocalMap is created
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

    ThreadLocalMap(ThreadLocal firstKey, Object firstValue){
       table = new Entry[INITIAL_CAPACITY];
       int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
       table[i] = new Entry(firstKey, firstValue);
       size = 1;
       setThreshold(INITIAL_CAPACITY);
    }

 It can be seen that ThreadLocalMap is created with the current ThreadLocal object as the key, and its internal storage structure is as above. After the key is hashed, the key and value are put into the Entry,

Note that the above t.threadLocals = new ThreadLocalMap(this, firstValue) is actually a member variable of Thread referencing this ThreadLocalMap as follows

public class Thread{
    ThreadLocal.ThreadLocalMap threadLocals = null;
}
 So we can analyze, when Thread finishes running (there is no thread pool):

 

  • This ThreadLocalMap object will be recycled by GC
  • The object pointed to by the member variable table of ThreadLocalMap will be recycled by gc. At this time, note that Entry inherits WeakReference, so the Entry object will also be recycled by gc
  • value as a member variable of Entry will naturally be recycled by gc

in conclusion

In this way, a more rigorous statement is that without using the thread pool, even if the remove method is not called, the "variable copy" of the thread will be reclaimed by gc, that is, there will be no memory leak.

question

1. What about the use of thread pools? Will there be a memory leak problem? I did such a simple little test

    public static void testThreadLocalExist(){
        ExecutorService service = Executors.newSingleThreadExecutor();
        for (int i = 0; i < 10; i++) {
            if(i == 0){
                service.execute(new Runnable() {
                    public void run() {
                        System.out.println("Thread id is " + Thread.currentThread().getId());
                        threadLocal.set("variable");
                    }
                });
            } else if(i > 0){
                service.execute(new Runnable() {
                    public void run() {
                        if("variable".equals(threadLocal.get())){
                            System.out.println("Thread id " + Thread.currentThread().getId() + " got it !");
                        }
                    }
                });
            }
        }
    }

output:
Thread id is 9
Thread id 9 got it !
Thread id 9 got it !
Thread id 9 got it !
Thread id 9 got it !
Thread id 9 got it !
Thread id 9 got it !
Thread id 9 got it !
Thread id 9 got it !
Thread id 9 got it !
 In the above test, I initialized a thread pool with a thread number of 1. In order to ensure that the same thread is obtained from the thread pool every time, it can be seen from this test that when the thread is called again from the thread pool, This "variable copy" can be obtained, that is, memory leaks may occur, but the impact cannot be estimated without actual combat. 2. When using a thread pool, how to avoid memory leaks for safety? In the above test, make a small change
    public static void testThreadLocalExist() {
        ExecutorService service = Executors.newSingleThreadExecutor();
        for (int i = 0; i < 10; i++) {
            if (i == 0) {
                service.execute(new Runnable() {
                    public void run() {
                        System.out.println("Thread id is " + Thread.currentThread().getId());
                        threadLocal.set("variable");
                        threadLocal.remove();
                    }
                });
            } else {
                service.execute(new Runnable() {
                    public void run() {
                        if ("variable".equals(threadLocal.get())) {
                            System.out.println("Thread id " + Thread.currentThread().getId() + " get it !");
                        } else {
                            System.out.println("Thread id " + Thread.currentThread().getId() + " can't get it !");
                        }
                    }
                });
            }
        }
    }
output:
Thread id is 9
Thread id 9 can't get it !
Thread id 9 can't get it !
Thread id 9 can't get it !
Thread id 9 can't get it !
Thread id 9 can't get it !
Thread id 9 can't get it !
Thread id 9 can't get it !
Thread id 9 can't get it !
Thread id 9 can't get it !
  As tested above, on the original basis, the remove method of ThreadLocal is called before the thread runs for the first time, and then the thread is put back into the thread pool, so that when the thread is called again, the "variable copy" no longer exists. When ThreadLocal calls the remove method, it actually calls the remove method of ThreadLocalMap
     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }
 Then take a deep look at the remove method of ThreadLocalMap
     private void remove(ThreadLocal key) {
            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)]) {
                if (e.get() == key) {
                    e.clear();
                    expungeStaleEntry(i);
                    return;
                }
            }
        }
 You can see that after emptying the Entry, the expungeStaleEntry method is called again
       private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;

            // expunge entry at staleSlot
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;

            // Rehash until we encounter null
            Entry e;
            int i;
            for (i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal k = e.get();
                if (k == null) {
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
                    int h = k.threadLocalHashCode & (len - 1);
                    if (h != i) {
                        tab[i] = null;

                        // Unlike Knuth 6.4 Algorithm R, we must scan until
                        // null because multiple entries could have been stale.
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
            return i;
        }
 Some processing is done here to prevent memory leaks. Please pay attention to the red part. Manually assign the value of value to null, so that the next round of gc can recycle the value object. The above content is a personal analysis and test, please refer to the actual situation. If the above analysis feels unreasonable, please point it out and learn together.

Guess you like

Origin http://10.200.1.11:23101/article/api/json?id=326776931&siteId=291194637