Java Multithreading and Concurrent ThreadLocal

Keywords: Java Spring

1. What is ThreadLocal?Use scenarios

Introduction to ThreadLocal

ThreadLocal is a thread-local variable that provides a solution to multithreaded concurrency problems. When using ThreadLocal to maintain variables, ThreadLocal provides a separate copy of variables for each thread that uses the variable, so each thread can independently change its own copy without affecting the corresponding copy of other threads.

ThreadLocal usage scenarios

When multiple threads try to get a shared variable, they need to get a copy of its initial value.Each thread stores a copy of the variable, and changes to the copy of the variable do not affect the variable itself.For scenarios where multiple threads depend on different variable values to complete operations.For example:

  • Switching of Multiple Data Sources
  • spring declarative transaction

2. Use cases for ThreadLocal

ThreadLocal class interface:

  • void set(T value): Sets the value of the thread local variable for the current thread
  • T get(): Gets the thread local variable for the current thread
  • void remove(): Deletes the value of the current thread local variable in order to reduce memory consumption
  • T initialValue(): The initial value of the thread's local variable (the default is null), which is a lazy load method of protected that is executed when the thread first calls get() or set(T value), and is designed to override subclasses.
public class ThreadLocalDemo {
    private static ThreadLocal<Index> index = new ThreadLocal(){
        @Override
        protected Object initialValue() {
            return new Index();
        }
    };

    private static class Index{
        private int num;

        public void incr(){
            num++;
        }
    }

    public static void main(String[] args) {
        for(int i=0; i<5; i++){
            new Thread(() ->{
                Index local = index.get();
                local.incr();
                System.out.println(Thread.currentThread().getName() + " " + index.get().num);
            }, "thread_" + i).start();
        }
    }
}

Output results:

thread_1 1 thread_0 1 thread_3 1 thread_4 1 thread_2 1

You can see that each thread gets an initial value of 0 and has no effect on the operation of num++.

3. How ThreadLocal is implemented

3.1 ThreadLocal data structure

ThreadLocal maintains a Map-like ThreadLocalMap data structure, and each Thread class has a ThreadLocalMap member variable.ThreadLocalMap takes a ThreadLocal variable as the key and a copy of the ThreadLocal variable as the value, as shown in the diagram:

It is important to note that the relationship between Entry's key and value in ThreadLocal is maintained systematically and that improper maintenance can lead to insecurity in a multithreaded state (generally not, at least with caution).

3.2 get() source code analysis

  public T get() {
      	//Get Current Thread
          Thread t = Thread.currentThread();
      	//Gets the ThreadLocalMap of the current thread
          ThreadLocalMap map = getMap(t);
          if (map != null) {
              //If the ThreadLocalMap has already been created, get the value by using the current threadLocal object as the key
              ThreadLocalMap.Entry e = map.getEntry(this);
              if (e != null) {
                  @SuppressWarnings("unchecked")
                  T result = (T)e.value;
                  return result;
              }
          }
      	//If the ThreadLocalMap has not been created or cannot be found in the ThreadLocalMap
          return setInitialValue();
      }

3.2.1 ThreadLocalMap not initialized

ThreadLocalMap is not initialized and when ThreadLocalMap is null, the setInitialValue() method is called:

  private T setInitialValue() {
      	//The initialValue method is usually overridden, and if not, returns null directly
          T value = initialValue();
          Thread t = Thread.currentThread();
      	//Gets the ThreadLocalMap of the current thread
          ThreadLocalMap map = getMap(t);
          if (map != null)
              //ThreadLocalMap has already been created, so set the initial value directly (that is, save a copy of the variable) from the initialValue method
              map.set(this, value);
          else
              //Create ThreadLocalMap
              createMap(t, value);
          return value;
      }

The initialValue() method is overridden by us, and it is important to note that the return value must be a new object, not a direct return of an object reference.Because if multiple threads hold a copy of the same reference, they modify the value of the shared variable through that reference, which affects each other.Our original purpose was to get a copy of the initial value of the shared variable, and each thread's modifications to the copy did not affect the variable itself.

Let's see how createMap creates a threadLocalMap

  void createMap(Thread t, T firstValue) {
          t.threadLocals = new ThreadLocalMap(this, firstValue);
      }
  ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
      	   //Create an Entry array with an initial capacity of 16
              table = new Entry[INITIAL_CAPACITY];
      		//Position in the array by threadLocal threadLocalHashCode
              int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
      		//Save in Array
              table[i] = new Entry(firstKey, firstValue);
      		//Record size used
              size = 1;
      		//Set threshold to 2/3 of capacity
              setThreshold(INITIAL_CAPACITY);
          }

3.2.2 Initialize threadLocalMap

After initializing threadLocalMap, the thread calls the get() method again, and what else does it do?

  public T get() {
      	//Get Current Thread
          Thread t = Thread.currentThread();
      	//Gets the ThreadLocalMap of the current thread
          ThreadLocalMap map = getMap(t);
          if (map != null) {
              //If the ThreadLocalMap has already been created, get the value by using the current threadLocal object as the key
              ThreadLocalMap.Entry e = map.getEntry(this);
              if (e != null) {
                  @SuppressWarnings("unchecked")
                  T result = (T)e.value;
                  return result;
              }
          }
      	//If the ThreadLocalMap has not been created or cannot be found in the ThreadLocalMap
          return setInitialValue();
      }

You can see that elements are found by map.getEntry(this)

  private Entry getEntry(ThreadLocal<?> key) {
              int i = key.threadLocalHashCode & (table.length - 1);
              Entry e = table[i];
              if (e != null && e.get() == key)
                  return e;
              else
                  //If the key of the positioned element is not equal to the key passed in, look back
                  return getEntryAfterMiss(key, i, e);
          }
  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)
                      //Clear out expired key s and move back elements (moved past positions) forward
                      expungeStaleEntry(i);
                  else
                      //Move one bit backward
                      i = nextIndex(i, len);
                  e = tab[i];
              }
              return null;
          }
  private static int nextIndex(int i, int len) {
              return ((i + 1 < len) ? i + 1 : 0);
          }
  private int expungeStaleEntry(int staleSlot) {
      Entry[] tab = table;
      int len = tab.length;
  
      // Clear current element
      tab[staleSlot].value = null;
      tab[staleSlot] = null;
      size--;
  
      //Move the element behind this element forward because the hash conflict moved through the position
      Entry e;
      int i;
      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 {
              int h = k.threadLocalHashCode & (len - 1);
              //H!= I indicates a hash conflict
              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;
  }

What if map.getEntry(this) also can't find the element?

  public T get() {
      	//Get Current Thread
          Thread t = Thread.currentThread();
      	//Gets the ThreadLocalMap of the current thread
          ThreadLocalMap map = getMap(t);
          if (map != null) {
              //If the ThreadLocalMap has already been created, get the value by using the current threadLocal object as the key
              ThreadLocalMap.Entry e = map.getEntry(this);
              if (e != null) {
                  @SuppressWarnings("unchecked")
                  T result = (T)e.value;
                  return result;
              }
          }
      	//If the ThreadLocalMap has not been created or cannot be found in the ThreadLocalMap
          return setInitialValue();
      }

Then continue to call the setInitialValue() method

  private T setInitialValue() {
      	//The initialValue method is usually overridden, and if not, returns null directly
          T value = initialValue();
          Thread t = Thread.currentThread();
      	//Gets the ThreadLocalMap of the current thread
          ThreadLocalMap map = getMap(t);
          if (map != null)
              //ThreadLocalMap has already been created, so set the initial value directly (that is, save a copy of the variable) from the initialValue method
              map.set(this, value);
          else
              //Create ThreadLocalMap
              createMap(t, value);
          return value;
      }

You can see that the map.set(this, value) method inside it will be called

  private void set(ThreadLocal<?> key, Object value) {
              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) {
                      //Replace expired elements and clear some of the later ones
                      replaceStaleEntry(key, value, i);
                      return;
                  }
              }
  			
      		//If you can't find one in the table, create a new one
              tab[i] = new Entry(key, value);
              int sz = ++size;
              if (!cleanSomeSlots(i, sz) && sz >= threshold)
                  //Expand and and re-hash if no elements are cleared beyond the threshold
                  rehash();
          }

3.3 set() source code analysis

See how set() handles things like setInitialValue, with fewer initialization values and the same everything else. Refer to get() where [ThreadLocalMap is not initialized] (#3.2.2 initializes threadLocalMap):

public void set(T value) {
    //Get Current Thread
    Thread t = Thread.currentThread();
    //Gets the ThreadLocalMap of the current thread
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

3.4 Summary

  • First determine if the threadLocalMap for the current thread exists, or create one if it does not.Save ThreadLocal as key and a copy of the initial value of the shared variable as value in threadLocalMap
  • If threadLocalMap exists, use ThreadLocal as the key and hash the location in the array to find the value
  • If ThreadLocal is not found in the threadLocalMap (reason: deleted, or this ThreaLocal is not joined to the threadLocalMap, since threadLocalMap can hold multiple threadLocals), save ThreadLocal as key, a copy of the shared variable initial value as value in the threadLocalMap
  • During the set process, if there are other elements (hash conflicts) at the current location, look back until no other elements exist.During the set process, some out-of-date (null key) elements are cleared.Finally, depending on the size of the size of the size, whether to expand or not, the hash will be relocated

Previous articles:

Are you all OK?If you like, point and focus with your finger!!Thank you for your support!
Welcome to Search, original technical articles first launched

Posted by if on Sun, 12 Jan 2020 19:01:18 -0800