ThreadLocal - in depth analysis of interview questions

Posted by chadtimothy23 on Wed, 26 Jan 2022 10:58:29 +0100

Original address

ThreadLocal brief introduction

ThreadLocal is a local thread copy variable utility class. It is mainly used to map the private thread and the copy object stored by the thread. The variables between threads do not interfere with each other. In high concurrency scenarios, stateless calls can be realized. It is especially suitable for scenarios where each thread depends on different variable values to complete operations.

Starting from the data structure, the following figure shows the internal structure of ThreadLocal

From the above structure diagram, we have seen the core mechanism of ThreadLocal:

  1. There is a Map inside each Thread.
  2. The Map stores the thread local object (key) and the variable copy (value) of the thread.
  3. However, the Map inside the Thread is maintained by ThreadLocal, which is responsible for obtaining and setting the variable value of the Thread from the Map.

Therefore, for different threads, when obtaining the copy value each time, other threads cannot obtain the copy value of the current thread, forming the isolation of copies without interference with each other.

The Map inside the Thread is described in the class as follows:

public class Thread implements Runnable {
    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;
}

Deep parsing ThreadLocal

ThreadLocal class provides the following core methods:

public T get()
public void set(T value)
protected T initialValue()
public void remove()
  • The get() method is used to get the copy variable value of the current thread.
  • The set() method is used to save the copy variable value of the current thread.
  • initialValue() is the initial copy variable value of the current thread.
  • The remove() method removes the copy variable value of the current future.

get() method

/**
 * 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)
            return (T)e.value;
    }
    return setInitialValue();
}

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

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;
}

protected T initialValue() {
    return null;
}

Steps:

  1. Gets the ThreadLocalMap object threadLocals of the current thread.
  2. Get the K-V Entry node of the thread store from the map.
  3. Get a copy of the stored Value from the Entry node and return the Value.
  4. If the map is empty, the initial value null will be returned, that is, the copy of the thread variable is null. When using it, you should pay attention to judge the NullPointerException.

set() method

/**
 * Sets the current thread's copy of this thread-local variable
 * to the specified value.  Most subclasses will have no need to
 * override this method, relying solely on the {@link #initialValue}
 * method to set the values of thread-locals.
 *
 * @param value the value to be stored in the current thread's copy of
 *        this thread-local.
 */
public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

Steps:

  1. Gets the member variable map of the current thread.
  2. If the map is not empty, put the ThreadLocal and the new value copy into the map again.
  3. If the map is empty, the member variable ThreadLocalMap of the thread is initialized and created, and copies of ThreadLocal and value are put into the map.

remove() method

/**
 * Removes the current thread's value for this thread-local
 * variable.  If this thread-local variable is subsequently
 * {@linkplain #get read} by the current thread, its value will be
 * reinitialized by invoking its {@link #initialValue} method,
 * unless its value is {@linkplain #set set} by the current thread
 * in the interim.  This may result in multiple invocations of the
 * <tt>initialValue</tt> method in the current thread.
 *
 * @since 1.5
 */
public void remove() {
 ThreadLocalMap m = getMap(Thread.currentThread());
 if (m != null)
     m.remove(this);
}

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

The remove method is relatively simple and will not be repeated.

ThreadLocalMap

ThreadLocalMap is the internal class of ThreadLocal. It does not implement the Map interface. It implements the function of Map in an independent way, and its internal Entry is also implemented independently.

In ThreadLocalMap, Entry is also used to save K-V structure data. However, the key in the Entry can only be ThreadLocal object, which is limited by the construction method of the Entry.

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

    Entry(ThreadLocal k, Object v) {
        super(k);
        value = v;
    }
}

Entry inherits from WeakReference (weak reference, the life cycle can only survive until the next GC), but only Key is of weak reference type, and Value is not weak reference.

Member variables of ThreadLocalMap:

static class ThreadLocalMap {
    /**
     * The initial capacity -- MUST be a power of two.
     */
    private static final int INITIAL_CAPACITY = 16;

    /**
     * The table, resized as necessary.
     * table.length MUST always be a power of two.
     */
    private Entry[] table;

    /**
     * The number of entries in the table.
     */
    private int size = 0;

    /**
     * The next size value at which to resize.
     */
    private int threshold; // Default to 0
}

How to resolve Hash conflict

The biggest difference between ThreadLocalMap and HashMap is that the structure of ThreadLocalMap is very simple and there is no next reference. That is to say, the way to solve Hash conflicts in ThreadLocalMap is not a linked list, but a linear detection. The so-called linear detection is to determine the position of elements in the table array according to the hashcode value of the initial key, If it is found that there are elements with other key values occupied in this position, use a fixed algorithm to find the next position with a certain step length, and judge in turn until the position that can be stored is found.

ThreadLocalMap solves Hash conflicts by simply increasing or decreasing the step size by 1 to find the next adjacent location.

/**
 * Increment i modulo len.
 */
private static int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0);
}

/**
 * Decrement i modulo len.
 */
private static int prevIndex(int i, int len) {
    return ((i - 1 >= 0) ? i - 1 : len - 1);
}

Obviously, ThreadLocal map uses linear detection to solve Hash conflicts, which is inefficient. If a large number of different ThreadLocal objects are put into the map and send conflicts, or secondary conflicts occur, it is inefficient.

Therefore, the good advice here is: only one variable is stored in each thread. In this way, the keys of all threads stored in the map are the same ThreadLocal. If a thread wants to save multiple variables, it needs to create multiple ThreadLocal. When multiple ThreadLocal are put into the map, it will greatly increase the possibility of Hash conflict.

ThreadLocalMap problem

Because the key of ThreadLocalMap is a weak reference and the value is a strong reference. This leads to a problem. When ThreadLocal has no strong reference to external objects, the weak reference key will be recycled during GC, but the value will not be recycled. If the thread creating ThreadLocal keeps running, the value in the Entry object may not be recycled and memory leakage may occur.

How to avoid leakage

Since the Key is a weak reference, what we need to do is to call the remove method after calling the get() and set() methods of ThreadLocal to remove the reference relationship between the Entry node and the Map. In this way, the whole Entry object will become unreachable after GC Roots analysis and can be recycled at the next GC.

If you call the remove method after using the set method of ThreadLocal, memory leakage may occur. Therefore, it is very important to develop good programming habits. Remember to call the remove method after using ThreadLocal.

ThreadLocal<Session> threadLocal = new ThreadLocal<Session>();
try {
    threadLocal.set(new Session(1, "Misout Blog"));
    // Other business logic
} finally {
    threadLocal.remove();
}

Application scenario

Remember the session acquisition scenario of Hibernate?

private static final ThreadLocal<Session> threadLocal = new ThreadLocal<Session>();

//Get Session
public static Session getCurrentSession(){
    Session session =  threadLocal.get();
    //Judge whether the session is empty. If it is empty, a session will be created and set to the local thread variable
    try {
        if(session ==null&&!session.isOpen()){
            if(sessionFactory==null){
                rbuildSessionFactory();// Create a Hibernate SessionFactory
            }else{
                session = sessionFactory.openSession();
            }
        }
        threadLocal.set(session);
    } catch (Exception e) {
        // TODO: handle exception
    }

    return session;
}

Why? Each thread accessing the database should be an independent Session. If multiple threads share the same Session, it is possible that other threads have closed the connection. When the current thread performs commit again, the exception that the Session has been closed will appear, resulting in system exception. This method can avoid thread contention for Session and improve the security under concurrency.

The typical scenario of using ThreadLocal is just like the above scenarios of database connection management and thread session management. It is only applicable to the copy of independent variables. If the variables are globally shared, it is not applicable to be used under high concurrency.

summary

  • Each ThreadLocal can only save one copy of variables. If you want to go online and a thread can save more than one copy, you need to create multiple ThreadLocal.
  • The ThreadLocalMap key inside ThreadLocal is a weak reference, and there is a risk of memory leakage.
  • It is applicable to high concurrency scenarios with no state and independent replica variables that do not affect business logic. If the business logic strongly depends on replica variables, ThreadLocal is not suitable for solution, and another solution needs to be found.

Topics: Concurrent Programming