【Java并发编程】ThreadLocal(二):从设计思路到源码分析

ThreadLocal 系列:

在开始看源码之前,我们必须要知道 ThreadLocal 有什么作用:ThreadLocal 使同一个变量在不同线程间隔离,即每个线程都可以有自己独立的副本,然后可以在该线程的方法间共享(随时取出使用)。不明白的话可以看文章最后一部分的使用示例。

这其实是一种空间换时间的思路,因为如果每个线程都有自己独立的副本,就不用通过加锁使线程串行化执行去保证线程安全了,节省了时间,但作为代价要为每个线程开辟一块独立的空间。

了解了 ThreadLocal 的功能后,那我们该如何设计ThreadLocal?

1.如何设计线程间隔离

首先,很容易想到每个线程都必须为 ThreadLocal 开辟一块单独的内存,但仅仅开辟一块大小等于 ThreadLocal 的内存是不够的的。因为一个线程可能有多个独立的副本,换句话说就是可以在多个类中创建 ThreadLocal,比如:

public static void main(String[] args) {
	new A().f1();
	new B().f2()}

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

那么对于main线程来说,test 和 001 都是它的独立副本,都要保存起来,而他俩的区别就在于保存的 ThreadLocal 实例对象不同。

PS:在我们实际开发中,TheadLocal 会常用于拦截器中,比如 LoginInterceptor 获取到 token 中保存的 userId,然后放到一个 ThreadLocal 中,后面在需要 userId 的时候就可以通过 LoginInterceptor.threadLocal.get() 就能拿到。同样的,我可能还有一个 UVInterceptor 用于获取到请求中携带的客户唯一端标识 ID,那么我就就需要再创建一个 ThreadLocal 去保存。所以说,在一个线程中,可能会有多个 TheadLocal 实例。

接下来,我们就看看在线程(Thread类)中到底是如何保存ThreadLocal的:

在这里插入图片描述
可以看到,每个Thread维护一个ThreadLocalMap,而存储在ThreadLocalMap内的就是一个以Entry为元素的table数组(Entry就是一个key-value结构:key为ThreadLocal,value为存储的值),所以我们可以得到以下两点信息:

  1. 数组保证了每个线程可以存储多个独立的副本
  2. Entry 提供了区分不同副本方式,即ThreadLocal实例对象不同

另外,虽然这里有两个变量,但只有 threadLocals 是直接进行set/get操作的。若在父线程中创建子线程,会拷贝父线程的 inheritableThreadLocals 到子线程。

问题:创建子线程时,子线程是得不到父线程的 ThreadLocal,有什么办法可以解决这个问题?

答:可以使用 InheritableThreadLocal 来代替 ThreadLocal,ThreadLocal 和 InheritableThreadLocal 都是线程的属性,所以可以做到线程之间的数据隔离,在多线程环境下我们经常使用,但在有子线程被创建的情况下,父线程 ThreadLocal 是无法传递给子线程的,但 InheritableThreadLocal 可以,主要是因为在线程创建的过程中,会把InheritableThreadLocal 里面的所有值传递给子线程,具体代码如下:

// 当父线程的 inheritableThreadLocals 的值不为空时
// 会把 inheritableThreadLocals 里面的值全部传递给子线程
if (parent.inheritableThreadLocals != null)
    this.inheritableThreadLocals =
        ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

看源码前的要理解的逻辑终于说完了,下面进入正戏…

2.ThreadLocal

ThreadLocal 核心成员变量及主要构造函数:

// 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();
    
    // ThreadLocalMap(核心)
    // 注:虽然 ThreadLocal 是它的 Entry 的 key,但是它是 ThreadLocal 的静态内部类
    static class ThreadLocalMap{...}
    
    // 只有空参构造
    public ThreadLocal() {
    }
	
	// 计算 ThreadLocal 的 hashCode 值,就是通过CAS让 nextHashCode++
	private static int nextHashCode() {
    	return nextHashCode.getAndAdd(HASH_INCREMENT);
	}
	
	//......
}

2.1 set()

拿到当前线程的 threadLocals 并将 Entry(当前ThreadLocal对象,value)放入。另外,因为 set 操作每个线程都是串行的,所以不会有线程安全的问题

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

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

这里说一下 createMap 初始化 ThreadLocalMap,它最后会走到内部类 ThreadLocalMap 的构造函数
在这里插入图片描述
可以看到数组的初始大小跟 HashMap 一样,都是 16;值得注意的是,它的扩容阈值并不是 HashMap 那样等于数组容量乘扩容因子(一般为0.75),而是扩容阈值直接等于当前数组程度,即空间用完了就扩容(2倍)。

注:其实这样做合理,因为毕竟只有一个线程在使用,初始的 16 个位置已经够多了,没必要在我用 12/13 个时扩个容。

2.2 get()

在当前线程的 theadLocals 中获取当前ThreadLocal对象对应的value

  1. 在当前线程拿到threadLocals
  2. 若threadLocals=null,则将其初始化
  3. 通过当前ThreadLocal对象获取到相应Entry
    • entry != null ,返回result
    • entry = null ,返回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

  • 虽然是内部类,但 ThreadLocalMap 不像 List 的 Node 是 List 的组成部分(List > Node)
  • ThreadLocalMap是用来给Thread作为属性,并保存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;
		
		//.......
}

这里放一个链接吧:【数据结构】哈希表,从特性到哈希冲突再到应用,可以先看看哈希表解决冲突的几种方案,然后再看 set 方法…

3.1 set()

将 Entry(threadLocal,Object Value)放入 threadLocals的数组

  1. 获取到 threadLocals 的数组
  2. 计算当前ThreadLocal对应的数组下标
  3. 将Entry(threadLocal,Object Value)放入数组
    • 无hash碰撞,new Entry放入
    • 若出现hash碰撞,则i++,直到找到没有Entry的位置,new Entry放入(开放寻址法)
    • 若碰见key相同(ThreadLocal),则替换value
  4. 判断是否需要扩容
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
	// nextIndex(int i, int len) {return ((i + 1 < len) ? i + 1 : 0);} 如果到数组末尾了,返回 0(循环)
    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()

获取相应节点Entry

  1. 计算当前ThreadLocal对应的索引位置(hashcode 取模数组大小-1 )
  2. 若 e != null,返回当前Entry
  3. 若 e == null 或 有但key(ThreadLocal)不符,调用 getEntryAfterMiss 自旋进行寻找
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:根据 thradLocalMap set 时解决数组索引位置冲突的逻辑,该方法的寻找逻辑也是对应的,即自旋 i+1,直到找到为止

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()

ThreadLocalMap 中的 ThreadLocal 的个数超过阈值时,ThreadLocalMap 就要开始扩容了

  1. 拿到 threadLocals 的table
  2. 初始化新数组,大小为原来2倍
  3. 将老数组拷贝到新数组
    • 根据key(ThreadLocal)计算新的索引位置
    • 若出现hash碰撞,i++
  4. 计算新的扩容阈值,将新数组赋给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
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

A minor

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值