一、使用用法

用于给每一个线程创建一份独立的变量副本,每次调用threadLocal的方法实际上都是对当前线程的变量进行操作
分析:

  • ThreadLocal内部有一个内部类ThreadLocalMap结构,ThreadLocal的相关操作都是由ThreadLocalMap来完成。
  • ThreadLocalMap类有一个 Entry[] table 数组,以数组作为数据结构,数组长度默认16,阈值默认为长度的2/3
  • Entry的key是ThreadLocal对象(key是弱引用对象),value是 threadLocal赋的值
  • Thread类有一个属性 threadLocals 指向ThreadLocalMap,threadLocal的操作,实际是是获取当前线程的ThreadLocalMap类,因此多线程对ThreadLocal的操作,实际上都是对自己线程的ThreadLocalMap进行操作,不会有线程安全问题

二、ThreadLocal源码分析

1. 属性

  1. //创建一个threadlocal对象,生成一个hashcode,
  2. //线程获取 threadLocal.get()时,如果是第一次在某个 threadLocal对象上get时,会给当前线程分配一个value,
  3. //这个value跟当前的thraedLocal对象被包装成为一个entry 其中key是threadlocal对象,value是threadLocal对象给当前线程分配的value,
  4. //这个entry存放到当前线程 threadLocals 这个map的那个桶位?与当前threadLocal对象的 threadLocalHashCode值有关系
  5. //使用threadLocalHashCode & table.length-1 得到的位置就是 当前entry存放的位置
  6. private final int threadLocalHashCode = nextHashCode();
  7. //每创建一个threadLocal对象,这个 threadLocalHashCode 就会增长
  8. //0x61c88647,值很特殊,是一个黄金分割数,使用这个hash增量的好处,就是hash分布均匀
  9. private static final int HASH_INCREMENT = 0x61c88647;
  10. private static int nextHashCode() {
  11. return nextHashCode.getAndAdd(HASH_INCREMENT);
  12. }
  13. //调用当前threadLocal对象的initialValue方法,默认初始化value 为null,一般我们需要重写
  14. protected T initialValue() {
  15. return null;
  16. }

2. get()

  1. //返回当前线程与当前ThreadLocal对象相关联的 线程局部变量,这个线程局部变量只有当前线程能够访问到
  2. public T get() {
  3. //获取当前线程
  4. Thread t = Thread.currentThread();
  5. //返回当前线程对象的ThreadLocalMap
  6. ThreadLocalMap map = getMap(t);
  7. //条件成立, 说明当前线程已经拥有自己的threadLocalMap对象了,ThreadLocalMap已经初始化过了
  8. if (map != null) {
  9. //从ThreadLocalMap根据key获取value,key是 当前ThreadLocal对象
  10. ThreadLocalMap.Entry e = map.getEntry(this);
  11. //条件成立:说明当前线程 初始化过 与当前threadLocal对象相关联的 线程局部变量
  12. if (e != null) {
  13. //返回对应的值
  14. @SuppressWarnings("unchecked")
  15. T result = (T)e.value;
  16. return result;
  17. }
  18. }
  19. //执行到这里
  20. //1. 当前线程的ThreadLocalMap 为空
  21. //2. 当前线程与当前threadlocal对象 没有设置 相关联的 键值对
  22. //进行初始化操作 包含初始化map 和初始化value两部分
  23. return setInitialValue();
  24. }

2.1 getMap()

  1. //返回当前线程的ThreadLocalMap
  2. ThreadLocalMap getMap(Thread t) {
  3. return t.threadLocals;
  4. }

2.2 setInitialValue()

  1. private T setInitialValue() {
  2. //获取初始值,留给子类重新,一般会在创建threadLocal时候重写方法
  3. T value = initialValue();
  4. //获取当前线程
  5. Thread t = Thread.currentThread();
  6. //获取当前线程的 threadLocals ThreadLocalMap对象
  7. ThreadLocalMap map = getMap(t);
  8. //如果ThreadLocalMap 不为空,只需要保存当前线程跟value
  9. if (map != null)
  10. map.set(this, value);
  11. //否则即需要创建 ThreadLocalMap,也需要生成当前线程的value
  12. else
  13. createMap(t, value);
  14. //返回当前线程与当前threadlocal对象绑定的 value
  15. return value;
  16. }

2.3 createMap()

  1. void createMap(Thread t, T firstValue) {
  2. //创建一个ThreadLocalMap,同时初始value值
  3. t.threadLocals = new ThreadLocalMap(this, firstValue);
  4. }

3.set()

  1. //修改 当前线程与当前threadlocal对象 相关联的value
  2. public void set(T value) {
  3. //获取当前线程
  4. Thread t = Thread.currentThread();
  5. //获取当前线程的threadLocalMap对象
  6. ThreadLocalMap map = getMap(t);
  7. //threadLocalMap已经初始化过了,设置值
  8. if (map != null)
  9. map.set(this, value);
  10. //否则生成map,设置值
  11. else
  12. createMap(t, value);
  13. }

4 remove()

  1. //移除当前线程与当前threadlocal对象 相关联的 value
  2. public void remove() {
  3. //获取当前线程的ThreadLocalMap对象
  4. ThreadLocalMap m = getMap(Thread.currentThread());
  5. if (m != null)
  6. //调用threadLocalMap.remove方法
  7. m.remove(this);
  8. }

5.内部类ThreadLocalMap

5.1 属性

  1. //默认容量16
  2. private static final int INITIAL_CAPACITY = 16;
  3. //threadLocalMap 散列表数组引用,长度必须是2的倍数
  4. private Entry[] table;
  5. //当前数组占用情况,存放了多少个entry
  6. private int size = 0;
  7. //扩容触发阈值,初始值为 数组长度的2/3
  8. private int threshold;
  9. //将扩容阈值设置为数组长度的三分之二
  10. private void setThreshold(int len) {
  11. threshold = len * 2 / 3;
  12. }
  13. //获取下一个位置
  14. private static int nextIndex(int i, int len) {
  15. return ((i + 1 < len) ? i + 1 : 0);
  16. }
  17. //获取前一个位置
  18. private static int prevIndex(int i, int len) {
  19. return ((i - 1 >= 0) ? i - 1 : len - 1);
  20. }
  21. //构造函数
  22. //延迟初始化,只有线程第一次调用get或set方法时候 才会去创建table数组
  23. ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
  24. //初始化散列表,长度16
  25. table = new Entry[INITIAL_CAPACITY];
  26. //计算entry的存储位置
  27. int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
  28. //创建一个entry,放到指定数组下标下面
  29. table[i] = new Entry(firstKey, firstValue);
  30. //占用情况设置为1
  31. size = 1;
  32. //设置扩容阈值
  33. setThreshold(INITIAL_CAPACITY);
  34. }

5.2 getEntry()

  1. //找到了返回entry,找不到返回null,在找的过程中如果碰到了过期数据会对过期数据进行清理并优化未过期数据保存的位置
  2. private Entry getEntry(ThreadLocal<?> key) {
  3. //根据当前线程的threadLocal对象获取entry位置
  4. int i = key.threadLocalHashCode & (table.length - 1);
  5. Entry e = table[i];
  6. if (e != null && e.get() == key)
  7. return e;
  8. else
  9. //几种情况走到这里?
  10. //1. e==null
  11. //2. e.key!=key
  12. //getEntryAfterMiss 会继续向当前桶位下标后面继续搜索 e.key==key的entery
  13. //为什么这么做?
  14. //是因为存储时 发生hash冲突时候,并没有在当前entry位置形成链表,而是 线性的向后找到一个合适的slot桶位,然后放进去
  15. return getEntryAfterMiss(key, i, e);
  16. }

5.3 getEntryAfterMiss()

  1. /*
  2. key -> threadlocal对象
  3. i -> 计算出来的数组索引
  4. e -> table[i] 的entry
  5. */
  6. private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
  7. //获取散列表
  8. Entry[] tab = table;
  9. //获取散列表数组长度
  10. int len = tab.length;
  11. //碰到entry==null 搜索结束
  12. while (e != null) {
  13. //获取当前entry的key,就是threadLocal对象
  14. ThreadLocal<?> k = e.get();
  15. //条件成立,说明向后查询过程中找到了合适的entry,直接返回entry
  16. if (k == key)
  17. return e;
  18. //条件成立,说明当前桶entry的key 关联的threalocal对象被回收了,因为key是弱引用,key=e.get()==null
  19. if (k == null)
  20. //做一次 探测式过期数据回收,从i位置开始向后清除过期数据并对未过期数据进行位置优化,直到桶位为空结束
  21. expungeStaleEntry(i);
  22. else
  23. //更新index,继续向后搜索
  24. i = nextIndex(i, len);
  25. //获取下一个i的entry
  26. e = tab[i];
  27. }
  28. //执行到这里,说明从i坐标开始之后的桶位没有找到对应的entry,返回null
  29. return null;
  30. }

5.4 expungeStaleEntry()

  1. /*
  2. 从staleSlot位置开始(包括该位置)向后清除过期数据,然后对当时存储时候发生了hash冲突的未过期数据进行优化存储位置,尽量放在原本正确位置或靠近正确位置的地方,直到碰到桶位为空,退出探测式过期数据清理逻辑
  3. return返回table数组没数据的下标
  4. */
  5. private int expungeStaleEntry(int staleSlot) {
  6. //当前table
  7. Entry[] tab = table;
  8. //table长度
  9. int len = tab.length;
  10. //回收过期数据,因为此处的数据已经是过期数据,threaLocal对象以及被回收了, 所以直接将value设置为null,help gc
  11. tab[staleSlot].value = null;
  12. //也将该桶位设置为null
  13. tab[staleSlot] = null;
  14. //数组长度减1
  15. size--;
  16. // Rehash until we encounter null
  17. //当前遍历节点
  18. Entry e;
  19. int i;
  20. //循环从当前过期数据的下一个位置开始搜索过期数据,直到碰到tab[i]==null为止
  21. for (i = nextIndex(staleSlot, len);
  22. (e = tab[i]) != null;
  23. i = nextIndex(i, len)) {
  24. //进入循环里面,当前entry一定不为空
  25. //获取当前遍历节点entry的key
  26. ThreadLocal<?> k = e.get();
  27. //条件成立:说明当前key表示的threadlocal对象为null了,表示已经threadLocal对象被回收了,说明当前entry是脏数据了。需要清理
  28. if (k == null) {
  29. //help gc...
  30. e.value = null;
  31. tab[i] = null;
  32. //长度减1
  33. size--;
  34. } else {
  35. //执行到这里,说明当前遍历的entry不是过期数据
  36. //重新获取当前entry的下标,因为当前entry 存储时候有可能遇到hash冲突了,往后偏移存储了,这个时候 应该去优化位置,让当前entry更靠近原本正确的位置,这样的话,查询起来效率更高
  37. //重新计算当前entry的索引
  38. int h = k.threadLocalHashCode & (len - 1);
  39. //条件成立,当前entry存储时候发生hash冲突了,往后偏移存储了
  40. if (h != i) {
  41. //将entry当前保存的位置 设置为null
  42. tab[i] = null;
  43. // Unlike Knuth 6.4 Algorithm R, we must scan until
  44. // null because multiple entries could have been stale.
  45. //h是正确位置,这里是从h正确位置开始,查找第一个可以保存当前entry的位置
  46. while (tab[h] != null)
  47. h = nextIndex(h, len);
  48. //将当前entry元素放入 距离正确位置最近的位置(也有可能就是正确位置)
  49. tab[h] = e;
  50. }
  51. }
  52. }
  53. return i;
  54. }

5.4 set()

  1. private void set(ThreadLocal<?> key, Object value) {
  2. //获取散列表
  3. Entry[] tab = table;
  4. //获取散列表长度
  5. int len = tab.length;
  6. int i = key.threadLocalHashCode & (len-1);
  7. //退出循环条件 table[i]桶位为空
  8. for (Entry e = tab[i];
  9. e != null;
  10. e = tab[i = nextIndex(i, len)]) {
  11. ThreadLocal<?> k = e.get();
  12. //条件成立,说明当前set操作是一个替换操作,直接替换值
  13. if (k == key) {
  14. e.value = value;
  15. return;
  16. }
  17. //条件成立,说明碰到entry是过期数据情况了
  18. if (k == null) {
  19. //替换过期数据的逻辑
  20. replaceStaleEntry(key, value, i);
  21. return;
  22. }
  23. }
  24. //执行到这里,说明是一个新数据,直接在合适的位置创建一个entry
  25. tab[i] = new Entry(key, value);
  26. int sz = ++size;
  27. //条件一 !cleanSomeSlots(i, sz) 成立说明 启发式清理 未清理到过期数据
  28. //条件二 sz >= threshold 成立 table元素个数超过阈值,进行扩容
  29. if (!cleanSomeSlots(i, sz) && sz >= threshold)
  30. rehash();
  31. }

5.5 replaceStaleEntry() 替换过期Entry

  1. /*
  2. 该方法 是在当前staleSlot位置(过期数据)开始,像后面搜索,是否有相同的key,有则将数据保存到staleSlot位置,然后搜索到的下标设置为过期数据,然后清理数据;没有向后搜索到就将数据添加到当前staleSlot位置,然后清理数据。
  3. */
  4. //staleSlot set方法查找时,发现当前桶位是过期数据的index
  5. private void replaceStaleEntry(ThreadLocal<?> key, Object value,
  6. int staleSlot) {
  7. //获取散列表
  8. Entry[] tab = table;
  9. //数组长度
  10. int len = tab.length;
  11. Entry e;
  12. //开始 探测式清理过期数据的开始下标,默认从当前staleSlot开始
  13. int slotToExpunge = staleSlot;
  14. //以当前staleSlot位置开始,向前查找有没有过期数据,for循环直到entry为空结束
  15. for (int i = prevIndex(staleSlot, len);
  16. (e = tab[i]) != null;
  17. i = prevIndex(i, len))
  18. //条件成立,说明向前找到了过期数据,更新 探测清理过期数据的开始下标为i
  19. if (e.get() == null)
  20. slotToExpunge = i;
  21. //以当前staleSlot开始,向后查找
  22. for (int i = nextIndex(staleSlot, len);
  23. (e = tab[i]) != null;
  24. i = nextIndex(i, len)) {
  25. ThreadLocal<?> k = e.get();
  26. //条件成立,说明找到相同key,是替换操作
  27. if (k == key) {
  28. //替换新数据
  29. e.value = value;
  30. //交换位置的逻辑
  31. //将tab[staleSlot]这个过期数据,放入到table[i]这个位置
  32. tab[i] = tab[staleSlot];
  33. //将匹配的entry放到tab[staleSlot]位置,这样,这个数据的位置就被优化了
  34. tab[staleSlot] = e;
  35. // Start expunge at preceding stale entry if it exists
  36. //条件成立:
  37. //1.说明一开始向前查找过期数据下标,并未找到过期数据的entry
  38. //2.向后也没有查找到过期数据
  39. if (slotToExpunge == staleSlot)
  40. //将 探测式清理过期数据的开始下标 修改为当前i
  41. slotToExpunge = i;
  42. //先探测式清理然后启发式清理
  43. cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
  44. return;
  45. }
  46. //条件1 k == null 成立 说明当前遍历的entry是过期数据
  47. //条件2 slotToExpunge == staleSlot 之前向前找过期数据下标未找到
  48. if (k == null && slotToExpunge == staleSlot)
  49. //条件成立,表明向后查找过程中找到一个过期数据了,更新slotToExpunge下标为当前i位置
  50. //前提条件是 向前查找过期数据,未找到
  51. slotToExpunge = i;
  52. }
  53. //什么时候执行到这里? 向后查找中并未找到key=e.key,说明set操作是一个添加逻辑
  54. //直接将新数据添加到tab[staleSlot],第一次的过期下标位置
  55. tab[staleSlot].value = null;
  56. tab[staleSlot] = new Entry(key, value);
  57. //条件成立,说明除了staleSlot以外,还是有其他的过期数据,要开启清理数据的逻辑
  58. if (slotToExpunge != staleSlot)
  59. //先探测式清理然后启发式清理
  60. cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
  61. }

5.6 cleanSomeSlots() 启发式清理过期Entry

  1. //int i 启发式清理工作开始位置 数组桶位元素为null的下标
  2. //int n 一般是table.length 这里也表示结束条件
  3. //返回true 表示清理过过期数据,false 未清理过过期数据
  4. private boolean cleanSomeSlots(int i, int n) {
  5. //启发式清理工作是否清除过期数据
  6. boolean removed = false;
  7. //当前散列表
  8. Entry[] tab = table;
  9. //散列表数组长度
  10. int len = tab.length;
  11. do {
  12. //获取下一个坐标
  13. i = nextIndex(i, len);
  14. Entry e = tab[i];
  15. //条件成立,当前entry的key是过期数据
  16. if (e != null && e.get() == null) {
  17. //更新结束条件位len
  18. n = len;
  19. //表示清理过数据
  20. removed = true;
  21. //从i开始 做一次 探测式清理工作
  22. i = expungeStaleEntry(i);
  23. }
  24. } while ( (n >>>= 1) != 0);
  25. return removed;
  26. }

5.7 rehash()

  1. private void rehash() {
  2. //执行该方法后,数组所有的过期数据都会被清除掉
  3. expungeStaleEntries();
  4. //条件成立 说明清理完过期数据,当前table数量仍然达到 threshold*3/4 扩容条件,真正触发扩容操作
  5. if (size >= threshold - threshold / 4)
  6. //扩容
  7. resize();
  8. }

5.8 resize()扩容

  1. //扩容
  2. private void resize() {
  3. //获取散列表
  4. Entry[] oldTab = table;
  5. //获取散列表长度
  6. int oldLen = oldTab.length;
  7. int newLen = oldLen * 2;
  8. //新数组
  9. Entry[] newTab = new Entry[newLen];
  10. //表示新table中 entry数量
  11. int count = 0;
  12. //将老表数据进行迁移
  13. for (int j = 0; j < oldLen; ++j) {
  14. //获取老表的数据
  15. Entry e = oldTab[j];
  16. if (e != null) {
  17. //获取entry的key,threadLocal对象
  18. ThreadLocal<?> k = e.get();
  19. //条件成立,说明老表中当前entry是一个过期数据,help gc
  20. if (k == null) {
  21. e.value = null; // Help the GC
  22. } else {
  23. //老表数据是非过期数据,正常数据
  24. //计算出当前entry在新表保存的位置
  25. int h = k.threadLocalHashCode & (newLen - 1);
  26. //有可能当前entry在新表中发生hash冲突了,所以找向后找一个最近的位置存放
  27. while (newTab[h] != null)
  28. h = nextIndex(h, newLen);
  29. //entry数据保存到新表中
  30. newTab[h] = e;
  31. //entry数量+1
  32. count++;
  33. }
  34. }
  35. }
  36. //设置扩容后的阈值
  37. setThreshold(newLen);
  38. //更新数组元素个数
  39. size = count;
  40. table = newTab;
  41. }

5.9 remove()

  1. private void remove(ThreadLocal<?> key) {
  2. Entry[] tab = table;
  3. int len = tab.length;
  4. int i = key.threadLocalHashCode & (len-1);
  5. for (Entry e = tab[i];
  6. e != null;
  7. e = tab[i = nextIndex(i, len)]) {
  8. //找到指定的桶位数据
  9. if (e.get() == key) {
  10. //清除
  11. e.clear();
  12. //从当前位置开始进行探测式清理过期数据
  13. expungeStaleEntry(i);
  14. return;
  15. }
  16. }
  17. }

ThreadLocal源码总结:

ThreadLocal的操作都交给了ThreadLocalMap来完成,ThreadLocalMap是一个entry数组,entry的key是ThreadLocal对象,key是一个弱引用对象,value是对应的值,当保存发生hash冲突后会继续向后查找,找到一个合适的位置保存entry。


1. get方法

  1. 先获取当前线程的ThreadLocalMap,如果map为空,表示未初始化,给当前线程创建一个map并新建一个entry,添加进去,返回value
  2. map不为空,则用threadLocal的hashcode & 数组长度-1,找到对应桶位下标,从下标位置开始循环向后查找,key相同找到返回值,找不到返回null,如果在查找过程中遇到了过期数据(key == null的数据),则从当前桶位开始,直到遇到桶位为空结束:进行探测式清理数据工作(清理key==null的桶位,并优化后面未过期数据的桶位位置,尽可能靠近原本正确的位置)

    2. set方法

  3. 获取当前线程的ThreadLocalMap,如果map为空,表示未初始化,给当前线程创建一个map并新建一个entry,添加进去

  4. 如果map不为空,则根据threadLocal的hashcode找到对应的桶位,如果桶位没数据,直接添加一个entry进去,然后进行启发式清理过期数据,清理后超过阈值,则rehash再次清理,如果再次清理之后超过了阈值的3/4,那么进行扩容;如果桶位有数据,那么从当前桶位开始向后遍历查询,找到key相同的桶位的话,则进行值更新,找的过程中如果发现了过期数据,那么从当前过期数据下标开始,进行第二次查找,这里查找分两步
  5. 第一步从当前过期数据下标开始向前查询过期数据,找到一个用变量A保存过期数据下标
  6. 第二步继续从当前过期数据下标开始遍历向后查询,直至桶位为空结束,如果找到key相同的桶位则在当前桶位更新数据,并与当前过期数据下标进行位置交换,然后探测式清理、启发式清理;找的过程中如果发现了过期数据,会用值A保存过期数据下标,如果循环结束未找到,会在当前过期数据下标进行更新,然后判断A是否与当前过期数据下标一致,不一致进行探测式清理、启发式清理过期数据

remove方法
根据threadLocal hashcode找到桶位下标,从下标开始向后循环遍历,找到了则清除entry,并从该下标开始进行探测式清理