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:
- 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");
). - Associate the newly created
Looper
object with the current thread throughsThreadLocal.set()
. - Determine if
sMainLooper
is not null, throw a status exception (throw new IllegalStateException("The main Looper has already been prepared."
)). - Get the
Looper
object associated with the current thread fromsThreadLocal
and assign it tosMainLooper
as the main threadLooper
.
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.