ThreadLocal solves the multi-threaded security case

The date utility class wrapped in the project is not working in a multithreaded environment. Let’s see how it works

public class ThreadLocalTest {

    public static void main(String[] args) {

        // Create a thread pool
        ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("thread-%d").build();
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(20.20.0L,
                TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(1024), threadFactory);

        for (int i = 0; i < 20; i++) {
            threadPoolExecutor.execute(
                ()-> System.out.println(DateUtilSafe.parse("The 2019-06-01 16:34:30"))); } threadPoolExecutor.shutdown(); }}Copy the code

Date utility classes (thread unsafe)

public class DateUtilNotSafe {

    private static final SimpleDateFormat sdf =
            new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static Date parse(String dateStr) {
        Date date = null;
        try {
            date = sdf.parse(dateStr);
        } catch (ParseException e) {
            e.printStackTrace();
        }
        returndate; }}Copy the code

Multithreading error screenshots:

ThreadLocal solution:

public class DateUtilSafe {

    private static final ThreadLocal<DateFormat> THREAD_LOCAL = ThreadLocal.withInitial(
        () -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));public static Date parse(String dateStr) {
        Date date = null;
        try {
            date = THREAD_LOCAL.get().parse(dateStr);
        } catch (ParseException e) {
            e.printStackTrace();
        }
        returndate; }}Copy the code

Analysis:

The SimpleDateFormat(SDF) class has a Calendar object reference inside it that stores date information associated with the SDF, Methods such as sdf.parse(dateStr), sdf.format(date), etc. pass date-related strings, dates, etc., which are stored by dating Calendar references. This causes a problem. If your SDF is static, multiple threads will share the SDF and Calendar reference, and look at the sdf.parse() method. Atomicity is not guaranteed in the parse method, so there are thread safety issues:

Date parse(a) {

  calendar.clear(); / / clean up the calendar.// Perform some operations, set the calendar date, etc

  calendar.getTime(); // Get the calendar time

}
Copy the code

Since SimpleDateFormat is being shared by multiple threads, let’s leave it unshared and each thread stores its own SimpleDateFormat object. Play with your own objects and there will be no threading problems. ThreadLocal lets threads keep separate copies of their own variables. Each thread uses its own copy of variables independently, without affecting copies of variables in other threads.

ThreadLocal profile

Many people think that ThreadLocal is a kind of multi-threaded synchronization mechanism. In fact, it is not a solution for variable thread safety in multi-threaded environment. It is to solve the security problem of member variables in multi-threaded environment, not to solve the security problem of shared variables in multi-threaded environment.

Thread synchronization allows multiple threads to share a variable, whereas ThreadLocal allows each thread to create its own copy of a variable, so each thread can change its own copy of a variable independently. And does not affect variable copies of other threads.

ThreadLocalMap

ThreadLocal has a very important inner class: The internal structure of a ThreadLocalMap is similar to that of a Map, consisting of an Entry consisting of a key and value pair. The key is the ThreadLocal itself, and the value is a copy of the corresponding thread variable

Note:

ThreadLocal itself does not store values; it simply provides you with a key that finds the value.

2, ThreadLocal is contained in Thread, not Thread contained in ThreadLocal.

ThreadLocalMap and HashMap function similarly, but the implementation is quite different:

  1. The data structure of a HashMap is an array + linked list
  2. The data structure of ThreadLocalMap is simply an array
  3. HashMap resolves hash conflicts by using chained addresses
  4. ThreadLocalMap resolves hash collisions using an open address method
  5. All references to the Entry inner class in a HashMap are strong references
  6. The key in the Entry inner class of ThreadLocalMap is a weak reference and the value is a strong reference

Chain address method

The basic idea of this method is to form a single linked list called synonym chain of all elements whose hash address is I, and store the head pointer of the single linked list in the ith element of the hash table, so the search, insertion and deletion are mainly carried out in the synonym chain.

Open address method

The basic idea of this approach is to find the next empty hash address whenever a conflict occurs (this is very important, the source code is based on this feature, must understand here to proceed), as long as the hash table is large enough, the empty hash address will always be found and the record will be saved.

Advantages and disadvantages of chain address method and open address method

Open address method:

  1. Prone to stacking problems, not suitable for large-scale data storage.
  2. The design of hash functions has a great influence on collisions, and many collisions may occur during insertion.
  3. The deleted element is one of several conflicting elements, which needs to be processed for the following elements, so the implementation is more complicated.

Chain address method:

  1. It is simple to deal with conflicts, and there is no accumulation phenomenon, and the average search length is short.
  2. Nodes in linked lists are applied dynamically, which is suitable for cases where the construction table cannot determine the length.
  3. Deleting nodes is easy to implement. Simply delete the corresponding nodes on the linked list.
  4. Pointers need extra space, so when the node size is small, the open addressing method is more space-saving.

ThreadLocalMap uses the open address method

  1. ThreadLocal HASH_INCREMENT = 0x61C88647 0x61C88647 is a magic number that allows hash codes to be evenly distributed in a 2 ^ N array, that is, Entry[] table, Google has a lot to explain about this magic number, which I won’t repeat here
  2. The amount of data stored in ThreadLocal is usually not very large (and the key is weak reference and will be garbage collected to reduce the amount of data in time). In this case, the simple structure of open address method can save more space, and the query efficiency of array is also very high. In addition, the guarantee of the first point, the conflict probability is also low

Relationships among threads, ThreadLocal, and ThreadLocalMap

From the structure diagram above, we can see the core mechanism of ThreadLocal:

Each Thread has a Map inside it. The Map inside a Thread is maintained by a ThreadLocal, which is responsible for fetching and setting Thread variable values from the Map. Therefore, for different threads, each time they obtain the duplicate value, other threads cannot obtain the duplicate value of the current thread, which forms the isolation of the duplicate and does not interfere with each other.

The source code interpretation 

Let’s take a look at a few methods provided by the ThreadLocal class:

public T get(a) {}public void set(T value) {}public void remove(a) {}protected T initialValue(a) {}Copy the code

The get() method is used to get a copy of the variables that ThreadLocal keeps in the current thread. Set () is used to set a copy of a variable in the current thread. Remove () is used to remove copies of variables in the current thread. InitialValue () is a protected method that is usually overridden when used

The get method

     // Get the value by key
     public T get(a) {

        // Get the current thread
        Thread t = Thread.currentThread();

        // Get the ThreadLocalMap of the current thread
        ThreadLocalMap map = getMap(t);

        if(map ! =null) {

            // This is the current ThreadLocalMap (key). GetEntry gets value: e from the key
            ThreadLocalMap.Entry e = map.getEntry(this);

            if(e ! =null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                // Return the value obtained
                returnresult; }}return setInitialValue();
    }

    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
Copy the code

Set method

    public void set(T value) {
        // Get the current thread
        Thread t = Thread.currentThread();
        
        // Get the ThreadLocalMap of the current thread
        ThreadLocalMap map = getMap(t);
        if(map ! =null)
            // re-place ThreadLocal and the new value copy into the map.
            map.set(this, value);
        else
            / / create
            createMap(t, value);
    }

    // Create a ThreadLocalMap and bind ThreadLocalMap to Thread
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
Copy the code

The remove method

    public void remove(a) {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if(m ! =null)
             // Call the remove method in ThreadLocalMap
             m.remove(this);
     }
Copy the code

ThreadLocalMap (ThreadLocalMap, ThreadLocalMap, ThreadLocalMap, ThreadLocalMap, ThreadLocalMap); See the analysis of ThreadLocalMap below.

ThreadLocalMap data structure

public class ThreadLocal<T> {    
    
    // The data structure uses array + open address method
    static class ThreadLocalMap {
        
        private Entry[] table;

        // Entry inherits WeakReference,
        // There are memory leaks in this block, which will be explained later
        static class Entry extends WeakReference<ThreadLocal<? >>{
            /** The value of the ThreadLocal key is value */
            Object value;

            // The inner class Entry is a key and value structure similar to the map structure
            // Key is a ThreadLocal and value is a copy of the variable valueEntry(ThreadLocal<? > k, Object v) {super(k); value = v; }}}}Copy the code

 Set method

        ThreadLocalMap Sets the key and value
        private void set(ThreadLocal
        key, Object value) {

            Entry[] tab = table;
            int len = tab.length;
            
            // Calculate the index of key
            int i = key.threadLocalHashCode & (len-1);

            for(Entry e = tab[i]; e ! =null;
                 e = tab[i = nextIndex(i, len)]) {

                // Get the key for this loopThreadLocal<? > k = e.get();// Index value calculated by key
                // An Entry whose first Key is empty after a linear search
                if (k == key) {
                    e.value = value;
                    return;
                }
                
                // if k == null &&e! = null, indicating that k is recycled,
                // because Entry inherits WeakReference WeakReference, GC will recycle the key
                if (k == null) {
                    // The new key and value can be placed in the position that is no longer used
                    replaceStaleEntry(key, value, i);
                    return; }}// If the method does not return in the above method
            // The Entry of position I is empty. You can set key and value
            tab[i] = new Entry(key, value);
            int sz = ++size;
            
            The cleanSomeSlots method returns false to indicate that there are no more entries in the array with empty keys that need to be cleared
            // The array is full, and sz means the number of elements in the array is greater than the threshold
            // You need to call rehash for expansion
            if(! cleanSomeSlots(i, sz) && sz >= threshold)/ / capacity
                rehash();
        }
Copy the code

ReplaceStaleEntry Replacement method

private void replaceStaleEntry(ThreadLocal<? > key, Object value,int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;
    Entry e;
 
    // Clear the starting position of the element.
    int slotToExpunge = staleSlot;

    // Traverse forward until Entry is empty
    for (inti = prevIndex(staleSlot, len); (e = tab[i]) ! =null;
         i = prevIndex(i, len))
        if (e.get() == null)
            // Record the index position where the last key is null
            slotToExpunge = i;
 
    // Find either the key or trailing null slot of run, whichever
    // occurs first
    // Iterate backwards until Entry is empty
    for (inti = nextIndex(staleSlot, len); (e = tab[i]) ! =null; i = nextIndex(i, len)) { ThreadLocal<? > k = e.get();// If the Entry key is the same as the incoming key, the incoming value is replaced with the Entry value
        if (k == key) {
            e.value = value;
 
            // Switch the I and staleSlot elements (staleSlot is the first element to be cleared)
            tab[i] = tab[staleSlot];	
            tab[staleSlot] = e;
 
            // if it is equal, it means that the above traversal is not found, and the key is null.
            SlotToExpunge = I, slotToExpunge = I
            // Since the original staleSlot element has been placed in position I, there is no need to clear the elements before position I
            if (slotToExpunge == staleSlot) 
                slotToExpunge = i;

            // Clear entries with empty keys from slotToExpunge
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
            return;
        }
 
        // If the first traversal of an element with a null key is not found, the first traversal of an element with a null key is not found.
        // Set slotToExpunge to the current location
        if (k == null && slotToExpunge == staleSlot)
            slotToExpunge = i;
    }
 
    // If the key is not found, create an Entry and place it in the staleSlot location
    tab[staleSlot].value = null;
    tab[staleSlot] = new Entry(key, value);
 
    / / if slotToExpunge! =staleSlot, indicating that there are other positions in addition to the staleSlot position that need to be cleared
    // Definitions to clear: Entries with a null key. Call cleanSomeSlots to clear entries with a null key
    if(slotToExpunge ! = staleSlot) cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); }Copy the code

CleanSomeSlots cleanup method

private boolean cleanSomeSlots(int i, int n) {
    boolean removed = false;
    Entry[] tab = table;
    int len = tab.length;
    do {

        // Next index location
        i = nextIndex(i, len);
        Entry e = tab[i];

        // Iterate over the element whose key is null
        if(e ! =null && e.get() == null) {

            // reset n
            n = len;

            // Indicates a remove element
            removed = true;

            // Remove the element whose key is null after the I positioni = expungeStaleEntry(i); }}while ( (n >>>= 1) != 0);
    return removed;
}
Copy the code

 The get () method

public T get(a) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if(map ! =null) {

        // Call the getEntry method and get the corresponding Entry through this (ThreadLocal that calls the get() method)
        ThreadLocalMap.Entry e = map.getEntry(this);

        // If Entry is not empty, the destination Entry is found and its value is returned
        if(e ! =null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            returnresult; }}// If the thread's ThreadLocalMap is empty or no target Entry is found, the setInitialValue method is called
    return setInitialValue();
}
Copy the code

 SetInitialValue method

private T setInitialValue(a) {
    
    // Default null, need to override this method,
    T value = initialValue();

    // The current thread
    Thread t = Thread.currentThread();

    // Get the threadLocals of the current thread
    ThreadLocalMap map = getMap(t);

    // if threadLocals is not empty, the current ThreadLocal will be used as the key
    // null is inserted as value into ThreadLocalMap
    if(map ! =null)	
        map.set(this, value);

    // If threadLocals is empty, create a ThreadLocalMap
    // Create a new Entry into the ThreadLocalMap
    // Call ThreadLocal and value of the set method as the key and value of this Entry
    else
        createMap(t, value);
    return value;
}
Copy the code

 The getEntry method

private Entry getEntry(ThreadLocal
        key) {
	
    // Compute the index position according to the hash code
    int i = key.threadLocalHashCode & (table.length - 1);    
    Entry e = table[i];

    // If the Entry key is equal to the incoming key, it is the destination Entry and is returned directly
    if(e ! =null && e.get() == key)    
        return e;

    // Otherwise, e is not the target Entry, and the search continues after e
    else
        return getEntryAfterMiss(key, i, e);
}
Copy the code

 GetEntryAfterMiss method

private Entry getEntryAfterMiss(ThreadLocal<? > key,int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;
 
    while(e ! =null) { ThreadLocal<? > k = e.get();// Find the destination Entry and return directly
        if (k == key)  
            return e;

        // Call expungeStaleEntry to remove elements whose key is null
        if (k == null)
            expungeStaleEntry(i);
        else
            // Next index location
            i = nextIndex(i, len);
        // Entry for the next iteration
        e = tab[i];
    }
    // Return null if not found
    return null;
}
Copy the code

Remove () method

public void remove(a) {
	// Get the ThreadLocalMap of the current thread
	ThreadLocalMap m = getMap(Thread.currentThread()); 

	if(m ! =null)
        // Call ThreadLocal of this method as an input and call remove method
		m.remove(this);  
 }
 
private void remove(ThreadLocal
        key) {
    Entry[] tab = table;
    int len = tab.length;

    // Calculate the index position of the current ThreadLocal based on hashCode
    int i = key.threadLocalHashCode & (len-1);  

    // Iterates from position I until Entry is null
    for(Entry e = tab[i]; e ! =null;
         e = tab[i = nextIndex(i, len)]) {

        // find the same key
        if (e.get() == key) {
            
            // Calls the clear method, which clears references to key
            e.clear();

            // Call the expungeStaleEntry method to clear the Entry whose key is null
            expungeStaleEntry(i);
            return; }}}Copy the code

ExpungeStaleEntry method

// Starting from staleSlot, clear the Entry with an empty key,
// The non-empty element is placed in the appropriate position, and the position with empty Entry is returned
private int expungeStaleEntry(int staleSlot) {  
    Entry[] tab = table;
    int len = tab.length;
 
    // Empty the object in the staleSlot position on TAB
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;
 
    // Rehash until we encounter null
    Entry e;
    int i;
    // Iterate over the next element at position (I +1)%len
    for (i = nextIndex(staleSlot, len);
          // When Entry is empty, exit the loop and return the index position(e = tab[i]) ! =null; i = nextIndex(i, len)) { ThreadLocal<? > k = e.get();// If the key of the current Entry traversal is empty, the object in that position will be emptied
        if (k == null) {
            e.value = null;
            tab[i] = null;
            size--;
        } else {    
            // Recalculate the index position of the Entry
            int h = k.threadLocalHashCode & (len - 1);  
            
            // If the index position is not the current index position I
            if(h ! = i) {// Empty the I position object to find the correct position for the current Entry
                tab[i] = null;  
 
                // If the h position is not null, the current Entry position is searched backwards
                while(tab[h] ! =null) h = nextIndex(h, len); tab[h] = e; }}}return i;   
}
Copy the code

Rehash method

private void rehash(a) {

    // Call the expungeStaleEntries method to clean up entries with empty keys
    expungeStaleEntries();
 
    // If the size exceeds 3/4 of the threshold, expand the capacity
    if (size >= threshold - threshold / 4)  
        resize();
}
 
/** * Double the capacity of the table. */
private void resize(a) {
    Entry[] oldTab = table;
    int oldLen = oldTab.length;

    // The new table is twice as long as the old one
    int newLen = oldLen * 2;

    // Create a new table
    Entry[] newTab = new Entry[newLen];
    int count = 0;
 
    for (int j = 0; j < oldLen; ++j) {
        
        // Get the Entry of the corresponding position
        Entry e = oldTab[j];
        if(e ! =null) { ThreadLocal<? > k = e.get();// If the key is null, empty the value
            if (k == null) {
                e.value = null; // Help the GC
            } else {

            	// Compute the index position of the new table using hash code
                int h = k.threadLocalHashCode & (newLen - 1);

                // If the new table already has elements at that position, the nextIndex method is called until an empty position is found
                while(newTab[h] ! =null)
                    h = nextIndex(h, newLen);
                
                // Put the element in its placenewTab[h] = e; count++; }}}// Set the capacity expansion threshold for the new table
    setThreshold(newLen);
    / / update the size
    size = count;	
    // table points to the new table
    table = newTab;	
}
Copy the code

Memory leak problem:

static class Entry extends WeakReference<ThreadLocal<? >>{
    /** The value associated with this ThreadLocal. */Object value; Entry(ThreadLocal<? > k, Object v) {super(k); value = v; }}Copy the code

If a ThreadLocal has no external strong reference to it, the ThreadLocal will be reclaimed the next time the system GC occurs. Null key entries appear in ThreadLocalMap, and there is no way to access the values of these null key entries.

The get, set, and remove methods introduced above all clear the Entry with the null key (expungeStaleEntry method clears the value of the Entry, and these entries will be completely recycled in the next garbage collection).

However, if the current thread is running and the get, set, and remove methods are not executed, the value of the Entry with a null key will always have a strong reference: Thread Ref -> Thread -> ThreadLocalMap -> Entry -> value. As a result, the value of the Entry whose key is null cannot be reclaimed, causing memory leakage.

How do I avoid memory leaks? To avoid this, we can manually call the remove method after using ThreadLocal to avoid memory leaks.

Follow wechat public account: IT elder brother

Java Foundation, Java Web, JavaEE, including Spring Boot, etc

Reply: Resume template, you can get 100 beautiful resumes

Reply: Java learning route, you can get the latest and most complete a learning roadmap

Re: Java ebooks, get 13 must-read books for top programmers