Discussion on ThreadLocal in Java Concurrent Programming

ThreadLocal may be rarely used in the normal development process, but it is indispensable for thread operation. In particular, the communication between threads through Handler is an important part. In this article, I will help you analyze the use and internal principle of ThreadLocal.

What is ThreadLocal

ThreadLocal is a class about creating thread local variables. ThreadLocal can save an object in a specified thread. After the object is saved, it can only obtain the saved data in the specified thread, but it cannot obtain data for other threads.

ThreadLocal use

ThreadLocal is very simple to use. Usually, we only need to care about the internal set() and get() methods.

public class MainActivity extends AppCompatActivity {

    private static final String TAG = "ThreadLocalTest";
    
    private ThreadLocal<String> stringThreadLocal = new ThreadLocal<>();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        // set an object in the main thread
        stringThreadLocal.set("MainThread");
        // The main thread obtains the object through TheadLocal, and the result is "MainThread"
        Log.d(TAG, "MainThread's stringThreadLocal=" + stringThreadLocal.get());
        
        new Thread("Thread#1") {
            @Override
            public void run() {
                // Get the object from the child thread, and the result is null
                Log.d(TAG, "Thread#1's stringThreadLocal=" + stringThreadLocal.get());
            }
        }.start();
    }
}

ThreadLocal can also set a default value. If there is a set value, it will take the set value. If there is no set value, it will take the default value.

private ThreadLocal<Boolean> booleanThreadLocal = new ThreadLocal<Boolean>(){
    @Override
    protected Boolean initialValue() {
        return false;
    }
};

ThreadLocal also has a remove to remove the saved object.

Relationship between loophandler and local thread Handler

We know that it is necessary to use Handler to build message body Looper in sub thread:

class MyThread extends Thread {
 
        @Override
        public void run() {
            super.run();
 
            // Prepare message loop body
            Looper.prepare();
            // Build Handler instance
            handler = new Handler(){
                @Override
                public void handleMessage(Message msg) {
                    super.handleMessage(msg);
                    System.out.println( "threadName--" + Thread.currentThread().getName() + "messageWhat-"+ msg.what );
                }
            };
            // Execute looper loop
            Looper.loop();
        }
    }

A static ThreadLocal is defined inside Looper, which is specially used to store Looper instances in the prepare() process and ensure that a thread can only have one Looper instance:

// sThreadLocal.get() will return null unless you've called prepare().
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
private static void prepare(boolean quitAllowed) {
    if (sThreadLocal.get() != null) {
        throw new RuntimeException("Only one Looper may be created per thread");
    }
    sThreadLocal.set(new Looper(quitAllowed));
}

Next, when initializing the Handler, use ThreadLocal Get gets the Looper and gets the Looper's MessageQueue. Then the Handler can execute sendMessage, and the message is stored in the MessageQueue.

Finally, looper The implementation behind looper () is to traverse the messages in MessageQueue and then distribute them.

Analysis of ThreadLocal principle

Each thread maintains a ThreadLocalMap object, and the stored value of ThreadLocal is in ThreadLocalMap (key is ThreadLocal itself, and value is the current value). This is why ThreadLocal can only be accessed in a thread, and other threads cannot. A thread can have multiple ThreadLocal (a thread can have multiple variables stored), However, a ThreadLocal can only be responsible for one thread.

ThreadLocalMap is the static internal class of ThreadLocal. ThreadLocal has a total of more than 700 lines of code, while ThreadLocalMap accounts for four or five hundred lines. ThreadLocalMap and ThreadLocal are close to each other. Combining the two is the single responsibility principle of design pattern.

ThreadLocal has only three public methods: set, get and remove.

set method

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}
  1. Get current thread
  2. Get the ThreadLocalMap owned by the current thread
  3. Save value, key is the current ThreadLocal itself, and value is all stored objects.

How is ThreadLocalMap set? Take a look at the source code of ThreadLocalMap:

/**
 * Set the value associated with key.
 *
 * @param key the thread local object
 * @param value the value to be set
 */
private void set(ThreadLocal<?> key, Object value) {

    // We don't use a fast path as with get() because it is at
    // least as common to use set() to create new entries as
    // it is to replace existing ones, in which case, a fast
    // path would fail more often than not.

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

Behind the stored value of ThreadLocalMap is the stored value of the Entry array. Each node of the array, that is, the Entry itself, is used as a key (ThreadLocal), a weak reference type. A variable is maintained inside the Entry, that is, the value used to point to the stored value of ThreadLocal.

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

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

During the storage of the array, the hash of the Key (ThreadLocal) is used as the index i of the array to find the Entry of the target. If the Entry already exists, replace the value value value. If it does not exist, create a new Entry.

Why is Entry a weak reference type

If the Entry is not a weak reference type, it will essentially cause the life cycle of the node to be strongly bound to the thread. As long as the thread is not destroyed, the node will always be reachable in GC analysis and cannot be recycled.

Although the Entry itself is a weak reference, the internally maintained value value is a strong reference. If the value reference chain is broken, the Entry will not be recycled. Once the value reference chain is broken, the Entry itself will be easily recycled.

Entry circular array

ThreadLocalMap maintains an Entry ring array. The ring structure of the Entry array comes from the nextIndex function in the set method:

private static int nextIndex(int i, int len) {
            return ((i + 1 < len) ? i + 1 : 0);
            }
  1. Pass in the index value plus one to return the coordinates.
  2. When the length of the index value exceeds the length of the array, it will directly return 0 and return to the head of the array to complete the ring structure.

The advantages are:

  1. The length is fixed, and the subscript never crosses the boundary.
  2. High space utilization can greatly save memory overhead and make full use of the space of the array.

get method

After analyzing the set method above, the get method is much simpler. First look at the source code:

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();
}
  1. Find current thread
  2. Find the ThreadLocalMap owned by the thread
  3. Find the target Entry through the Key (current ThreadLocal)
  4. The value of Entry is the value of get

map. Behind getentry is to find the target Entry by using the Hash of the Key (current ThreadLocal) as a coordinate of the Entry array.

remove method

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

Mainly look at the threadlocalmap remove:

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

The remove method is also very simple. Use the Hash of the Key (current ThreadLocal) as a coordinate of the Entry array to find the target Entry and directly clear (weak reference to the internal inherent method).

Expansion of Entry array

Judge the expansion through rehash() method:

/**
 * Re-pack and/or re-size the table. First scan the entire
 * table removing stale entries. If this doesn't sufficiently
 * shrink the size of the table, double the table size.
 */
private void rehash() {
    expungeStaleEntries();

    // Use lower threshold for doubling to avoid hysteresis
    if (size >= threshold - threshold / 4)
        resize();
}

Capacity expansion condition: the number of instances in the array is greater than or equal to three-quarters of the threshold (threshold is two-thirds of the length of the array).

resize method is the actual capacity expansion algorithm:

/**
 * Double the capacity of the table.
 */
private void resize() {
    Entry[] oldTab = table;
    int oldLen = oldTab.length;
    int newLen = oldLen * 2;
    Entry[] newTab = new Entry[newLen];
    int count = 0;

    for (int j = 0; j < oldLen; ++j) {
        Entry e = oldTab[j];
        if (e != null) {
            ThreadLocal<?> k = e.get();
            if (k == null) {
                e.value = null; // Help the GC
            } else {
                int h = k.threadLocalHashCode & (newLen - 1);
                while (newTab[h] != null)
                    h = nextIndex(h, newLen);
                newTab[h] = e;
                count++;
            }
        }
    }

    setThreshold(newLen);
    size = count;
    table = newTab;
}
  1. The core of capacity expansion is array replacement and replication.
  2. The new array is twice the size of the old array.
  3. Copy, calculate the index value and store it in the expansion array. If the node conflicts, find the null node backward and store it.

Keywords: Java Concurrent Programming thread

Added by dan182skater on Mon, 07 Mar 2022 09:55:10 +0200