1. 大概去哪里看
ThreadLocal 其根本实现方法,是在 Thread 里面,有一个 ThreadLocal.ThreadLocalMap 属性
- ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocalMap 静态内部类维护了一个 Entry 数组
- private Entry[] table;
查看 Entry 源码,它维护了两个属性,ThreadLocal 对象 与一个 Object
- static class Entry extends WeakReference> {
- /** The value associated with this ThreadLocal. */
- Object value;
- Entry(ThreadLocal k, Object v) {
- super(k);
- value = v;
- }
- }
那么,这几项似乎可以这么串下来: Thread. currentThread().threadLocals. table{当前线程,的 ThreadLocalMap 对象,的 Entry 数组}(忽略访问权限的事儿)
------------------------------------------------ 我是分割线 ------------------------------------------------
2. 代码实现分析
ThreadLocal 提供 set(),get() 方法,用于数据的写入与读取。数据的存储与获取的位置,即
Thread. currentThread().threadLocals. table {当前线程,的 ThreadLocalMap 对象,的 Entry 数组}
- public void set(T value) {
- Thread t = Thread.currentThread();//获取当前线程t
- ThreadLocalMap map = getMap(t);//获取threadLocals 对象
- if (map != null)
- map.set(this, value);//调用 ThreadLocalMap 的set方法向 threadLocals 中写入一条数据
- else
- createMap(t, value);//如果threadLocals 为null 则为当前线程t 创建一个map,并插入数据
- }
map.set(this, value); 注意,这里,传入的第一个参数为 this 即 ThreadLocal 对象自身,
假如我们声明了一串代码:
- private static ThreadLocal<String> threadLocal = new ThreadLocal<String>();
然后我们又执行了 threadLocal.set("string 1234");
那么,在 Thread. currentThread().threadLocals. table 中,应该有这么一个 Entry :ThreadLocal 指向 threadLocal,value 为"string 1234"
分析源码(这里,所有的源码都来自于 jdk1.7.0_71):
- private void set(ThreadLocal key, Object value) {
- // We don't use a fast path as with get() because it is at
- // least as common to use set() to create new entries as
- // it is to replace existing ones, in which case, a fast
- // path would fail more often than not.
- Entry[] tab = table;
- int len = tab.length;
- int i = key.threadLocalHashCode & (len-1);
- for (Entry e = tab[i];
- e != null;
- e = tab[i = nextIndex(i, len)]) {
- ThreadLocal k = e.get();
- if (k == key) {
- e.value = value;
- return;
- }
- if (k == null) {
- replaceStaleEntry(key, value, i);
- return;
- }
- }
- tab[i] = new Entry(key, value);
- int sz = ++size;
- if (!cleanSomeSlots(i, sz) && sz >= threshold)
- rehash();
- }
分析两处:
1. int i = key.threadLocalHashCode & (len-1);
根据当前的 ThreadLocal 的 threadLocalHashCode 跟 ThreadLocalMap.table 的长度 - 1 ,按位与,获得目标索引值 i , 如果 tab[i] 为空的话,将会在 tab[i] 处插入一个 Entry ;
2. for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)])
如果如果 tab[i] 不为空,则调用 i = nextIndex(i, len) 将 i 值进行 + 1 或者置为 0,然后判断 e 是否为 null 如果 e!=null 判断 e 中的 ThreadLocal 对象,跟传入的 ThreadLocal 对象,是否为同一个对象。如果是同一个对象,则对 e 的 value 进行重新赋值。如果在遍历的过程中发现某个 e 的 ThreadLocal 对象为空,则将 Entry(threadLocal," string 1234") 设置在此时的 tab[i] 处。
(如果一开始进来的时候 e 为 null 即 tab[i]==null 。是不会走 for 循环的,会直接把 Entry(threadLocal," string 1234") 赋值到 table[i]);
- private static int nextIndex(int i, int len) {
- return ((i + 1 < len) ? i + 1 : 0);
- }
分析,为什么会有 i = nextIndex(i, len) 这样的设定。
执行 int i = key.threadLocalHashCode & (len-1); 的时候,很可能不同的 key.threadLocalHashCode 得到了相同的 i 值,那么,就从 i 开始,遍历 table 对象,找到一个可以放置 Entry(threadLocal," string 1234") 的位置,
比如:
- System.out.println(626627285 & 16 - 1); //5
- System.out.println(626627317 & 16 - 1); //5
- System.out.println(626627573 & 16 - 1); //5
这三个,获取到的 i 值,都为 5(当然实际用到的 hashCode 的算法不是这样的,不会产生这么接近的数)。
- 在同一个线程中,626627285先set(value1)了,得到5,table[5]为空,那就填进去table[5] = new Entry(626627285, value1);
- 626627317接着set(value2),算出来i = 5,
- 但是table[5]已经有人占了,那就只能看table[6]有没有空闲位置,一看table[6] == null,
- 好,就放这儿了table[6] = new Entry(626627317, value2);
- 626627573接着set(value3),算出来i = 5,
- 但是table[5]已经有人占了,那就只能看table[6]有没有空闲位置,一看table[6]也被占了,再看table[7] == null,
- 好,就放这儿了table[7] = new Entry(626627573, value3)假如有个线程算出来i = 15但是table[15] != null,
- 需要向后找空闲位置,table[16]是越界的,nextIndex返回0,从table[0]开始找
下面这串代码,维护了 table 的长度,避免了遍历了一圈 table 却找不到 table[i]==null 的情况,即保证 table 的某些索引处肯定为 null,因为还没填满的时候就已经扩容了。
- if (!cleanSomeSlots(i, sz) && sz >= threshold)
- rehash();
------------------------------------------------ 我是分割线 ------------------------------------------------
下面分析 get()
- public T get() {
- Thread t = Thread.currentThread();
- ThreadLocalMap map = getMap(t);
- if (map != null) {
- ThreadLocalMap.Entry e = map.getEntry(this);
- if (e != null) {
- @SuppressWarnings("unchecked")
- T result = (T)e.value;
- return result;
- }
- }
- return setInitialValue();
- }
1. 获取当前线程的 threadLocals 并传入 ThreadLocal 对象,获取对应的值。
2. 如果当前线程的 threadLocals 为 null ,则为当前线程 t 创建一个 map,并插入数据 setInitialValue ()=null,并返回 null
- private T setInitialValue() {
- T value = initialValue();
- Thread t = Thread.currentThread();
- ThreadLocalMap map = getMap(t);
- if (map != null)
- map.set(this, value);
- else
- createMap(t, value);
- return value;
- }
分析 map.getEntry(this)
- private Entry getEntry(ThreadLocal key) {
- int i = key.threadLocalHashCode & (table.length - 1);
- Entry e = table[i];
- if (e != null && e.get() == key)
- return e;
- else
- return getEntryAfterMiss(key, i, e);
- }
这里看到,也是先使用 int i = key.threadLocalHashCode & (table.length - 1); 得到一个索引值,然后去 table 获取 Entry 对象,得到几种结果:
1. e!=null && e.get()!=key 因为是通过"int i = key.threadLocalHashCode & (table.length - 1);" 获取的索引值,不同的 ThreadLocal 对象,可能获取到相同的索引值,所以,这种情况是存在的。
2. e==null 当前的 ThreadLocal 对象 压根儿没有 set 值。
3. e!=null&&e.get()==key 如果 Entry 对象的 key 值与当前传入的 ThreadLocal 对象,是同一个对象,则返回 e 然后在 get() 方法中,返回 e.value;
以上 1、2 两种情况的时候,会执行 getEntryAfterMiss(key, i, e);
- 在同一个线程中,626627285、626627317、626627573算到的i值均为5,但是只有626627285==table[5].get(),
- 626627317、626627573这两个,都需要走getEntryAfterMiss(key, i, e)方法了
- private Entry getEntryAfterMiss(ThreadLocal key, int i, Entry e) {
- Entry[] tab = table;
- int len = tab.length;
- while (e != null) {
- ThreadLocal k = e.get();
- if (k == key)
- return e;
- if (k == null)
- expungeStaleEntry(i);
- else
- i = nextIndex(i, len);
- e = tab[i];
- }
- return null;
- }
1. 如果初始出入的 e==null 则不进入 while 循环直接返回 null
2. 在 while 循环里面,如果 e.get()==key 即,e 的 Entry 对象的 key 值与当前传入的 ThreadLocal 对象,是同一个对象,则返回 e 。
3. 如果 e.get()==null 的情况下,先将 table[i] 置为空,然后向后遍历直到
table[nextIndex(i, len)]==null,将 table[nextIndex(i, len)]!=null 的对象,重新写入 table 中。
4. k!=key&&key!=null 的时候,则调用 i = nextIndex(i, len) 将 i 值进行 + 1 或者置为 0,然后判断 e 是否为 null 如果 e!=null 判断 e 中的 ThreadLocal 对象,跟传入的 ThreadLocal 对象,是否为同一个对象。如果是同一个对象,返回当前的 Entry 对象,如果遇到了 e==null 的时候,还没有找到目标的 Entry ,就返回 null 。
为什么找到 e==null 的地方就可以跳出了呢?
如果如果 tab[i] 不为空,则调用 i = nextIndex(i, len) 将 i 值进行 + 1 或者置为 0,然后判断 e 是否为 null 如果 e!=null 判断 e 中的 ThreadLocal 对象,跟传入的 ThreadLocal 对象,是否为同一个对象。如果是同一个对象,则对 e 的 value 进行重新赋值。如果在遍历的过程中发现某个 e 的 ThreadLocal 对象为空,则将 Entry(threadLocal," string 1234") 设置在此时的 tab[i] 处。
这里,插入的 Entry(threadLocal," string 1234") ,要么,在初始的 i 处,要么,往后 + 1 顺延,不会跳过某个索引值,然后进行赋值。所以,当 table[i]==null 的时候,已经可以不继续找了。
------------------------------------------------ 我是分割线 ------------------------------------------------
3 还有一点我觉得很重要的东西
分析到这里的时候,我们应该有发现一个很重要的问题,即:
int i = key.threadLocalHashCode & (table.length - 1);
获得的 i 值是固定的吗?
很明显不是的,因为 table 会扩容,table.length 会变,得到的 i 值也是不一样的:
- System.out.println(626627285 & 16 - 1); //5
- System.out.println(626627285 & 32 - 1); //21
这样的话,table[i] 岂不是会出错?
然后,仔细阅读源码,在进行扩容的时候,会调用 resize() 方法:
- 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 {
- int h = k.threadLocalHashCode & (newLen - 1);
- while (newTab[h] != null)
- h = nextIndex(h, newLen);
- newTab[h] = e;
- count++;
- }
- }
- }
- setThreshold(newLen);
- size = count;
- table = newTab;
- }
在 resize() 方法中,我们看到,这里新建了一个长度为原本的长度 2 倍的 Entry 数组,然后,将原 Entry 数组的所有元素,
挨个儿的重新计算索引 int h = k.threadLocalHashCode & (newLen - 1);
然后赋值原有的 Entry 到新的 Entry 数组中,这样,就保证了数组扩容之后,获取到的 i 值,是适配新数组的正确的值。
- 继续拿626627285、626627317、626627573举例,这三个,在数组扩容为长度=32的时候,算出来的i值,均为21,那么他们将会是这么算的:
- oldTab[5]!=null oldTab[5].get=626627285 算得i=21,table[21]==null table[21]=oldTab[5];
- oldTab[6]!=null oldTab[6].get=626627317 算得i=21,table[21]!=null table[22]==null table[22]=oldTab[6];
- oldTab[7]!=null oldTab[7].get=626627573 算得i=21,table[21]!=null tanle[22]!=null table[23]==null table[23]=oldTab[7];
------------------------------------------------ 我是分割线 ------------------------------------------------
以上,是我个人查看 jdk 源码,分析出来的 ThreadLocal 得实现原理与工作原理。欢迎大家批评讨论斧正。谢谢
来源: