您好,欢迎访问一九零五行业门户网

Java编程中的ThreadLocal详解和源码分析

引导语threadlocal 提供了一种方式,让在多线程环境下,每个线程都可以拥有自己独特的数据,并且可以在整个线程执行过程中,从上而下的传递。
1、用法演示可能很多同学没有使用过 threadlocal,我们先来演示下 threadlocal 的用法,demo 如下:
/** * threadlocal 中保存的数据是 map */static final threadlocal<map<string, string>> context = new threadlocal<>();@testpublic void testthread() { // 从上下文中拿出 map map<string, string> contextmap = context.get(); if (collectionutils.isempty(contextmap)) { contextmap = maps.newhashmap(); } contextmap.put("key1", "value1"); context.set(contextmap); log.info("key1,value1被放到上下文中"); // 从上下文中拿出刚才放进去的数据 getfromcomtext();}private string getfromcomtext() { string value1 = context.get().get("key1"); log.info("从 threadlocal 中取出上下文,key1 对应的值为:{}", value1); return value1;}//运行结果:demo.ninth.threadlocaldemo - key1,value1被放到上下文中demo.ninth.threadlocaldemo - 从 threadlocal 中取出上下文,key1 对应的值为:value1
从运行结果中可以看到,key1 对应的值已经从上下文中拿到了。
getfromcomtext 方法是没有接受任何入参的,通过 context.get().get(“key1”) 这行代码就从上下文中拿到了 key1 的值,接下来我们一起来看下 threadlocal 底层是如何实现上下文的传递的。
2、类结构2.1、类泛型threadlocal 定义类时带有泛型,说明 threadlocal 可以储存任意格式的数据,源码如下:
public class threadlocal<t> {}
2.2、关键属性threadlocal 有几个关键属性,我们一一看下:
// threadlocalhashcode 表示当前 threadlocal 的 hashcode,用于计算当前 threadlocal 在 threadlocalmap 中的索引位置private final int threadlocalhashcode = nexthashcode();// 计算 threadlocal 的 hashcode 值(就是递增)private static int nexthashcode() { return nexthashcode.getandadd(hash_increment);}// static + atomicinteger 保证了在一台机器中每个 threadlocal 的 threadlocalhashcode 是唯一的// 被 static 修饰非常关键,因为一个线程在处理业务的过程中,threadlocalmap 是会被 set 多个 threadlocal 的,多个 threadlocal 就依靠 threadlocalhashcode 进行区分private static atomicinteger nexthashcode = new atomicinteger();

还有一个重要属性:threadlocalmap,当一个线程有多个 threadlocal 时,需要一个容器来管理多个 threadlocal,threadlocalmap 的作用就是这个,管理线程中多个 threadlocal。
2.2.1、threadlocalmapthreadlocalmap 本身就是一个简单的 map 结构,key 是 threadlocal,value 是 threadlocal 保存的值,底层是数组的数据结构,源码如下:
// threadlocalhashcode 表示当前 threadlocal 的 hashcode,用于计算当前 threadlocal 在 threadlocalmap 中的索引位置private final int threadlocalhashcode = nexthashcode();// 计算 threadlocal 的 hashcode 值(就是递增)private static int nexthashcode() { return nexthashcode.getandadd(hash_increment);}// static + atomicinteger 保证了在一台机器中每个 threadlocal 的 threadlocalhashcode 是唯一的// 被 static 修饰非常关键,因为一个线程在处理业务的过程中,threadlocalmap 是会被 set 多个 threadlocal 的,多个 threadlocal 就依靠 threadlocalhashcode 进行区分private static atomicinteger nexthashcode = new atomicinteger();

从源码中看到 threadlocalmap 其实就是一个简单的 map 结构,底层是数组,有初始化大小,也有扩容阈值大小,数组的元素是 entry,entry 的 key 就是 threadlocal 的引用,value 是 threadlocal 的值。
3、threadlocal 是如何做到线程之间数据隔离的threadlocal 是线程安全的,我们可以放心使用,主要因为是 threadlocalmap 是线程的属性,我们看下线程 thread 的源码,如下:
从上图中,我们可以看到 threadlocals.threadlocalmap 和 inheritablethreadlocals.threadlocalmap 分别是线程的属性,所以每个线程的 threadlocals 都是隔离独享的。
父线程在创建子线程的情况下,会拷贝 inheritablethreadlocals 的值,但不会拷贝 threadlocals 的值,源码如下:
从上图中我们可以看到,在线程创建时,会把父线程的 inheritablethreadlocals 属性值进行拷贝。
4、set 方法 set 方法的主要作用是往当前 threadlocal 里面 set 值,假如当前 threadlocal 的泛型是 map,那么就是往当前 threadlocal 里面 set map,源码如下:
// set 操作每个线程都是串行的,不会有线程安全的问题public void set(t value) { thread t = thread.currentthread(); threadlocalmap map = getmap(t); // 当前 thradlocal 之前有设置值,直接设置,否则初始化 if (map != null) map.set(this, value); // 初始化threadlocalmap else createmap(t, value);}
代码逻辑比较清晰,我们在一起来看下 threadlocalmap.set 的源码,如下:
private void set(threadlocal<?> key, object value) { entry[] tab = table; int len = tab.length; // 计算 key 在数组中的下标,其实就是 threadlocal 的 hashcode 和数组大小-1取余 int i = key.threadlocalhashcode & (len-1); // 整体策略:查看 i 索引位置有没有值,有值的话,索引位置 + 1,直到找到没有值的位置 // 这种解决 hash 冲突的策略,也导致了其在 get 时查找策略有所不同,体现在 getentryaftermiss 中 for (entry e = tab[i]; e != null; // nextindex 就是让在不超过数组长度的基础上,把数组的索引位置 + 1 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();}
上面源码我们注意几点:
是通过递增的 atomicinteger 作为 threadlocal 的 hashcode 的;
计算数组索引位置的公式是:hashcode 取模数组大小,由于 hashcode 不断自增,所以不同的 hashcode 大概率上会计算到同一个数组的索引位置(但这个不用担心,在实际项目中,threadlocal 都很少,基本上不会冲突);
通过 hashcode 计算的索引位置 i 处如果已经有值了,会从 i 开始,通过 +1 不断的往后寻找,直到找到索引位置为空的地方,把当前 threadlocal 作为 key 放进去。
好在日常工作中使用 threadlocal 时,常常只使用 1~2 个 threadlocal,通过 hash 计算出重复的数组的概率并不是很大。
set 时的解决数组元素位置冲突的策略,也对 get 方法产生了影响,接着我们一起来看一下 get 方法。
5、get 方法get 方法主要是从 threadlocalmap 中拿到当前 threadlocal 储存的值,源码如下:
public t get() { // 因为 threadlocal 属于线程的属性,所以需要先把当前线程拿出来 thread t = thread.currentthread(); // 从线程中拿到 threadlocalmap threadlocalmap map = getmap(t); if (map != null) { // 从 map 中拿到 entry,由于 threadlocalmap 在 set 时的 hash 冲突的策略不同,导致拿的时候逻辑也不太一样 threadlocalmap.entry e = map.getentry(this); // 如果不为空,读取当前 threadlocal 中保存的值 if (e != null) { @suppresswarnings("unchecked") t result = (t)e.value; return result; } } // 否则给当前线程的 threadlocal 初始化,并返回初始值 null return setinitialvalue();}
接着我们来看下 threadlocalmap 的 getentry 方法,源码如下:
// 得到当前 thradlocal 对应的值,值的类型是由 thradlocal 的泛型决定的// 由于 thradlocalmap set 时解决数组索引位置冲突的逻辑,导致 thradlocalmap get 时的逻辑也是对应的// 首先尝试根据 hashcode 取模数组大小-1 = 索引位置 i 寻找,找不到的话,自旋把 i+1,直到找到索引位置不为空为止private entry getentry(threadlocal<?> key) { // 计算索引位置:threadlocal 的 hashcode 取模数组大小-1 int i = key.threadlocalhashcode & (table.length - 1); entry e = table[i]; // e 不为空,并且 e 的 threadlocal 的内存地址和 key 相同,直接返回,否则就是没有找到,继续通过 getentryaftermiss 方法找 if (e != null && e.get() == key) return e; else // 这个取数据的逻辑,是因为 set 时数组索引位置冲突造成的 return getentryaftermiss(key, i, e);}
// 自旋 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;}
get 逻辑源码中注释已经写的很清楚了,我们就不重复说了。
6、扩容threadlocalmap 中的 threadlocal 的个数超过阈值时,threadlocalmap 就要开始扩容了,我们一起来看下扩容的逻辑:
//扩容private void resize() { // 拿出旧的数组 entry[] oldtab = table; int oldlen = oldtab.length; // 新数组的大小为老数组的两倍 int newlen = oldlen * 2; // 初始化新数组 entry[] newtab = new entry[newlen]; int count = 0; // 老数组的值拷贝到新数组上 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++; } } } // 给新数组初始化下次扩容阈值,为数组长度的三分之二 setthreshold(newlen); size = count; table = newtab;}
源码注解也比较清晰,我们注意两点:
扩容后数组大小是原来数组的两倍;
扩容时是绝对没有线程安全问题的,因为 threadlocalmap 是线程的一个属性,一个线程同一时刻只能对 threadlocalmap 进行操作,因为同一个线程执行业务逻辑必然是串行的,那么操作 threadlocalmap 必然也是串行的。
以上就是java编程中的threadlocal详解和源码分析的详细内容。
其它类似信息

推荐信息