Handler-ThreadLocal analysis

ThreadLocal source code analysis

Under Android’s Handler mechanism, ThreadLocal provides the storage of copies of local variables of different threads, and realizes the isolation of thread data, so that the data of different threads will not be confused. And after a thread ends, its corresponding data in ThreadLocal will be released, unless there are references to this part of the data elsewhere.

Basic introduction

ThreadLocal provides the function of saving thread local variables. Modification of thread local variables is implemented by set() of ThreadLocal, and reading is implemented by get() method of ThreadLocal accomplish. ThreadLocal instances are usually defined with static fields that are associated with the state of a thread (for example, user ID or business ID).

According to the app startup process, the use of ThreadLocal under the main thread will be analyzed accordingly.

Usage of Android main thread ThreadLocal

In the Android main thread, the ThreadLocal object is held by Looper, and the static field is defined in the Looper class .

static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();

When the app process starts executing, the ActivityThread.main() method is first executed:

// ActivityThread.java

public static void main(String[] args) {<!-- -->
    Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "ActivityThreadMain");
    // ...
    Looper.prepareMainLooper(); // Under the main thread, prepare the main looper.
    // ...
    ActivityThread thread = new ActivityThread();
    thread.attach(false, startSeq);

    if (sMainThreadHandler == null) {<!-- -->
        sMainThreadHandler = thread.getHandler();
    }
    // ...
    Looper.loop(); // looper starts looping

    throw new RuntimeException("Main thread loop unexpectedly exited");
}

In the main thread, create and initialize Looper and ThreadLocal objects by calling Looper.prepareMainLooper(). Call the Looper.prepareMainLooper() method:

// Looper.java

// sThreadLocal.get() will return null if it returns before the prepare() method is called.
@UnsupportedAppUsage
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();

@UnsupportedAppUsage
private static Looper sMainLooper; // This object is static, that is, its life cycle is consistent with the app.

public static void prepare() {
    prepare(true);
}

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)); // Set the current thread to be associated with looper
}

// The main looper of the main thread is created by the system environment, so it does not need to be called in the app program.
public static void prepareMainLooper() {
    prepare(false);
    synchronized (Looper.class) {
        if (sMainLooper != null) {
            throw new IllegalStateException("The main Looper has already been prepared.");
        }
        sMainLooper = myLooper();
    }
}

// Return the Looper object associated with the current thread. If the current thread does not have an associated Looper object, return null.
public static @Nullable Looper myLooper() {
    return sThreadLocal.get();
}

In a thread, only one Looper object can be created, otherwise the corresponding thread will crash, including the prompt message "Only one Looper may be created per thread". The following sequence diagram is the calling process of prepareMainLooper() in the app process.

To sum up, it can be summarized in a few simple parts:

  1. Determine whether the current thread already has a Looper object associated with it, and if so, throw a runtime error ( throw new RuntimeException("Only one Looper may be created per thread");).
  2. Associate the newly created Looper object with the current thread through sThreadLocal.set().
  3. Determine if sMainLooper is not null, throw a status exception (throw new IllegalStateException("The main Looper has already been prepared.")).
  4. Get the Looper object associated with the current thread from sThreadLocal and assign it to sMainLooper as the main thread Looper.

After the looper of the main thread is created, that is, after the object referenced by sMainLooper is saved in sThreadLocal. The looper starts to perform the loop() operation (we will talk about Looper specifically later).

Below is an analysis of the most commonly used methods of TreadLocal set() get().

set()

When the app process is running, the main looper is created and associated with the main thread. That is the process of calling sThreadLocal.set(new Looper(quitAllowed)).

Definition of set() method:

// ThreadLocal.java

private final int threadLocalHashCode = nextHashCode();

// This is static. Create ThreadLocal in the thread and call the set() method. The value of nextHashCode increases after each call.
// And used to calculate the index position value of Entry in the table.
private static AtomicInteger nextHashCode = new AtomicInteger();

// The hash value is incremented. After each increment, the AND operation is performed with 0x0F (i.e. 15). The index value generated by each calculation is different.
// When calculating the 16th time, the index value starts again, which is the same as the loop value calculated for the first time.
private static final int HASH_INCREMENT = 0x61c88647;

// used for growth is not the original value
private static int nextHashCode() {<!-- -->
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}

// Set the current thread value of a thread-local variable to a specific value.
public void set(T value) {<!-- -->
    Thread t = Thread.currentThread(); // Get the current thread
    ThreadLocalMap map = getMap(t); // When called for the first time, the getMap(t) method returns null.
    if (map != null)
        map.set(this, value); // When ThreadLocalMap has been created, call the set method directly.
    else
        createMap(t, value); // The first call to getMap(t) returns Null, so the execution will go here to create a ThreadLocalMap object.
}

// Get the thread local variable map of the current thread.
ThreadLocalMap getMap(Thread t) {<!-- -->
    return t.threadLocals;
}

// Get the local variable table of the current thread.
ThreadLocalMap getMap(Thread t) {<!-- -->
    return t.threadLocals;
}

// Create a ThreadLocalMap object for the current thread.
void createMap(Thread t, T firstValue) {<!-- --> // In the main thread here, the firstValue passed in is a Looper object.
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

// The ThreadLocalMap inside ThreadLocal actually saves the implementation of thread local variables.
static class ThreadLocalMap {<!-- -->
 private static final int INITIAL_CAPACITY = 16;
    private Entry[] table; // Structural class that encapsulates (ThreadLocal, Looper) pairs.
    private int size = 0; // The size of the entry table that stores values.
    
    static class Entry extends WeakReference<ThreadLocal<?>> {<!-- -->
        Object value;
        Entry(ThreadLocal<?> k, Object v) {<!-- -->
            super(k);
            value = v;
        }
    }

    // Create a ThreadLocalMaps object that holds (firstKey, firstValue) key-value pairs, where firstKey is always ThreadLocal.
    ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {<!-- -->
        table = new Entry[INITIAL_CAPACITY];
        int i = firstKey.threadLocalHashCode & amp; (INITIAL_CAPACITY - 1); // This method generates the index value for storing Entry.
        table[i] = new Entry(firstKey, firstValue);
        size = 1;
        setThreshold(INITIAL_CAPACITY); //When the table array is full, the size of the table needs to be expanded.
    }

}

Each time the getMap(t) method is called for the first time through the current thread object and attempts to obtain the thread local variable map result, the method will return null , so createMap(t, value); will be called. If it has been created, ThreadLocalMap.set(ThreadLocal key, Object value) will be called. > .

//ThreadLocalMap in ThreadLocal.java

// Quickly calculate the next index position.
private static int nextIndex(int i, int len) {<!-- -->
    return ((i + 1 < len) ? i + 1 : 0);
}

private void set(ThreadLocal<?> key, Object value) {<!-- --> // In the main thread, the value of value is the Looper object.

    Entry[] tab = table;
    int len = tab.length; // Generally it is 16 of the array size, or the expanded capacity.
    int i = key.threadLocalHashCode & amp; (len-1);

    // Search in Entry[]. If the Entry of the same ThreadLocal is found, update the corresponding value directly.
    for (Entry e = tab[i];
         e != null;
         // Find the next search position.
         e = tab[i = nextIndex(i, len)]) {<!-- -->
        ThreadLocal<?> k = e.get();

        if (k == key) {<!-- --> // The same ThreadLocal Entry is found, and the corresponding value is directly updated.
            e.value = value;
            return;
        }

        if (k == null) {<!-- --> // ThreadLcoal<?> of Entry is null, replace the Entry object.
            // Clear all expired entries in the current run.
            // The main purpose is to clear expired entries in the hash table when inserting new elements or clearing expired elements to reduce the size of the hash table and improve efficiency.
            replaceStaleEntry(key, value, i);
            return;
        }
    }

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

This code is the real implementation of the set(T) method of ThreadLocal. During the setting process, expired entries in ThreadLocalMap are cleared. The judgment condition is whether the Entry object has been recycled by GC, that is, e.get() == null.

get()

get() method definition:

// Returns a copy of the current thread's local variable table. If there is no value in the table, the initial value set in the setInitialValue() method is returned.
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();
}

static class ThreadLocalMap {<!-- -->
    // Calculate the index position of the Entry object through hashcode. And get the corresponding value. If it is recycled by GC, call getEntryAfterMiss().
    private Entry getEntry(ThreadLocal<?> key) {<!-- -->
        int i = key.threadLocalHashCode & amp; (table.length - 1);
        Entry e = table[i];
        if (e != null & amp; & amp; e.get() == key)
            return e;
        else
            return getEntryAfterMiss(key, i, e);
    }
    
    // When the corresponding Entry cannot be found at the directly calculated index position, calculate the next position and try to search.
    // If e.get() == null, it means that the entry has expired and the index position Entry will be cleared.
    private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {<!-- -->
        Entry[] tab = table;
        int len = tab.length;

        while (e != null) {<!-- -->
            ThreadLocal<?> k = e.get();
            if (k == key)
                return e;
            if(k==null)
                expungeStaleEntry(i);
            else
                i = nextIndex(i, len);
            e = tab[i];
        }
        return null;
    }
}

The above get() set() methods are the two most commonly used methods in ThreadLocal. The most commonly used analysis of ThreadLocal comes here first.