The use and principle of ThreadLocal

I. overview

The simple understanding of ThreadLocal is to share resources for a thread, save some resources in the thread through set() method, and then obtain this resource through get method. It solves the problem of sharing the same object or variable in the same thread and method of different classes. Note that it is not to solve the resource sharing of multithreading in concurrency. In this scenario, lock is generally required. Instead, in order to maintain the resources held by each thread independently among multiple threads, there is no need to lock.

It can be imagined that the use scenario of ThreadLocal is to share database connection in a thread. Although I haven't seen the source code of mybatis and hibernate (to be studied later), it is estimated that ThreadLocal is indispensable in these frameworks. In addition, ThreadLocal is used in java read-write lock to save the number of times a thread re enters the read lock.

Let's start with the basic code structure of ThreadLocal to understand the source code implementation.

2, Infrastructure

The following code does not need to be studied carefully. You can read it carefully after reading the set() method. From the following code, we need to understand two things:

  1. ThreadLocal and the corresponding variables are stored in the table array of the inner ThreadLocalMap class.
  2. The Entry of ThreadLocalMap uses weak reference. We will explain the advantages of using weak reference later.
public class ThreadLocal<T> {
    /** Generate hash code for ThreadLocal */
    private final int threadLocalHashCode = nextHashCode();

    /** Generate hash code with AtomicInteger, note that it is static here  */
    private static AtomicInteger nextHashCode =
        new AtomicInteger();

    /** Hash increment */
    private static final int HASH_INCREMENT = 0x61c88647;

    /** Return next hash code */
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }
	
	/** The constructor is relatively simple */
	public ThreadLocal() {
    }

	/** ThreadLocal The main logic of ThreadLocalMap is ThreadLocalMap, which is thread independent, and each thread can keep at most one copy*/
	static class ThreadLocalMap {

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

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

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

        /** Single thread table can keep multiple ThreadLocal variables */
        private Entry[] table;

        /** table Size */
        private int size = 0;

        /** The capacity expansion threshold. If the table size exceeds the capacity expansion threshold, the capacity will be expanded */
        private int threshold; // Default to 0

        /**
         * Set the resize threshold to maintain at worst a 2/3 load factor.
         */
        private void setThreshold(int len) {
            threshold = len * 2 / 3;
        }

        /**
         * 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);
        }

        /** ThreadLocalMap Initializes the first value of table and sets the expansion threshold */
        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);
        }
	}  
}	      

3, Variable settings

The variable of ThreadLocal is set in the set() method, from which we can roughly understand the principle of ThreadLocal. Before reading the set() method, we will use an example and diagram to briefly comb the principle of ThreadLocal saving variables.

 public static void main(String[] args) throws InterruptedException {
        ThreadLocal threadLocal = new ThreadLocal();
        String var1 = "Variable 1";
        String var2 = "Variable 2";

        Thread thread1 = new Thread(()->{
            threadLocal.set(var1);
            System.out.println("Variables for thread 1=" + threadLocal.get());
        });

        Thread thread2 = new Thread(()->{
            threadLocal.set(var2);
            System.out.println("Variables for thread 2=" + threadLocal.get());
        });

        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();
    }

Operation output:
Variable for thread 2 = variable 2
Variable for thread 1 = variable 1

The above example can be understood in combination with the following diagram. First, the main method creates ThreadLocal and the variables var1 and var2, then sets the variables for Thread 1 and Thread 2 respectively, and finally prints the set variables respectively. As you can see from the figure below, each Thread object has a threadLocals variable (in the Thread class), whose type is the ThreadLocalMap mentioned in the source code above. ThreadLocal and var1, var2 are stored in the Entry array of ThreadLocalMap as key value. You can see that each Thread maintains an Entry array, which ensures that variables are Thread independent.

		public void set(T value) {
	        Thread t = Thread.currentThread();
	        // Get ThreadLocalMap from Thread object
	        ThreadLocalMap map = getMap(t);
	        if (map != null)
	        	// If ThreadLocalMap is available, set the variable to the thread. This method is explained below
	            map.set(this, value);
	        else
	        	// Create a ThreadLocalMap 
	            createMap(t, value);
	    }
	
		/** Thread Class method */
		ThreadLocalMap getMap(Thread t) {
	        return t.threadLocals;
	    }

		/** ThreadLocalMap set() method in class */
		private void set(ThreadLocal<?> key, Object value) {
            Entry[] tab = table;
            int len = tab.length;
            // Find the insertion position with hash value and array size and operation
            int i = key.threadLocalHashCode & (len-1);
			
			/** If the insertion position has a value, take the next position of the insertion position (it can be understood that it is a ring array), poll until the insertion position is found to be empty,
			Either the key is found to be equal, or k is found to be empty (indicating that some variables are invalid, and the weak reference is set to null by gc,
			But value still has value, and this position will be wiped out.)*/
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();
				// If the key is equal, reset the value directly
                if (k == key) {
                    e.value = value;
                    return;
                }
				// If k is null, the replacestateentry() method will be called. There are replace and erase operations in it
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
			// If the insertion position is empty, insert an Entry directly
            tab[i] = new Entry(key, value);
            // Number of Entry arrays plus one
            int sz = ++size;
            // The cleanSomeSlots() method will clear the Entry whose key is empty again. If there are no entries to clear and the array size exceeds the threshold value, the capacity will be expanded
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
            	// Double the capacity expansion method, which will rearrange the location of the Entry
                rehash();
        }
        
        /** This method will be called when a new variable is added and the key is null in the insertion position. It will erase or exchange the Entry*/
		private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                       int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;
            Entry e;
			// Look forward to see if there is an Entry subscript whose Entry is not empty but key is empty
            int slotToExpunge = staleSlot;
            for (int i = prevIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = prevIndex(i, len))
                if (e.get() == null)
                    slotToExpunge = i;

            // Look backward to see if the key is equal or the key is null
            for (int i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();

				// If the key s are equal, exchange the entries, advance the new entries, and move the positions found back
                if (k == key) {
                    e.value = value;

                    tab[i] = tab[staleSlot];
                    tab[staleSlot] = e;

                    // If it is the same, it means that no Entry with empty key can be found forward, then the erasing position can start from i
                    if (slotToExpunge == staleSlot)
                        slotToExpunge = i;
                    // Call two erasure methods
                    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                    return;
                }

                // Find that k is null and slotToExpunge == staleSlot, then erase starts from i
                if (k == null && slotToExpunge == staleSlot)
                    slotToExpunge = i;
            }

            // You can't find the same one backward, so the insertion position is the staleSlot you just know
            tab[staleSlot].value = null;
            tab[staleSlot] = new Entry(key, value);

            // Slotttoexpunge! = staleSlot. It means that in addition to the Entry with null key in the staleSlot location, there are other places. Then call the erase method
            if (slotToExpunge != staleSlot)
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
        }
        
	/** This method is to find the Entry whose Entry is not empty but whose key is null from the specified location, and then erase it */        
	private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;

            // staleSlot position meets the conditions, erase first
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;

            // Rehash until we encounter null
            Entry e;
            int i;
            // Find the Entry whose key is null from the staleSlot position and erase it
            for (i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
                if (k == null) {
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
                	// It is possible to wipe out the Entry, and the size will change, so the location of all hash maps may change, and rearrange here
                    int h = k.threadLocalHashCode & (len - 1);
                    if (h != i) {
                        tab[i] = null;

                        // Unlike Knuth 6.4 Algorithm R, we must scan until
                        // null because multiple entries could have been stale.
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
            return i;
        }
	
	/** expungeStaleEntry()There may be omissions in the method erasure. Here, perform log(n) search or erasure again */
	private boolean cleanSomeSlots(int i, int n) {
            boolean removed = false;
            Entry[] tab = table;
            int len = tab.length;
            do {
                i = nextIndex(i, len);
                Entry e = tab[i];
                if (e != null && e.get() == null) {
                    n = len;
                    removed = true;
                    i = expungeStaleEntry(i);
                }
            } while ( (n >>>= 1) != 0);
            return removed;
        }

A lot of erasure logic is designed in the above code because ThreadLocal is saved in each thread. It's unnecessary to join this ThreadLocal. We can't clear the Entry of each thread, which may cause memory leak.

But the key of Entry is ThreadLocal, which uses weak reference. When ThreadLocal does not have a strong reference point, the GC payback period will empty the weak reference, so we can clear the useless Entry by judging whether the key of the Entry is null, which can effectively avoid memory leakage.

Here is a brief summary of the types of java references and the differences between memory leaks and memory overflows

1. Type of reference

  1. Soft reference: only when memory is not enough can it be recycled. It can be used as browser's back cache page
  2. Weak reference: when an object is only pointed to by a weak reference, the gc runtime will be recycled and null will be set.
  3. Strong reference: we use new or reflection to create references to objects.

2. Difference between memory leak and memory overflow:

  1. out of memory: when a program applies for memory, there is not enough memory for the applicant to use. For example, there is a piece of storage space for int type data, but the programmer uses it to store long type data. As a result, the memory is not enough, and an error OOM will be reported, which is called memory overflow.
  2. Memory leak: refers to that a program cannot release the memory space that has been applied after applying for memory. A memory leak does not seem to have a big impact, but the result of memory leak accumulation is memory overflow.

4, Get variable

Get variable directly calls the get() method of ThreadLocal

	public T get() {
        Thread t = Thread.currentThread();
        // Get ThreadLocalMap from Thread object
        ThreadLocalMap map = getMap(t);
        if (map != null) {
        	// Later code analysis
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        // If there is no variable corresponding to ThreadLocal in ThreadLocalMap, the initial
        return setInitialValue();
    }

		private Entry getEntry(ThreadLocal<?> key) {
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            // The corresponding location of the hash just finds the variable, otherwise the getEntryAfterMiss() method is called
            if (e != null && e.get() == key)
                return e;
            else
                return getEntryAfterMiss(key, i, e);
        }
        
		private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
            Entry[] tab = table;
            int len = tab.length;

			// If the corresponding position is not empty, poll the back of the array to see if an object with the same key can be found
            while (e != null) {
                ThreadLocal<?> k = e.get();
                if (k == key)
                    return e;
                // Erase if k is empty
                if (k == null)
                    expungeStaleEntry(i);
                else
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;
        }
    
	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;
    }

	// Initialized value is null
	protected T initialValue() {
        return null;
    }

5, Delete variable

When setting the variable method, we see a lot of erasure logic. In fact, for the sake of security, if ThreadLocal is found to be no longer used in the thread, we need to call the remove method to delete the Entry.

	public void remove() {
		 // Get ThreadLocalMap from the current thread
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

	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) {
                	// After finding the Entry corresponding to key, the reference is set to null, and then the erasure method is called.
                    e.clear();
                    expungeStaleEntry(i);
                    return;
                }
            }
        }
        
	public void clear() {
        this.referent = null;
    }

Six, summary

ThreadLocal is a tool to save and transfer variables between different threads or methods. It is designed to avoid the influence of multiple threads on variables. In addition, if we no longer use ThreadLocal, we should call its remove() method to clear the corresponding Entry.

Published 65 original articles, won praise 7, visited 8544
Private letter follow

Tags: Java Database Mybatis Hibernate

Posted on Wed, 05 Feb 2020 23:24:57 -0800 by vhaxu