JDK 版本 1.8

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

# ThreadLocal 简单使用案例

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

# ThreadLocal#set 分析

public class ThreadLocal<T> {

    public void set(T value) {
        // ① 获取当前线程的 ThreadLocalMap
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        // ② 当前线程的 ThreadLocalMap 存在,直接往里面设值
        if (map != null)
            map.set(this, value);
        else
            // ③ 当前线程的 ThreadLocalMap 不存在,创建一个信息的 ThreadLocalMap
            createMap(t, value);
    }

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

    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
}

执行流程如下

所有的操作离不开 ThreadLocalMap

# ThreadLocal#get 分析

public class ThreadLocal<T> {
    public T get() {
        // ① 获取当前线程 ThreadLocalMap
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            // ② 尝试从 ThreadLocalMap 中获取相关数据,存在直接返回
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }

        return setInitialValue();
    }

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

    // ③ 如果不存在进行值的初始化,同时创建 ThreadLocalMap 到当前线程中
    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;
    }
}

执行流程如下

getset 一样,所有的操作都是对 ThreadLocalMap 的操作

# ThreadLocalMap 分析

ThreadLocalMap 是 Thread 类中的一个变量,也就是说每个线程都有自己专属的 ThreadLocalMap。

线程上下文的存值操作本质上都是在对自己线程内部的 ThreadLocalMap 做操作,从而实现的线程隔离。

# ThreadLocalMap 数据结构

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 不会被回收

相关代码操作如下

# 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 计算到的数组下标值

通过测试能够发现 hash 之后的数据散列的很均匀,长度为 16 的数组,进行16次散列获取下标值,一次都没有重复过。

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

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

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

逻辑图如下:

对应 ThreadLocalMap#set 部分代码如下

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

逻辑图如下:

对应 ThreadLocalMap#set 部分代码如下

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

此情况会有四种情况

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

对应 ThreadLocalMap#set 部分代码如下

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

对应 ThreadLocalMap#set 部分代码如下

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

主要操作如下:

1、向前遍历获取 key =null 最前面的下标值(Entrty 不为空的前提下)

对应 ThreadLocalMap#replaceStaleEntry 代码如下

2、向后遍历尝试寻找相同 key 的位置进行更新

对应 ThreadLocalMap#replaceStaleEntry 代码如下

3、向后遍历如果不存在相同 key 位置时,则直接往该位置进行添加操作,(该位置此时 key=null ,为可以被替换的数据)

对应 ThreadLocalMap#replaceStaleEntry 代码如下

# 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 为例

执行逻辑图如下:

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

回收代码如下:

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

类似下图:

# ThreadLocalMap#cleanSomeSlots

以 'ThreadLocalMap#set' 中调用的 ThreadLocalMap#cleanSomeSlots 为例

ThreadLocalMap#cleanSomeSlots 调用时配合 ThreadLocalMap#expungeStaleEntry

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

精彩内容推送,请关注公众号!
最近更新时间: 5/15/2020, 10:37:21 AM