菜鸟笔记
提升您的技术认知

面试题 - ThreadLocal详解

目录(jdk1.8)

  • 一、什么是ThreadLocal
  • 二、ThreadLocal怎么用
  • 三、ThreadLocal的原理
  • 四、ThreadLocal源码分析
    • 1.ThreadLocal的内部属性
    • 2.ThreadLocal 之 set() 方法
    • 3.ThreadLocal 之 get() 方法
    • 4.TreadLocal的remove方法
    • 5.内部类ThreadLocalMap的基本结构和源码分析
      • 5.1先看成员和结构部分
      • 5.3ThreadLocalMap 之 set() 方法
      • 5.4ThreadLocalMap 之 getEntry() 方法
      • 5.5ThreadLocalMap 之 rehash() 方法
      • 5.6ThreadLocalMap 之 remove(key) 方法
  • 五、什么情况下ThreadLocal的使用会导致内存泄漏
  • 六、ThreadLocal的最佳实践
  • 七、黄金分割 - 魔数0x61c88647
  • 八、总结

一、什么是ThreadLocal

ThreadLocal 是 JDK java.lang 包下的一个类,是天然的线程安全的类,

1.ThreadLoca 是线程局部变量,这个变量与普通变量的区别,在于每个访问该变量的线程,在线程内部都会
初始化一个独立的变量副本,只有该线程可以访问【get() or set()】该变量,ThreadLocal实例通常声明
为 private static。

2.线程在存活并且ThreadLocal实例可被访问时,每个线程隐含持有一个线程局部变量副本,当线程生命周期
结束时,ThreadLocal的实例的副本跟着线程一起消失,被GC垃圾回收(除非存在对这些副本的其他引用)

JDK 源码中解析:

/**
 * This class provides thread-local variables.  These variables differ from
 * their normal counterparts in that each thread that accesses one (via its
 * {@code get} or {@code set} method) has its own, independently initialized
 * copy of the variable.  {@code ThreadLocal} instances are typically private
 * static fields in classes that wish to associate state with a thread (e.g.,
 * a user ID or Transaction ID).
 * /

稍微翻译一下:ThreadLocal提供线程局部变量。这些变量与正常的变量不同,因为每一个线程在访问ThreadLocal实例的时候(通过其get或set方法)都有自己的、独立初始化的变量副本。ThreadLocal实例通常是类中的私有静态字段,使用它的目的是希望将状态(例如,用户ID或事务ID)与线程关联起来。

二、ThreadLocal怎么用

讨论ThreadLocal用在什么地方前,我们先明确下,如果仅仅就一个线程,那么都不用谈ThreadLocal的,ThreadLocal是用在多线程的场景的!!!

ThreadLocal归纳下来就3类用途:

  1. 保存线程上下文信息,在任意需要的地方可以获取!!!
  2. 线程安全的,避免某些情况需要考虑线程安全必须同步带来的性能损失!!!
  3. 线程间数据隔离

1.保存线程上下文信息,在任意需要的地方可以获取!!!
由于ThreadLocal的特性,同一线程在某地方进行设置,在随后的任意地方都可以获取到。从而可以用来保存线程上下文信息。

常用的比如每个请求怎么把一串后续关联起来,就可以用ThreadLocal进行set,在后续的任意需要记录日志的方法里面进行get获取到请求id,从而把整个请求串起来。

还有比如Spring的事务管理,用ThreadLocal存储Connection,从而各个DAO可以获取同一Connection,可以进行事务回滚,提交等操作。

2.线程安全的,避免某些情况需要考虑线程安全必须同步带来的性能损失!!!
由于不需要共享信息,自然就不存在竞争问题了,从而保证了某些情况下线程的安全,以及避免了某些情况需要考虑线程安全必须同步带来的性能损失!!!

ThreadLocal局限性
ThreadLocal为解决多线程程序的并发问题提供了一种新的思路。但是ThreadLocal也有局限性,我们来看看阿里规范:

这类场景阿里规范里面也提到了:

ThreadLocal用法

public class MyThreadLocalDemo {
  

	private static ThreadLocal<String> threadLocal = new ThreadLocal<String>();

    public static void main(String[] args) throws InterruptedException {
  
        int threads = 9;
        MyThreadLocalDemo demo = new MyThreadLocalDemo();
        CountDownLatch countDownLatch = new CountDownLatch(threads);
        for (int i = 0; i < threads; i++) {
  
            Thread thread = new Thread(() -> {
  
                threadLocal.set(Thread.currentThread().getName());
                System.out.println("threadLocal.get()================>" + threadLocal.get());
                countDownLatch.countDown();
            }, "执行线程 - " + i);
            thread.start();
        }
        countDownLatch.await();
    }

}

代码运行结果:

threadLocal.get()================>执行线程 - 1
threadLocal.get()================>执行线程 - 0
threadLocal.get()================>执行线程 - 3
threadLocal.get()================>执行线程 - 4
threadLocal.get()================>执行线程 - 5
threadLocal.get()================>执行线程 - 8
threadLocal.get()================>执行线程 - 7
threadLocal.get()================>执行线程 - 2
threadLocal.get()================>执行线程 - 6

Process finished with exit code 0

三、ThreadLocal的原理

ThreadLocal虽然叫线程局部变量,但是实际上它并不存放任何的信息,可以这样理解:它是线程(Thread)操作ThreadLocalMap中存放的变量的桥梁。它主要提供了初始化、set()、get()、remove()几个方法。这样说可能有点抽象,下面画个图说明一下在线程中使用ThreadLocal实例的set()和get()方法的简单流程图。

假设我们有如下的代码,主线程的线程名字是main(也有可能不是main):

public class Main {
  

    private static final ThreadLocal<String> LOCAL = new ThreadLocal<>();

    public static void main(String[] args) throws Exception{
  
        LOCAL.set("doge");
        System.out.println(LOCAL.get());
    }
}


上面只描述了单线程的情况并且因为是主线程忽略了Thread t = new Thread()这一步,如果有多个线程会稍微复杂一些,但是原理是不变的,ThreadLocal实例总是通过Thread.currentThread()获取到当前操作线程实例,然后去操作线程实例中的ThreadLocalMap类型的成员变量,因此它是一个桥梁,本身不具备存储功能

四、ThreadLocal源码分析

从Thread源码入手:

public class Thread implements Runnable {
  
......
//与此线程有关的ThreadLocal值。该映射由ThreadLocal类维护。
ThreadLocal.ThreadLocalMap threadLocals = null;
//与此线程有关的InheritableThreadLocal值。该Map由InheritableThreadLocal类维护
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
......
}

从上面Thread类源代码可以看出Thread类中有一个threadLocals和一个inheritableThreadLocals 变量,它们都是ThreadLocalMap类型的变量,默认情况下这两个变量都是null,只有当前线程调用ThreadLocal类的Iset或get方法时才创建它们,实际上调用这两个方法的时候,我们调用的是ThreadLocalMap类对应的get()、set()方法。

1.ThreadLocal的内部属性

ThreadLocalMap 的 key 是 ThreadLocal,但它不会传统的调用 ThreadLocal 的 hashCode 方法(继承自Object 的 hashCode),而是调用 nextHashCode() ,具体运算如下:

public class ThreadLocal<T> {
  
	//获取下一个ThreadLocal实例的哈希魔数
	private final int threadLocalHashCode = nextHashCode();
	
	//原子计数器,主要到它被定义为静态
	private static AtomicInteger nextHashCode = new AtomicInteger();
	
	//哈希魔数(增长数),也是带符号的32位整型值黄金分割值的取正
	private static final int HASH_INCREMENT = 0x61c88647;
	
	//生成下一个哈希魔数
	private static int nextHashCode() {
  
	    return nextHashCode.getAndAdd(HASH_INCREMENT);
	}
	...
}

这里需要注意一点,threadLocalHashCode是一个final的属性,而原子计数器变量nextHashCode和生成下一个哈希魔数的方法nextHashCode()是静态变量和静态方法,静态变量只会初始化一次。换而言之,每新建一个ThreadLocal实例,它内部的threadLocalHashCode就会增加0x61c88647。举个例子:

//t1中的threadLocalHashCode变量为0x61c88647
ThreadLocal t1 = new ThreadLocal();
//t2中的threadLocalHashCode变量为0x61c88647 + 0x61c88647
ThreadLocal t2 = new ThreadLocal();
//t3中的threadLocalHashCode变量为0x61c88647 + 0x61c88647 + 0x61c88647
ThreadLocal t3 = new ThreadLocal();

threadLocalHashCode是下面的ThreadLocalMap结构中使用的哈希算法的核心变量,对于每个ThreadLocal实例,它的threadLocalHashCode是唯一的。

这里写个demo看一下基于魔数 1640531527 方式产生的hash分布多均匀:


public class ThreadLocalTest {
  
    public static void main(String[] args) {
  
        printAllSlot(8);
        printAllSlot(16);
        printAllSlot(32);
    }

    static void printAllSlot(int len) {
  
        System.out.println("********** len = " + len + " ************");
        for (int i = 1; i <= 64; i++) {
  
            ThreadLocal<String> t = new ThreadLocal<>();
            int slot = getSlot(t, len);
            System.out.print(slot + " ");
            if (i % len == 0) {
  
                System.out.println(); // 分组换行
            }
        }
    }

    /**
     * 获取槽位
     *
     * @param t   ThreadLocal
     * @param len 模拟map的table的length
     * @throws Exception
     */
    static int getSlot(ThreadLocal<?> t, int len) {
  
        int hash = getHashCode(t);
        return hash & (len - 1);
    }

    /**
     * 反射获取 threadLocalHashCode 字段,因为其为private的
     */
    static int getHashCode(ThreadLocal<?> t) {
  
        Field field;
        try {
  
            field = t.getClass().getDeclaredField("threadLocalHashCode");
            field.setAccessible(true);
            return (int) field.get(t);
        } catch (Exception e) {
  
            e.printStackTrace();
        }
        return 0;
    }
}

上述代码模拟了 ThreadLocal 做为 key 的hashCode产生,看看完美槽位分配:

********** len = 8 ************
2 1 0 7 6 5 4 3 
2 1 0 7 6 5 4 3 
2 1 0 7 6 5 4 3 
2 1 0 7 6 5 4 3 
2 1 0 7 6 5 4 3 
2 1 0 7 6 5 4 3 
2 1 0 7 6 5 4 3 
2 1 0 7 6 5 4 3 
********** len = 16 ************
10 1 8 15 6 13 4 11 2 9 0 7 14 5 12 3 
10 1 8 15 6 13 4 11 2 9 0 7 14 5 12 3 
10 1 8 15 6 13 4 11 2 9 0 7 14 5 12 3 
10 1 8 15 6 13 4 11 2 9 0 7 14 5 12 3 
********** len = 32 ************
10 17 24 31 6 13 20 27 2 9 16 23 30 5 12 19 26 1 8 15 22 29 4 11 18 25 0 7 14 21 28 3 
10 17 24 31 6 13 20 27 2 9 16 23 30 5 12 19 26 1 8 15 22 29 4 11 18 25 0 7 14 21 28 3 

Process finished with exit code 0

2. ThreadLocal 之 set() 方法

ThreadLocal中set()方法的源码如下:


  protected T initialValue() {
  
        return null;
    }
    
   /**
    * 将此线程局部变量的当前线程副本设置为指定值。大多数子类将不需要
    * 重写此方法,而仅依靠{@link #initialValue} 
    * 方法来设置线程局部变量的值。
    *
    * @param value 要存储在此线程的thread-local副本中的值
    */
   public void set(T value) {
  
    //设置值前总是获取当前线程实例
    Thread t = Thread.currentThread();
    //从当前线程实例中获取threadLocals属性
    ThreadLocalMap map = getMap(t);
    if (map != null)
         //threadLocals属性不为null则覆盖key为当前的ThreadLocal实例,值为value
         map.set(this, value);
    else
    //threadLocals属性为null,则创建ThreadLocalMap,第一个项的Key为当前的ThreadLocal实例,值为value
        createMap(t, value);
	}
	
	//这里看到获取ThreadLocalMap实例时候总是从线程实例的成员变量获取
 	ThreadLocalMap getMap(Thread t) {
  
        return t.threadLocals;
    }
    
    //创建ThreadLocalMap实例的时候,会把新实例赋值到线程实例的threadLocals成员
 	void createMap(Thread t, T firstValue) {
  
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

上面的过程源码很简单,设置值的时候总是先获取当前线程实例并且操作它的变量threadLocals。步骤是:

  1. 获取当前运行线程的实例。
  2. 通过线程实例获取线程实例成员threadLocals(ThreadLocalMap),如果为null,则创建一个新的ThreadLocalMap实例赋值到threadLocals。
  3. 通过threadLocals设置值value,如果原来的哈希槽已经存在值,则进行覆盖。



3.ThreadLocal 之 get() 方法

ThreadLocal中get()方法的源码如下:


 	/**
     * 返回此线程局部变量的当前线程副本中的值。如果该变量没有当前线程的值,
     * 则首先通过调用{@link #initialValue}方法将其初始化为*返回的值。
     *
     * @return 当前线程局部变量中的值
     */
     public T get() {
  
	    //获取当前线程的实例
	    Thread t = Thread.currentThread();
	    ThreadLocalMap map = getMap(t);
	    if (map != null) {
  
	    //根据当前的ThreadLocal实例获取ThreadLocalMap中的Entry,使用的是ThreadLocalMap的getEntry方法
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
  
            @SuppressWarnings("unchecked")
            T result = (T) e.value;
             return result;
            }
        }
	    //线程实例中的threadLocals为null,则调用initialValue方法,并且创建ThreadLocalMap赋值到threadLocals
	    return setInitialValue();
	}
	
	private T setInitialValue() {
  
	    // 调用initialValue方法获取值
	    T value = initialValue();
	    Thread t = Thread.currentThread();
	    ThreadLocalMap map = getMap(t);
	    // ThreadLocalMap如果未初始化则进行一次创建,已初始化则直接设置值
	    if (map != null)
	        map.set(this, value);
	    else
	        createMap(t, value);
	    return value;
	}
	
	protected T initialValue() {
  
       return null;
    }

initialValue()方法默认返回null,如果ThreadLocal实例没有使用过set()方法直接使用get()方法,那么ThreadLocalMap中的此ThreadLocal为Key的项会把值设置为initialValue()方法的返回值。如果想改变这个逻辑可以对initialValue()方法进行覆盖。

4.TreadLocal的remove方法

ThreadLocal中remove()方法的源码如下:

public void remove() {
  
    //获取Thread实例中的ThreadLocalMap
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
       //根据当前ThreadLocal作为Key对ThreadLocalMap的元素进行移除
       m.remove(this);
}

这里罗列了 ThreadLocal 的几个public方法,其实所有工作最终都落到了 ThreadLocalMap 的头上,ThreadLocal 仅仅是从当前线程取到 ThreadLocalMap 而已,具体执行,请看下面对 ThreadLocalMap 的分析。

5.内部类ThreadLocalMap的基本结构和源码分析

ThreadLocalMap 是ThreadLocal 内部的一个Map实现,然而它并没有实现任何集合的接口规范,因为它仅供内部使用,数据结构采用 数组 + 开方地址法,Entry 继承 WeakReference,是基于 ThreadLocal 这种特殊场景实现的 Map,它的实现方式很值得研究。

ThreadLocal内部类ThreadLocalMap使用了默认修饰符,也就是包(包私有)可访问的。ThreadLocalMap内部定义了一个静态类Entry。我们重点看下ThreadLocalMap的源码,

5.1先看成员和结构部分

/**
 * ThreadLocalMap是一个定制的散列映射,仅适用于维护线程本地变量。
 * 它的所有方法都是定义在ThreadLocal类之内。
 * 它是包私有的,所以在Thread类中可以定义ThreadLocalMap作为变量。
 * 为了处理非常大(指的是值)和长时间的用途,哈希表的Key使用了弱引用(WeakReferences)。
 * 引用的队列(弱引用)不再被使用的时候,对应的过期的条目就能通过主动删除移出哈希表。
 */
static class ThreadLocalMap {
  

    //注意这里的Entry的Key为WeakReference<ThreadLocal<?>>
    static class Entry extends WeakReference<ThreadLocal<?>> {
  

        //这个是真正的存放的值
        Object value;
        // Entry的Key就是ThreadLocal实例本身,Value就是输入的值
        Entry(ThreadLocal<?> k, Object v) {
  
                    super(k);
                    value = v;
        }
    }
    //初始化容量,必须是2的幂次方
    private static final int INITIAL_CAPACITY = 16;

    //哈希(Entry)表,必须时扩容,长度必须为2的幂次方
    private Entry[] table;

    //哈希表中元素(Entry)的个数
    private int size = 0;

    //下一次需要扩容的阈值,默认值为0
    private int threshold;

    //设置下一次需要扩容的阈值,设置值为输入值len的三分之二
    private void setThreshold(int len) {
  
        threshold = len * 2 / 3;
    }

    // 以len为模增加i
    private static int nextIndex(int i, int len) {
  
        return ((i + 1 < len) ? i + 1 : 0);
    }

    // 以len为模减少i
    private static int prevIndex(int i, int len) {
  
        return ((i - 1 >= 0) ? i - 1 : len - 1);
    }
}
  1. 这里注意到十分重要的一点:ThreadLocalMap$Entry是WeakReference(弱引用),并且键值Key为ThreadLocal<?>实例本身,这里使用了无限定的泛型通配符。
  2. ThreadLocalMap 的 key 是 ThreadLocal,但它不会传统的调用 ThreadLocal 的 hashCode 方法(继承自Object 的 hashCode),而是调用 nextHashCode()

5.2接着看ThreadLocalMap的构造函数

// 构造ThreadLocal时候使用,对应ThreadLocal的实例方法void createMap(Thread t, T firstValue)
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
  
    // 哈希表默认容量为16
    table = new Entry[INITIAL_CAPACITY];
    // 计算第一个元素的哈希码
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
    setThreshold(INITIAL_CAPACITY);
}

// 构造InheritableThreadLocal时候使用,基于父线程的ThreadLocalMap里面的内容进行
// 提取放入新的ThreadLocalMap的哈希表中
// 对应ThreadLocal的静态方法static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap)
private ThreadLocalMap(ThreadLocalMap parentMap) {
  
    Entry[] parentTable = parentMap.table;
    int len = parentTable.length;
    setThreshold(len);
    table = new Entry[len];
    // 基于父ThreadLocalMap的哈希表进行拷贝
    for (Entry e : parentTable) {
  
        if (e != null) {
  
            @SuppressWarnings("unchecked")
            ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
            if (key != null) {
  
                Object value = key.childValue(e.value);
                Entry c = new Entry(key, value);
                int h = key.threadLocalHashCode & (len - 1);
                while (table[h] != null)
                    h = nextIndex(h, len);
                table[h] = c;
                size++;
            }
        }
    }
}

这里注意一下,ThreadLocal的set()方法调用的时候会懒初始化一个ThreadLocalMap并且放入第一个元素。而ThreadLocalMap的私有构造是提供给静态方法ThreadLocal#createInheritedMap()使用的。

5.3ThreadLocalMap 之 set() 方法

private void set(ThreadLocal<?> key, Object value) {
  

    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1); // 用key的hashCode计算槽位
    // hash冲突时,使用开放地址法
    // 因为独特和hash算法,导致hash冲突很少,一般不会走进这个for循环
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
  
        ThreadLocal<?> k = e.get();

        if (k == key) {
   // key 相同,则覆盖value
            e.value = value; 
            return;
        }

        if (k == null) {
   // key = null,说明 key 已经被回收了,进入替换方法
            replaceStaleEntry(key, value, i);
            return;
        }
    }
    // 新增 Entry
    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold) // 清除一些过期的值,并判断是否需要扩容
        rehash(); // 扩容
}

这个 set 方法涵盖了很多关键点:

  1. 开放地址法:与我们常用的Map不同,java里大部分Map都是用链表发解决hash冲突的,而 ThreadLocalMap 采用的是开发地址法。
  2. hash算法:hash值算法的精妙之处上面已经讲了,均匀的 hash 算法使其可以很好的配合开方地址法使用;
  3. 过期值清理

下面对 set 方法里面的几个关键方法展开:

1.replaceStaleEntry()
因为开发地址发的使用,导致 replaceStaleEntry 这个方法有些复杂,它的清理工作会涉及到slot前后的非null的slot。

//这里个方法比较长,作用是替换哈希码为staleSlot的哈希槽中Entry的值
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                               int staleSlot) {
  
    Entry[] tab = table;
    int len = tab.length;
    Entry e;

    // 往前寻找过期的slot
    int slotToExpunge = staleSlot;
    for (int i = prevIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = prevIndex(i, len))
        if (e.get() == null)
            slotToExpunge = i;

    // 找到 key 或者 直到 遇到null 的slot 才终止循环
    // 遍历staleSlot之后的哈希槽,如果Key匹配则用输入值替换
    for (int i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
  
        ThreadLocal<?> k = e.get();

        // 如果找到了key,那么需要将它与过期的 slot 交换来维护哈希表的顺序。
        // 然后可以将新过期的 slot 或其上面遇到的任何其他过期的 slot 
        // 给 expungeStaleEntry 以清除或 rehash 这个 run 中的所有其他entries。

        if (k == key) {
  
            e.value = value;

            tab[i] = tab[staleSlot];
            tab[staleSlot] = e;

            // 如果存在,则开始清除前面过期的entry
            if (slotToExpunge == staleSlot)
                slotToExpunge = i;
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
            return;
        }

        // 如果我们没有在向前扫描中找到过期的条目,
        // 那么在扫描 key 时看到的第一个过期 entry 是仍然存在于 run 中的条目。
        if (k == null && slotToExpunge == staleSlot)
            slotToExpunge = i;
    }

    // 如果没有找到 key,那么在 slot 中创建新entry
    tab[staleSlot].value = null;
    tab[staleSlot] = new Entry(key, value);

    // 如果还有其他过期的entries存在 run 中,则清除他们
    if (slotToExpunge != staleSlot)
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}

上文中的 run 不好翻译,理解为开放地址中一个slot中前后不为null的连续entry

2.cleanSomeSlots()
cleanSomeSlots 清除一些slot(一些?是不是有点模糊,到底是哪些?)

//清理第i个哈希槽之后的n个哈希槽,如果遍历的时候发现Entry的Key为null,则n会重置为哈希表的长度,
//expungeStaleEntry有可能会重哈希使得哈希表长度发生变化
private boolean cleanSomeSlots(int i, int n) {
  
    boolean removed = false;
    Entry[] tab = table;
    int len = tab.length;
    do {
  
        i = nextIndex(i, len);
        Entry e = tab[i];
        if (e != null && e.get() == null) {
  
            n = len;
            removed = true;
            i = expungeStaleEntry(i); // 清除方法 
        }
    } while ( (n >>>= 1) != 0);  // n = n / 2, 对数控制循环 
    return removed;
}

当新元素被添加时,或者另一个过期元素已被删除时,会调用cleanSomeSlots。该方法会试探性地扫描一些 entry 寻找过期的条目。它执行 对数 数量的扫描,是一种 基于不扫描(快速但保留垃圾)和 所有元素扫描之间的平衡。

上面说到的对数数量是多少?循环次数 = log2(N) (log以2为底N的对数),此处N是map的size,如:

log2(4) = 2
log2(5) = 2
log2(18) = 4

因此,此方法并没有真正的清除,只是找到了要清除的位置,而真正的清除在 expungeStaleEntry(int staleSlot) 里面

3.expungeStaleEntry(int staleSlot)

这里是真正的清除,并且不要被方法名迷惑,不仅仅会清除当前过期的slot,还回往后查找直到遇到null的slot为止。开放地址法的清除也较难理解,清除当前slot后还有往后进行rehash。

//对当前哈希表中所有的Key为null的Entry调用expungeStaleEntry
// 1.清空staleSlot对应哈希槽的Key和Value
// 2.对staleSlot到下一个空的哈希槽之间的所有可能冲突的哈希表部分槽进行重哈希,置空Key为null的槽
// 3.注意返回值是staleSlot之后的下一个空的哈希槽的哈希码
private int expungeStaleEntry(int staleSlot) {
  
    Entry[] tab = table;
    int len = tab.length;

    // 清空staleSlot对应哈希槽的Key和Value
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;

    // Rehash 直到 null 的 slot
    Entry e;
    int i;
    for (i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
  
        ThreadLocal<?> k = e.get();
        if (k == null) {
  //空key直接清除
            e.value = null;
            tab[i] = null;
            size--;
        } else {
  //key非空,则Rehash
            int h = k.threadLocalHashCode & (len - 1);
            if (h != i) {
  
                tab[i] = null;

                while (tab[h] != null)
                    h = nextIndex(h, len);
                tab[h] = e;
            }
        }
    }
    return i;
}

5.4ThreadLocalMap 之 getEntry() 方法

getEntry() 主要是在 ThreadLocal 的 get() 方法里被调用

/**
 * 这个方法主要给`ThreadLocal#get()`调用,通过当前ThreadLocal实例获取哈希表中对应的Entry
 *
 */
private Entry getEntry(ThreadLocal<?> key) {
  
    // 计算Entry的哈希值
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i]; 
    if (e != null && e.get() == key)//无hash冲突情况
        return e;
    else  // 注意这里,如果e为null或者Key对不上,表示:有hash冲突情况,会调用getEntryAfterMiss
        return getEntryAfterMiss(key, i, e);
}

// 如果Key在哈希表中找不到哈希槽的时候会调用此方法
// 这个方法是在遇到 hash 冲突时往后继续查找,并且会清除查找路上遇到的过期slot。
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
  
    Entry[] tab = table;
    int len = tab.length;
    // 这里会通过nextIndex尝试遍历整个哈希表,如果找到匹配的Key则返回Entry
    // 如果哈希表中存在Key == null的情况,调用expungeStaleEntry进行清理
    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;
}

5.5ThreadLocalMap 之 rehash() 方法

// 重哈希,必要时进行扩容
private void rehash() {
  
    // 清理所有空的哈希槽,并且进行重哈希
    expungeStaleEntries();

    // Use lower threshold for doubling to avoid hysteresis
    // 在上面的清除过程中,size会减小,在此处重新计算是否需要扩容
    // 并没有直接使用threshold,而是用较低的threshold (约 threshold 的 3/4)提前触发resize
    if (size >= threshold - threshold / 4)
        resize();
}

// 对当前哈希表中所有的Key为null的Entry调用expungeStaleEntry
private void expungeStaleEntries() {
  
    Entry[] tab = table;
    int len = tab.length;
    for (int j = 0; j < len; j++) {
  
        Entry e = tab[j];
        if (e != null && e.get() == null)
            expungeStaleEntry(j);
    }
}

// 扩容,简单的扩大2倍的容量        
private void resize() {
  
    Entry[] oldTab = table;
    int oldLen = oldTab.length;
    int newLen = oldLen * 2;
    Entry[] newTab = new Entry[newLen];
    int count = 0;

    for (Entry e : oldTab) {
  
        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;
}

PS :ThreadLocalMap 没有 影响因子 的字段,是采用直接设置 threshold 的方式,threshold = len * 2 / 3,相当于不可修改的影响因子为 2/3,比 HashMap 的默认 0.75 要低。这也是减少hash冲突的方式。

5.6ThreadLocalMap 之 remove(key) 方法

	/**
	 * Remove the entry for key.
	 */
	private void remove(ThreadLocal<?> key) {
  
	    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)]) {
  
	        if (e.get() == key) {
  
	            e.clear();
	            expungeStaleEntry(i);
	            return;
	        }
	    }
	}

remove 方法是删除特定的 ThreadLocal,建议在 ThreadLocal 使用完后一定要执行此方法。

五、什么情况下ThreadLocal的使用会导致内存泄漏

其实ThreadLocal本身不存放任何的数据,而ThreadLocal中的数据实际上是存放在线程实例中,从实际来看是线程内存泄漏,底层来看是Thread对象中的成员变量threadLocals持有大量的K-V结构,并且线程一直处于活跃状态导致变量threadLocals无法释放被回收。threadLocals持有大量的K-V结构这一点的前提是要存在大量的ThreadLocal实例的定义,一般来说,一个应用不可能定义大量的ThreadLocal,所以一般的泄漏源是线程一直处于活跃状态导致变量threadLocals无法释放被回收。但是我们知道,·ThreadLocalMap·中的Entry结构的Key用到了弱引用(·WeakReference<ThreadLocal<?>>·),当没有强引用来引用ThreadLocal实例的时候,jvm的GC会回收ThreadLocalMap中的这些Key,此时,ThreadLocalMap中会出现一些Key为null,但是Value不为null的Entry项,这些Entry项如果不主动清理,就会一直驻留在ThreadLocalMap中。也就是为什么ThreadLocal中get()、set()、remove()这些方法中都存在清理ThreadLocalMap实例key为null的代码块。总结下来,内存泄漏可能出现的地方是:

大量地(静态)初始化ThreadLocal实例,初始化之后不再调用get()、set()、remove()方法。

初始化了大量的ThreadLocal,这些ThreadLocal中存放了容量大的Value,并且使用了这些ThreadLocal实例的线程一直处于活跃的状态。
ThreadLocal中一个设计亮点是ThreadLocalMap中的Entry结构的Key用到了弱引用。试想如果使用强引用,等于ThreadLocalMap中的所有数据都是与Thread的生命周期绑定,这样很容易出现因为大量线程持续活跃导致的内存泄漏。使用了弱引用的话,JVM触发GC回收弱引用后,ThreadLocal在下一次调用get()、set()、remove()方法就可以删除那些ThreadLocalMap中Key为null的值,起到了惰性删除释放内存的作用。

其实ThreadLocal在设置内部类ThreadLocal.ThreadLocalMap中构建的Entry哈希表已经考虑到内存泄漏的问题,所以ThreadLocal.ThreadLocalMap$Entry类设计为弱引用,类签名为static class Entry extends WeakReference<ThreadLocal<?>>。之前一篇文章介绍过,如果弱引用关联的对象如果置为null,那么该弱引用会在下一次GC时候回收弱引用关联的对象。举个例子:

public class ThreadLocalMain {
  

    private static ThreadLocal<Integer> TL_1 = new ThreadLocal<>();

    public static void main(String[] args) throws Exception {
  
        TL_1.set(1);
        TL_1 = null;
        System.gc();
        Thread.sleep(300);
    }
}

这种情况下,TL_1这个ThreadLocal在主动GC之后,线程绑定的ThreadLocal.ThreadLocalMap实例中的Entry哈希表中原来的TL_1所在的哈希槽Entry的引用持有值referent(继承自WeakReference)会变成null,但是Entry中的value是强引用,还存放着TL_1这个ThreadLocal未回收之前的值。这些被”孤立”的哈希槽Entry就是前面说到的要惰性删除的哈希槽。

六、ThreadLocal的最佳实践

其实ThreadLocal的最佳实践很简单:

  • 每次使用完ThreadLocal实例,都调用它的remove()方法,清除Entry中的数据。

调用remove()方法最佳时机是线程运行结束之前的finally代码块中调用,这样能完全避免操作不当导致的内存泄漏,这种主动清理的方式比惰性删除有效。

七、黄金分割 - 魔数0x61c88647

1.黄金分割数与斐波那契数列

首先复习一下斐波那契数列,下面的推导过程来自某搜索引擎的wiki:

斐波那契数列:1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, …
通项公式:假设F(n)为该数列的第n项(n ∈ N*),那么这句话可以写成如下形式:F(n) = F(n-1) + F(n-2)。
有趣的是,这样一个完全是自然数的数列,通项公式却是用无理数来表达的。而且当n趋向于无穷大时,前一项与后一项的比值越来越逼近0.618(或者说后一项与前一项的比值小数部分越来越逼近0.618),而这个值0.618就被称为黄金分割数。证明过程如下:

黄金分割数的准确值为(根号5 - 1)/2,约等于0.618。

2.黄金分割数的应用

黄金分割数被广泛使用在美术、摄影等艺术领域,因为它具有严格的比例性、艺术性、和谐性,蕴藏着丰富的美学价值,能够激发人的美感。当然,这些不是本文研究的方向,我们先尝试求出无符号整型和带符号整型的黄金分割数的具体值:

public static void main(String[] args) throws Exception {
  
    //黄金分割数 * 2的32次方 = 2654435769 - 这个是无符号32位整数的黄金分割数对应的那个值
    long c = (long) ((1L << 32) * (Math.sqrt(5) - 1) / 2);
    System.out.println(c);
    //强制转换为带符号为的32位整型,值为-1640531527
    int i = (int) c;
    System.out.println(i);
}

通过一个线段图理解一下:

也就是2654435769为32位无符号整数的黄金分割值,而-1640531527就是32位带符号整数的黄金分割值。而ThreadLocal中的哈希魔数正是1640531527(十六进制为0x61c88647)。为什么要使用0x61c88647作为哈希魔数?这里提前说一下ThreadLocal在ThreadLocalMap(ThreadLocal在ThreadLocalMap以Key的形式存在)中的哈希求Key下标的规则:

哈希算法:keyIndex = ((i + 1) * HASH_INCREMENT) & (length - 1)

其中,i为ThreadLocal实例的个数,这里的HASH_INCREMENT就是哈希魔数0x61c88647,length为ThreadLocalMap中可容纳的Entry(K-V结构)的个数(或者称为容量)。在ThreadLocal中的内部类ThreadLocalMap的初始化容量为16,扩容后总是2的幂次方,因此我们可以写个Demo模拟整个哈希的过程:

public class Main {
  

    private static final int HASH_INCREMENT = 0x61c88647;

    public static void main(String[] args) throws Exception {
  
        hashCode(4);
        hashCode(16);
        hashCode(32);
    }

    private static void hashCode(int capacity) throws Exception {
  
        int keyIndex;
        for (int i = 0; i < capacity; i++) {
  
            keyIndex = ((i + 1) * HASH_INCREMENT) & (capacity - 1);
            System.out.print(keyIndex);
            System.out.print(" ");
        }
        System.out.println();
    }
}

上面的例子中,我们分别模拟了ThreadLocalMap容量为4,16,32的情况下,不触发扩容,并且分别”放入”4,16,32个元素到容器中,输出结果如下:

3 2 1 0 
7 14 5 12 3 10 1 8 15 6 13 4 11 2 9 0 
7 14 21 28 3 10 17 24 31 6 13 20 27 2 9 16 23 30 5 12 19 26 1 8 15 22 29 4 11 18 25 0

每组的元素经过散列算法后恰好填充满了整个容器,也就是实现了完美散列。实际上,这个并不是偶然,其实整个哈希算法可以转换为多项式证明:证明(x - y) HASH_INCREMENT != 2^n (n m),在x != y,n != m,HASH_INCREMENT为奇数的情况下恒成立,具体证明可以自行完成。HASH_INCREMENT赋值为0x61c88647的API文档注释如下:

连续生成的哈希码之间的差异(增量值),将隐式顺序线程本地id转换为几乎最佳分布的乘法哈希值,这些不同的
哈希值最终生成一个2的幂次方的哈希表。

八、总结

ThreadLocal线程本地变量是线程实例传递和存储共享变量的桥梁,真正的共享变量还是存放在线程实例本身的属性中。ThreadLocal里面的基本逻辑并不复杂,但是一旦涉及到性能影响、内存回收(弱引用)和惰性删除等环节,其实它考虑到的东西还是相对全面而且有效的。

ThreadLocalMap 的 value 清理触发时间:

  1. set(ThreadLocal<?> key, Object value)
    若无hash冲突,则先向后检测log2(N)个位置,发现过期 slot 则清除,如果没有任何 slot 被清除,则判断 size >= threshold,超过阀值会进行 rehash(),rehash()会清除所有过期的value;
  2. getEntry(ThreadLocal<?> key) (ThreadLocal 的 get() 方法调用)
    如果没有直接在hash计算的 slot 中找到entry, 则需要向后继续查找(直到null为止),查找期间发现的过期 slot 会被清除;
  3. remove(ThreadLocal<?> key)
    remove 不仅会清除需要清除的 key,还是清除hash冲突的位置的已过期的 key;
    清晰了以上过程,相信对于 ThreadLocal 的 内存溢出问题会有自己的看法。在实际开发中,不应乱用 ThreadLocal ,如果使用 ThreadLocal 发生了内存溢出,那应该考虑是否使用合理。

PS:这里的清除并不代表被回收,只是把 value 置为 null,value 的具体回收时间由 垃圾收集器 决定。