[JUC source code] Deep analysis of ThreadLocal source code & usage examples

Before starting to look at the source code, we must know what ThreadLocal does: ThreadLocal isolates the same variable between different threads, that is, each thread can have its own independent copy, and then it can be shared between the methods of the thread (take it out at any time use). If you don’t understand, you can see the usage example in the third part of the article.

This is actually a space-for-time idea, because if each thread has its own independent copy, there is no need to lock the thread to serialize execution to ensure thread safety, saving time, but at the cost of each Each thread opens up an independent space.

After understanding the function of ThreadLocal, how should we design ThreadLocal?

1. How to design thread isolation

First of all, it is easy to think that each thread must open up a separate memory for ThreadLocal, but it is not enough to open up a memory equal to ThreadLocal. Because a thread may have multiple independent copies, in other words, ThreadLocal can be created in multiple classes, such as:

public class A {
    
    
	private static ThreadLocal<String> threadLocal1 = new ThreadLocal<>();
	
	public void f1(){
    
     threadLocal1.set("test");
}
public class B {
    
    
	private static ThreadLocal<Integer> threadLocal2 = new ThreadLocal<>();

	public void f2(){
    
     threadLocal1.set(001);
}

Then for the thread of execution, test and 001 are independent copies of it and must be saved, and the difference between them lies in the specific ThreadLocal object.

Next, let's take a look at how to save ThreadLocal in threads (Thread class):
Insert picture description here
you can see that each Thread maintains a ThreadLocalMap, and what is stored in ThreadLocalMap is a table array with Entry as elements (Entry is A key-value structure: key is ThreadLocal, value is stored value), so we can get the following two points of information:

  1. The array ensures that each thread can store multiple independent copies
  2. Entry provides a way to distinguish different copies, that is, ThreadLocal is different

In addition, although there are two variables, only threadLocals are directly set/get operations. If a child thread is created in the parent thread, the inheritableThreadLocals of the parent thread will be copied to the child thread.

The logic to be understood before looking at the source code is finally finished, let’s enter the main show...

2.ThreadLocal

ThreadLocal core member variables and main constructor:

// ThreadLocal使用了泛型,所以可以存放任何类型
public class ThreadLocal<T> {
    
    
    
    // 当前 ThreadLocal 的 hashCode,作用是计算当前 ThreadLocal 在 ThreadLocalMap 中的索引位置
    // nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT);}
    private final int threadLocalHashCode = nextHashCode();
   
    // nextHashCode 直接决定 threadLocalHashCode(= nextHashCode++)
    // 这么做因为ThreadLocal可能在不同类中new出来多个,但线程只有一个,若每次下标都从同一位置开始,虽然有hash碰撞处理策略,但仍然会影响效率
    // static:保证了nextHashCode的唯一性,间接保证了threadHashCode唯一性
    private static AtomicInteger nextHashCode = new AtomicInteger();
    
    static class ThreadLocalMap{
    
    ...}
    
    // 只有空参构造
    public ThreadLocal() {
    
    
    }
	
	// 计算 ThreadLocal 的 hashCode 值,就是通过CAS让 nextHashCode++
	private static int nextHashCode() {
    
    
    	return nextHashCode.getAndAdd(HASH_INCREMENT);
	}
	
	//......
}

2.1 set()

Get the threadLocals of the current thread and put Entry (current ThreadLocal object, value) into it. In addition, because each thread of the set operation is serial, there will be no thread safety issues

public void set(T value) {
    
    
    // 拿到当前线程
    Thread t = Thread.currentThread();
    // 拿到当前线程的ThreadLocalMap,即threadLocals变量
    ThreadLocalMap map = getMap(t);
    
    // 当前 thradLocal 非空,即之前已经有独立的副本数据了
    if (map != null)
        map.set(this, value); // 直接将当前 threadLocal和value传入
    // 当前threadLocal为空
    else
        createMap(t, value); // 初始化ThreadLocalMap
}

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

2.2 get()

Get the value corresponding to the current ThreadLocal object in the theadLocals of the current thread

  1. Get threadLocals in the current thread
  2. If threadLocals=null, initialize it
  3. Obtain the corresponding Entry through the current ThreadLocal object
    • entry != null, return result
    • entry = null, return null
public T get() {
    
    
    // 拿出当前线程
    Thread t = Thread.currentThread();
    // 从线程中拿到 threadLocals(ThreadLocalMap)
    ThreadLocalMap map = getMap(t);
    
    if (map != null) {
    
    
        // 从 map 中拿到相应entry
        ThreadLocalMap.Entry e = map.getEntry(this);
        // 如果不为空,读取当前 ThreadLocal 中保存的值
        if (e != null) {
    
    
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    // 否则给当前线程的 ThreadLocal 初始化,并返回初始值 null
    return setInitialValue();
}

3.ThreadLocalMap

  • Although it is an internal class, ThreadLocalMap is not like List's Node, which is a component of List (List> Node)
  • ThreadLocalMap is used to give Thread as an attribute and save ThreadLocal (Thread> ThreadLocalMap> ThreadLocal)
// 静态内部类,可直接被外部调用
static class ThreadLocalMap {
    
    
        // Entry(k,v)
    	// k = WeakReference 是弱引用,当没有引用指向时,会直接被回收
        static class Entry extends WeakReference<ThreadLocal<?>> {
    
    
            // 当前 ThreadLocal 关联的值
            Object value;
            // WeakReference 的引用 referent 就是 ThreadLocal
            Entry(ThreadLocal<?> k, Object v) {
    
    
                super(k);
                value = v;
            }
        }
    	
    	// 存储 (ThreadLocal,Obj) 的数组
        private Entry[] table;
        // 数组的初始化大小
        private static final int INITIAL_CAPACITY = 16; 
        // 扩容的阈值,默认是数组大小的三分之二
        private int threshold;
		
		//.......
}

3.1 set()

Put Entry (threadLocal, Object Value) into the array of threadLocals

  1. Get the array of threadLocals
  2. Calculate the array subscript corresponding to the current ThreadLocal
  3. Put Entry (threadLocal, Object Value) into the array
    • No hash collision, new Entry put
    • If there is a hash collision, i++, until you find a position where there is no Entry, put the new Entry
    • If you encounter the same key (ThreadLocal), replace value
  4. Determine whether you need to expand
private void set(ThreadLocal<?> key, Object value) {
    
    
    // 1.拿到当前threadLocals的数组
    Entry[] tab = table;
    int len = tab.length;
    // 2.计算当前 ThreadLocal 在数组中的下标,其实就是 ThreadLocal 的 hashCode 和数组大小-1取余
    int i = key.threadLocalHashCode & (len-1);
	
	// 可以看到循环的结束条件是 tab[i]==null,即无哈希冲突
	// 若出现哈希冲突时,依次向后(i++)寻找空槽点。nextIndex方法就是让在不超过数组长度的基础上,把数组的索引位置 + 1
    for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
    
    
        ThreadLocal<?> k = e.get();
        // 找到内存地址一样的 ThreadLocal,直接替换
        if (k == key) {
    
    
            e.value = value;
            return;
        }
        // 当前 key 是 null,说明 ThreadLocal 被清理了,直接替换掉
        if (k == null) {
    
    
            replaceStaleEntry(key, value, i);
            return;
        }
    }
    // 当前 i 位置是无值的,可以被当前 thradLocal 使用
    tab[i] = new Entry(key, value);
    int sz = ++size;
    
    // 当数组大小大于等于扩容阈值(数组大小的三分之二)时,进行扩容
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

3.2 getEntry()

Get the corresponding node Entry

  1. Calculate the index position corresponding to the current ThreadLocal (hashcode modulo the array size -1)
  2. If e != null, return the current Entry
  3. If e == null or key (ThreadLocal) does not match, call getEntryAfterMiss spin to find
private Entry getEntry(ThreadLocal<?> key) {
    
    
    // 计算索引位置:ThreadLocal 的 hashCode 取模数组大小-1
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    
    // e 不为空 && e 的 ThreadLocal 的内存地址和 key 相同
    if (e != null && e.get() == key)
        return e; // 直接返回
    // 因为上面解决Hash冲突的方法是i++,所以会出现计算出的槽点为空或者不等于当前ThreadLocal的情况
    else
        return getEntryAfterMiss(key, i, e); // 继续通过 getEntryAfterMiss 方法找
}

getEntryAfterMiss: According to the logic of resolving the array index position conflict when thradLocalMap set, the search logic of this method is also corresponding, that is, spin i+1, until found

private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    
    
    Entry[] tab = table;
    int len = tab.length;
    // 在大量使用不同 key 的 ThreadLocal 时,其实还蛮耗性能的
    while (e != null) {
    
    
        ThreadLocal<?> k = e.get();
        // 内存地址一样,表示找到了
        if (k == key)
            return e;
        // 删除没用的 key
        if (k == null)
            expungeStaleEntry(i);
        // 继续使索引位置 + 1
        else
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}

3.3 resize()

When the number of ThreadLocal in ThreadLocalMap exceeds the threshold, ThreadLocalMap will start to expand

  1. Get the threadLocals table
  2. Initialize a new array, the size is 2 times the original
  3. Copy the old array to the new array
    • Calculate the new index position according to the key (ThreadLocal)
    • If there is a hash collision, i++
  4. Calculate the new expansion threshold and assign the new array to the table
private void resize() {
    
    
    // 1.拿出旧的数组
    Entry[] oldTab = table;
    int oldLen = oldTab.length;
    // 2.计算新数组的大小,为老数组的两倍
    int newLen = oldLen * 2;
    // 初始化新数组
    Entry[] newTab = new Entry[newLen];
    int count = 0;
    
    // 3.老数组的值拷贝到新数组上
    for (int j = 0; j < oldLen; ++j) {
    
    
        Entry e = oldTab[j];
        if (e != null) {
    
    
            ThreadLocal<?> k = e.get();
            if (k == null) {
    
    
                e.value = null; // Help the GC
            } else {
    
    
                // 计算 ThreadLocal 在新数组中的位置
                int h = k.threadLocalHashCode & (newLen - 1);
                // 如果出现哈希冲突,即索引 h 的位置值不为空,往后+1,直到找到值为空的索引位置
                while (newTab[h] != null)
                    h = nextIndex(h, newLen);
                // 给新数组赋值
                newTab[h] = e;
                count++;
            }
        }
    }
    // 4.计算新数组下次扩容阈值,为数组长度的三分之二
    setThreshold(newLen);
    size = count;
    table = newTab;
}

4. Example of use

Next, start 9 sub-threads, each sub-thread stores the name (thread-i) in the same variable, and then print it out. It is conceivable that if the same variable can be isolated between threads (complementary effects), the correct result of the console should be thread-0 to thread-8.

The two implementations of this variable are demonstrated below: 1. Ordinary variable String, 2.ThreadLocal<String>

4.1 Ordinary variables

public class StringTest {
    
    
	// 保存线程名的普通变量value
    private String value;
	// 不直接设置value,而是暴露出get和set方法
    private String getString() {
    
     return string; }
    private void setString(String string) {
    
     this.string = string; }

    public static void main(String[] args) {
    
    
    
        StringTest test= new StringTest ();
        
        int threads = 9; // 要启动的线程个数
        CountDownLatch countDownLatch = new CountDownLatch(threads); // countDownLatch 用于防止主线程在子线程未完成前结束
       
        // 启动9个子线程
        for (int i = 0; i < threads; i++) {
    
    
            Thread thread = new Thread(() -> {
    
    
                test.setString(Thread.currentThread().getName()); // 向变量value中存入线程名 thread - i
                System.out.println(test.getString()); // 然后打印出来。注:这里可能存在并发
                countDownLatch.countDown(); // 门栓-1
            }, "thread - " + i); 
            thread.start();
        }
    }

	countDownLatch.await(); // 等countDownLatch为0时,主线程恢复运行
}

The results are as follows:

thread - 1
thread - 2
thread - 1
thread - 3
thread - 4
thread - 5
thread - 6
thread - 7
thread - 8

You can see that there is no thread-0, but thread-1 appears twice, so the same variable cannot be isolated from different threads by using ordinary variables.

4.2 ThreadLocal

When using ThreadLocal, it is generally declared as static

  • One ThreadLocal for each class -> one Entry for the current thread is enough
  • Easy to call when using
public class ThreadLocalStringTest {
    
    
	// 保存线程名的ThreadLocal变量threadLocal
	// 注:这里除了是String,也可是别的任何类型(Integer,List,Map...)
    private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
	// 不直接操作 threadLocal,而是封装成 set/get 方法
    private String getString() {
    
     return threadLocal.get(); }
    private void setString(String string) {
    
     threadLocal.set(string);}

    public static void main(String[] args) {
    
    
    
        ThreadLocalStringTest test= new ThreadLocalStringTest();
        
        int threads = 9; // 要创建的子线程个数
        CountDownLatch countDownLatch = new CountDownLatch(threads); // countDownLatch 用于防止主线程在子线程未完成前结束
		
        // 创建 9 个线程
        for (int i = 0; i < threads; i++) {
    
    
            Thread thread = new Thread(() -> {
    
    
                test.setString(Thread.currentThread().getName()); // 向ThreadLocal中存入当前线程名 thread - i
                System.out.println(test.getString()); // 向ThreadLocal获取刚存的线程名。注:可能存在并发
                countDownLatch.countDown(); // 门栓-1
            }, "thread - " + i);
            thread.start();
        }
		
		countDownLatch.await(); // 等countDownLatch为0时,主线程恢复运行
    }

}

operation result:

thread - 0
thread - 1
thread - 2
thread - 3
thread - 4
thread - 5
thread - 6
thread - 7
thread - 8

You can see that the running result is in line with expectations, that is, ThreadLocal realizes the isolation of the same variable between threads. In addition, ThreadLocal can also be used for single sign-on, to save the tokens of different request threads, and then take them out during parsing.

Guess you like

Origin blog.csdn.net/weixin_43935927/article/details/108889167