ThreadLocal principle analysis and whether to call the remove method

In the normal development process, if a class is not thread safe, such as SimpleDateFormat, to make this class thread safe in the concurrent process, you can set the variable to a local variable. However, the problem is that objects are created frequently, which will reduce and consume performance and resources; ThreadLocal can be used as thread isolation here. How does ThreadLocal realize the isolation between threads? It will be explained later.

In the previous article, ThreadLocal was used as an example of thread isolation. You can refer to: Implementation of Mybatis multi data source using ThreadLocal

JDK reference type

Before understanding the principle of ThreadLocal, let's first understand the reference types in JDK.

There are four reference types in JDK:

1. Strong reference type: objects created normally belong to strong reference types, such as Object object = new Object(); The object is of strong reference type. If the reference is not actively set to null, the referenced object will not be recycled by GC. Therefore, generally, the object reference used will be set to null after writing a business, in order to assist GC in better garbage collection.

2. Soft reference type: it is weaker than the strong reference type. The memory occupied by these weak references will be recycled before OOM (memory overflow) occurs in the application. The SoftReference class used is as follows:

3. Weak reference type: it is weaker than the soft reference type. It will be recycled by the garbage collector before the next GC recycling. Use the WeakReference class. Examples are as follows:

4. Virtual reference type: This is the weakest reference type in the JDK. Before the object is recycled, it will be moved to a queue and then deleted. This reference type is not used much. The class referenced in the JDK is PhantomReference. This needs to be used in combination with the reference queue and overriding the finalize method. Examples are as follows:

 

For application scenarios, if the current object is dispensable, soft reference or weak reference can be used. For virtual reference, I think it can be used in non GC recycled areas (such as meta space) to monitor the recycling of these areas.

Basic principles

The usage of ThreadLocal will not be discussed here; The bottom layer uses ThreadLocalMap. The bottom layer of this Map uses ThreadLocalMap Entry, ThreadLocalMap this class will have a corresponding separate object in each Thread. The variables in the Thread class are as follows:

    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

This is the focus of thread safety. In the case of concurrency, each thread has its own ThreadLocalMap, so there is no problem of multi-threaded competition for resources. Therefore, if ThreadLocal is used together with threads, it is recommended to reduce the performance overhead of the system and reuse ThreadLocal objects to improve the system performance.

If you look at ThreadLocalMap, you will find that Entry inherits the WeakReference class, indicating that the object created by this class is a weak reference object

static class Entry extends WeakReference<ThreadLocal<?>> {
      /** The value associated with this ThreadLocal. */
      Object value;

      Entry(ThreadLocal<?> k, Object v) {
      super(k);
      value = v;
   }
}
//capacity
private static final int INITIAL_CAPACITY = 16;
//Store attribute values
private Entry[] table;
//Construct initialization table array
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);
}

Let's take a look at the get method: get the current thread, and then get the ThreadLocalMap object of the current thread by calling the getMap method. For the thread just initialized or not found in the ThreadLocalMap, the following setInitialValue method will be used. If the ThreadLocalMap has been initialized, the corresponding Entry object will be obtained directly

    /**
     * Returns the value in the current thread's copy of this
     * thread-local variable.  If the variable has no value for the
     * current thread, it is first initialized to the value returned
     * by an invocation of the {@link #initialValue} method.
     *
     * @return the current thread's value of this thread-local
     */
    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

In the setInitialValue method, you will find the object in the initialization method. If you do not rewrite the initialization method when you create ThreadLocal, you will return to null, or you can call the set method to reset the initialization value of the ThreadLocalMap in the current thread before you use it. You can see the set method separately.

private T setInitialValue() {
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}

Here is a list of getEntry methods. Calculate the hash value according to the ThreadLocal object, find the location of the corresponding table array, and get the object.

private Entry getEntry(ThreadLocal<?> key) {
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key)
        return e;
    else
        return getEntryAfterMiss(key, i, e);
}

problem

Think it's over? No, after using the get method, you should call the remove method to delete it. If it is a thread pool, the data will always exist. If it is not deleted in time, it will cause memory leakage.

Call the remove method to execute the remove method of ThreadLocalMap. In this method, the position of the corresponding Entry array is calculated, and the reference is cleared and the table array is emptied to avoid memory leakage,

public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}
/**
 * Remove the entry for key.
 */
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;
        }
    }
}

However, if ThreadLocal is frequently used, I suggest that it should not be deleted, because if it is frequently used, it will be set to null and will be called again later. If ThreadLocalMap is not found, setInitialValue method will be called to recreate the object and assign value. In a sense, it is the same as local variables, This goes against the original need to reduce performance overhead.

In addition, the Entry inherits the WeakReference class, so the created object will be a weak reference type and will be recycled during GC recycling. If the reference object is recycled, does the value variable value in the Entry still exist;

Continue parsing

Note that here is the corresponding to recycling the reference, that is, ref.get() is null, but the object of the weak reference itself is still there. Let's see how to deal with it in the setInitialValue method and reset it. If the ThreadLocal reference is not obtained, it means that the weak reference has been recycled, and the replacestateentry method will be called here.

private T setInitialValue() {
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);//Reset value
    else
        createMap(t, value);
    return value;
}
//ThreadLocalMap

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();

        if (k == key) {
            e.value = value;
            return;
        }

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

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

In the replacestateentry method, there is a line of code that sets the value variable to null and recreates the Entry object. Therefore, even if the remove deletion method is not called, it will still be set to null after GC.

            // If key not found, put new entry in stale slot
            tab[staleSlot].value = null;
            tab[staleSlot] = new Entry(key, value);

There will be a small problem here. Why use WeakReference instead of SoftReference? Personally, I think it is OK if soft reference is used, if thread pool is used and ThreadLocal will be accessed frequently, but this is not the only case in practical application. Moreover, before OOM, only soft reference objects will be recycled, but the value variable in Entry is still there, It can't really recycle the value. It can't be set to null until the next use, so it's the best choice to use WeakReference.

Keywords: Java Multithreading

Added by eggradio on Tue, 21 Dec 2021 08:59:44 +0200