C/C++教程

ThreadLocal源码解读

本文主要是介绍ThreadLocal源码解读,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

ThreadLocal是什么呢?简单来说,它是一个线程内部的存储类

类组成:
1、无参构造方法
2、一个ThreadLocalMap静态内部类
3、ThreadLocalMap静态内部类里面存在一个Entry<ThreadLocal<?> k, Object v>[]数组
4、其他的就不细说,可以自己看源码,上面的三点是ThreadLocal在存取数据(set()和get())时,主要用到参数。
其中最重要的是set()和get()方法,下面来分析一下。

一、先分析下set()

查看代码
public void set(T value) {
	//获取当前线程t
    Thread t = Thread.currentThread();
	//然后调用getMap(t)方法见【1】
    ThreadLocalMap map = getMap(t);
	//判断获取到的ThreadLocalMap是否为null
    if (map != null)
		//不为null见【3】
        map.set(this, value);
    else
		//若为null见【2】
        createMap(t, value);
}
 
//【1】返回的是当前线程的一个类变量ThreadLocalMap threadLocals
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}
//这个就是Thread的一个类变量,初始值为null
ThreadLocal.ThreadLocalMap threadLocals = null;
 
//【2】由上面知道ThreadLocalMap初始值为null,所以会调用createMap(t, value)方法,
//这里是new了一个ThreadLocalMap实例,然后赋值给Thread类变量t.threadLocals
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}
//ThreadLocalMap构造方法,主要是创建了一个new Entry[]数组,INITIAL_CAPACITY的值为16
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】ThreadLocalMap的set()方法,逻辑如下:
private void set(ThreadLocal<?> key, Object value) {
    //获取的是的ThreadLocalMap类变量tab,由构造方法知道,table是一个长度为16的数组
    Entry[] tab = table;
    int len = tab.length;
	//根据hash值计算数组下标
    int i = key.threadLocalHashCode & (len - 1);
    //从计算的下标开始遍历整个Entry[]
    for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
        //存在当前ThreadLocal为键的数据,则覆盖掉对应的值
        ThreadLocal<?> k = e.get();
        if (k == key) {
            e.value = value;
            return;
        }
        //如果出现键为空的数据,则使用当前ThreadLocal为键覆盖掉数据(感觉是为了优化,键为空的数据,其实值已经是无用的了)
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }
    //如果遍历完后不满足,则new一个Entry并赋值
    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        //size超过设置的阈值的3/4,则会扩充
        rehash();
}

从上面可以知道,set()流程是:ThreadLocal.set()—>ThreadLocalMap.set()—>存入到Entry[]数组中,并且可以知道,
一个线程只会存在一个ThreadLocalMap,所以也只会存在一个Entry[]数组,Entry[]按不同的下标,存储了不同的ThreadLocal数据
所以一个线程可以存在多个ThreadLocal数据,以键值对的形式存储在同一个Entry[]数组,只是数组的下标不同。
那么一个ThreadLocal变量可以被不同的线程使用吗?答案是可以,这里的ThreadLocal仅仅指引用,不是指set()存储的实际值。
因为从上面知道,最终都是以键值对的形式存储在Entry[]中,ThreadLocal的引用为键,实际存储的数据为值。
(ThreadLocal本身不存储信息,它只是被作为键值对中的键,显然这个键在不同的Entry[]中是可以重复的,也就是可以被多个线程使用的)
下面用一幅图来更直观的说明一下

a、看线程1的红线和蓝线,ThreadLocal_1.set(),首先找到当前线程1,然后找到当前线程的ThreadLocalMap_1,然后通过引用哈希值按一定的规则计算数组下标 i,
然后以ThreadLocal_1引用为键,实际要存储的数据为值,存在Entry_1[ i ]位置
b、第二个ThreadLocal_2执行set()方法,和1中的步骤一样,不同点在于计算出的数组下标是 j,ThreadLocal_1和ThreadLocal_2的引用哈希值不同,
所以按相同的规则计算出来的下标是不同的,所以存在Entry_1[ j ]位置
上面的a和b说的是不同的ThreadLocal变量在同一个线程中的存储,下面说一下同一个ThreadLocal变量在不同的线程中的存储
c、看ThreadLocal_1的两条红线,和a一样,ThreadLocal_1存在线程1的Entry_1[ i ]位置
d、ThreadLocal_1.set(),在线程2中使用时,找到的是当前线程2,然后找到当前线程的ThreadLocalMap_2,然后通过引用哈希值按一定的规则计算数组下标 i,然后以ThreadLocal_1引用为键,实际要存储的数据为值,存在Entry_2[ i ]位置
这里可以发现,存储的Entry[]数组不同了,在c中是Entry_1[],在d中是Entry_2[],前面已经说过,每一个线程都有一个自己的Entry[]数组,但是他们在各自的数组中的下标是一样的,因为ThreadLocal引用是一样的,哈希值也是一样的,按相同的规则计算得到的下标也就是一样的

二、分析下get()
get()方法和set()是类似的,代码如下:

查看代码
 public T get() {
    Thread t = Thread.currentThread();
    //获取当前线程的ThreadLocalMap变量
    ThreadLocal.ThreadLocalMap map = getMap(t);
    if (map != null) {
        //然后在Entry[]数组中获取以当前ThreadLocal引用为键的数据
        ThreadLocal.ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            //返回值
            return result;
        }
    }
    //返回null
    return setInitialValue();
}

同样是获取到当前线程,然后获取到当前线程的ThreadLocalMap,然后获取到Entry[]数组,按ThreadLocal引用哈希值计算得到数组下标 i,然后获取到对应的Entry,返回里面的值。

总结:
1、ThreadLocal本身不存储实际信息,它只是被作为键值对中的键
2、一个线程可以存在多个ThreadLocal,以键值对的形式存储在同一个Entry[]数组,只是数组的下标不同
3、一个ThreadLocal可以被多个线程使用,以键值对的形式存储在不同的Entry[]数组中,数组下标相同,且Entry的键是一样的,存储的值按各个线程实际set的数据为准

三、下面介绍下ThreadLocal的简单用法
ThreadLocal在实际中如何使用?由上面的总结知道,一个ThreadLocal可以被多个线程使用的,我们只需要拿到这个ThreadLocal引用,然后调用get()方法,里面会自己去寻找各个线程的Entry[]数组,然后计算下标后取对应的值
所以可以创建一个中间类,然后新建一个ThreadLocal静态变量(静态变量只会存在一份),如果想要多个ThreadLocal可以在类中创建多个ThreadLocal,如下:

查看代码
public class ThreadLocalTest {

    private static ThreadLocal<String> threadLocal;

    private static ThreadLocal<String> threadLocal1;

    public static Object getUserId() {
        return threadLocal1.get();
    }

    public static void setUserId(String userId) {
        threadLocal1.set(userId);
    }

    public static Object getUserName() {
        return threadLocal.get();
    }

    public static void setUserName(String userName) {
        threadLocal.set(userName);
    }

    static {
        threadLocal = new ThreadLocal<>();
        threadLocal1 = new ThreadLocal<>();
    }
}

在代码中使用如下:

查看代码
public class test {

    public void testThreadLocal() {
        ThreadLocalTest.setUserName("李四");
        ThreadLocalTest.setUserId("lisi");
        new Thread(() -> {
            ThreadLocalTest.setUserName("张三");
            ThreadLocalTest.setUserId("zhangsan");
            test();
        }).start();
        test();
    }

    public void test() {
        String name = (String) ThreadLocalTest.getUserName();
        String id = (String) ThreadLocalTest.getUserId();
        System.out.println("name:" + name);
        System.out.println("id:" + id);
    }
}

运行结果如下:

这篇关于ThreadLocal源码解读的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!