JDK 版本 1.8

ThreadLocal 为每一个使用该变量的线程都提供了独立的副本,可以做到线程间的数据隔离,每一个线程都可以范文各自内部的副本变量。
code.7z

一、ThreadLocal 简单使用案例

02.png
从执行结果能够知道,线程间的数据是相互隔离的

二、ThreadLocal#set 分析

  1. public class ThreadLocal<T> {
  2. public void set(T value) {
  3. // ① 获取当前线程的 ThreadLocalMap
  4. Thread t = Thread.currentThread();
  5. ThreadLocalMap map = getMap(t);
  6. // ② 当前线程的 ThreadLocalMap 存在,直接往里面设值
  7. if (map != null)
  8. map.set(this, value);
  9. else
  10. // ③ 当前线程的 ThreadLocalMap 不存在,创建一个信息的 ThreadLocalMap
  11. createMap(t, value);
  12. }
  13. ThreadLocalMap getMap(Thread t) {
  14. return t.threadLocals;
  15. }
  16. void createMap(Thread t, T firstValue) {
  17. t.threadLocals = new ThreadLocalMap(this, firstValue);
  18. }
  19. }

执行流程如下
01.png
所有的操作离不开 ThreadLocalMap

三、ThreadLocal#get 分析

  1. public class ThreadLocal<T> {
  2. public T get() {
  3. // ① 获取当前线程 ThreadLocalMap
  4. Thread t = Thread.currentThread();
  5. ThreadLocalMap map = getMap(t);
  6. if (map != null) {
  7. // ② 尝试从 ThreadLocalMap 中获取相关数据,存在直接返回
  8. ThreadLocalMap.Entry e = map.getEntry(this);
  9. if (e != null) {
  10. @SuppressWarnings("unchecked")
  11. T result = (T)e.value;
  12. return result;
  13. }
  14. }
  15. return setInitialValue();
  16. }
  17. ThreadLocalMap getMap(Thread t) {
  18. return t.threadLocals;
  19. }
  20. // ③ 如果不存在进行值的初始化,同时创建 ThreadLocalMap 到当前线程中
  21. private T setInitialValue() {
  22. T value = initialValue();
  23. Thread t = Thread.currentThread();
  24. ThreadLocalMap map = getMap(t);
  25. if (map != null)
  26. map.set(this, value);
  27. else
  28. createMap(t, value);
  29. return value;
  30. }
  31. }

执行流程如下
03.png
getset 一样,所有的操作都是对 ThreadLocalMap 的操作

四、ThreadLocalMap 分析

ThreadLocalMap 是 Thread 类中的一个变量,也就是说每个线程都有自己专属的 ThreadLocalMap。 线程上下文的存值操作本质上都是在对自己线程内部的 ThreadLocalMap 做操作,从而实现的线程隔离。

4.1、ThreadLocalMap 数据结构

04.png
ThreadLocalMap 通过数组进行数据存储,而数组的结构为:Entry

Entry 分析

Entry 的数据结构为 key-value 形式,key 为 ThreadLocal<?> value 为相应的值,相关代码如下:

public class ThreadLocal<T> {
    static class ThreadLocalMap {
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
    }
}

小贴士 强引用:通常 new 出来的对象就是强引用,只要强引用存在,垃圾回收器将永远不会被引用的对象,即使内存不足 软引用:使用 SoftReference 修饰的对象被称为 软引用,软引用指向的对象在内存要溢出的时候会被回收 弱引用:使用 WeakReference 修饰的对象被称为 弱引用,只要发生垃圾回收,如果这个对象只被弱引用指向,那么就会被回收 虚引用:虚引用是最弱的引用,使用 PhantomReference 进行定义。

Entry 中的 key 采用的弱引用,而 value 使用的强引用。

小贴士 Entry key 使用的弱引用,在 gc 后一定会被回收? 当 key 没有被强引用,在 gc 之后,key 会被回收。当 key 被前引用,在 gc 后 key 不会被回收 相关代码操作如下
05.png

4.2、ThreadLocalMap Hash 算法

Hash 算法

ThreadLocalMap 中使用 Enrty[] 来存值,同时存在要将值放在哪个数组槽的问题,这时候就需要相关 Hash 算法来实现。

通过 ThreadLocalMap#set 找到相关的 Hash 算法代码如下:

public class ThreadLocal<T> {
    private final int threadLocalHashCode = nextHashCode();
    private static AtomicInteger nextHashCode = new AtomicInteger();
    private static final int HASH_INCREMENT = 0x61c88647;
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }
    static class ThreadLocalMap {
        private void set(ThreadLocal<?> key, Object value) {
            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);
            ......
        }
    }
}

根据上述的算法对长度为 16 的数组,模拟插入 16个数据,看看hash 计算到的数组下标值
06.png
通过测试能够发现 hash 之后的数据散列的很均匀,长度为 16 的数组,进行16次散列获取下标值,一次都没有重复过。

不过当存入的值越多势必会存在 冲突的问题,ThreadLocalMap 如何解决 Hash 冲突问题

Hash 的结果存在以下几种情况

第一种:直接 hash 到一个空白的位置,直接插入

逻辑图如下:
07.png
对应 ThreadLocalMap#set 部分代码如下
08.png

第二种:hash 到一个正常的位置,不过该位置 key 和要存入的值 key 相同,直接更新

逻辑图如下:
09.png
对应 ThreadLocalMap#set 部分代码如下
10.png

第三种:hash到一个正常的位置,不过该位置 key 不相同

此情况会有四种情况

  • 一、向后移动的过程中遇到空的,直接进行存值

11.png

对应 ThreadLocalMap#set 部分代码如下
12.png

  • 二、向后移动过程中遇到相同key的,直接进行更新

13.png
对应 ThreadLocalMap#set 部分代码如下
14.png

  • 三、向后移动的过程中遇到 key = null 的复杂操作
    在遇到 key =null 的时候,会通过调用 ThreadLocalMap#replaceStaleEntry完成设值操作
    主要操作的步骤有如下图代码:

15.png
主要操作如下:

1、向前遍历获取 key =null 最前面的下标值(Entrty 不为空的前提下)
16.png
对应 ThreadLocalMap#replaceStaleEntry 代码如下
17.png
2、向后遍历尝试寻找相同 key 的位置进行更新
18.png
对应 ThreadLocalMap#replaceStaleEntry 代码如下
19.png
3、向后遍历如果不存在相同 key 位置时,则直接往该位置进行添加操作,(该位置此时 key=null ,为可以被替换的数据)
20.png
对应 ThreadLocalMap#replaceStaleEntry 代码如下
21.png

4.3、ThreadLocalMap 如何防止内存泄露

ThreadLocalMap 以 key - value 的方式进行存值,其中 key 是弱引用,value 是强引用,在 key 没有指向强引用时,垃圾回收会将 key 进行回收,此时会出现 key = null value 有值的情况,如果此时 线程对象不被回收,那么 value 就会常驻内存,长期积累便会导致内存泄露问题,对此 ThreadLocalMap 提供了方案来缓解该问题。

1、在 ThreadLocal#get 时,清除已经key被垃圾回收的数据

public class ThreadLocal<T> {
    public T get() {
        ......
        ThreadLocalMap.Entry e = map.getEntry(this);
        ......
        return setInitialValue();
    }

    static class ThreadLocalMap {
        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);
        }

        private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
            Entry[] tab = table;
            int len = tab.length;
            // 查找 key 为 null 的 Entry 
            while (e != null) {
                ThreadLocal<?> k = e.get();
                if (k == key)
                    return e;
                // 将 key = null 的 Entry 删除    
                if (k == null)
                    expungeStaleEntry(i);
                else
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;
        }
    }
}

2、在 ThreadLocal#set 时,清除key已经被垃圾回收的数据

public class ThreadLocal<T> {
    public void set(T value) {
        ......
        map.set(this, value);
        ......
    }

    static class ThreadLocalMap {
        private void set(ThreadLocal<?> key, Object value) {
            ......
            if (k == key) {
                ......
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
            }
            ......
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

        private boolean cleanSomeSlots(int i, int n) {
            boolean removed = false;
            Entry[] tab = table;
            int len = tab.length;
            // 查找 key = null 的 Entry
            do {
                i = nextIndex(i, len);
                Entry e = tab[i];
                if (e != null && e.get() == null) {
                    n = len;
                    removed = true;
                    i = expungeStaleEntry(i); // 删除 key = null 的 Entry
                }
            } while ( (n >>>= 1) != 0);
            return removed;
        }
    }
}

无论是通过 get 还是 set 方法进行清除 key 被回收的数据,最终采用的是两种请求方式:

  • ThreadLocalMap#cleanSomeSlots
  • ‘ThreadLocalMap#expungeStaleEntry’

ThreadLocalMap#expungeStaleEntry

以 ‘ThreadLocalMap#get’ 中调用到的 ThreadLocalMap#expungeStaleEntry 为例

22.png
执行逻辑图如下:
23.png
ThreadLocalMap#get 在获取不到值,会进行循环遍历,在 下标位置不为空时,一直向后遍历,如果遍历得到就返回,如果遍历不到返回空,再次期间通过 ThreadLocalMap#expungeStaleEntry 进行 key =null 的 Enrty 对应的 value 进行回收。

回收代码如下:
24.png
回收的方式是一轮一轮的扫,防止遗漏,在这过程中,如果存在 key 不为 null 但是对应的 hash 值计算不一致的,进行位移处理。

类似下图:
25.png

ThreadLocalMap#cleanSomeSlots

以 ‘ThreadLocalMap#set’ 中调用的 ThreadLocalMap#cleanSomeSlots 为例

26.png
ThreadLocalMap#cleanSomeSlots 调用时配合 ThreadLocalMap#expungeStaleEntry

进行多次扫描,尽可能保证被回收的key,对应的 value 能够得到及时的回收,尽可能的防止因为 ThreadLocalMap 导致的内存泄露问题。