Java的ThreadLocal

2019/07/28 21:30
阅读数 168

简介

ThreadLocal是一个本地线程副本变量工具类。主要用于将私有线程和该线程存放的副本对象做一个映射,各个线程之间的变量互不干扰,在高并发场景下,可以实现无状态的调用,特别适用于各个线程依赖不通的变量值完成操作的场景。

源码分析

set方法

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


  2. // We don't use a fast path as with get() because it is at

  3. // least as common to use set() to create new entries as

  4. // it is to replace existing ones, in which case, a fast

  5. // path would fail more often than not.


  6. Entry[] tab = table;

  7. int len = tab.length;

  8. // 根据 ThreadLocal 的散列值,查找对应元素在数组中的位置

  9. int i = key.threadLocalHashCode & (len - 1);


  10. // 使用线性探测法查找元素

  11. for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {

  12. ThreadLocal <?> k = e.get();

  13. // ThreadLocal 对应的 key 存在,直接覆盖之前的值

  14. if (k == key) {

  15. e.value = value;

  16. return;

  17. }

  18. // key为 null,但是值不为 null,说明之前的 ThreadLocal 对象已经被回收了,当前数组中的 Entry 是一个陈旧(stale)的元素

  19. if (k == null) {

  20. // 用新元素替换陈旧的元素,这个方法进行了不少的垃圾清理动作,防止内存泄漏,具体可以看源代码,没看太懂

  21. replaceStaleEntry(key, value, i);

  22. return;

  23. }

  24. }

  25. // ThreadLocal 对应的 key 不存在并且没有找到陈旧的元素,则在空元素的位置创建一个新的 Entry。

  26. tab[i] = new Entry(key, value);

  27. int sz = ++size;

  28. // cleanSomeSlot 清理陈旧的 Entry(key == null),具体的参考源码。如果没有清理陈旧的 Entry 并且数组中的元素大于了阈值,则进行 rehash。

  29. if (!cleanSomeSlots(i, sz) && sz >= threshold)

  30. rehash();

  31. }

set 方法大致流程如下:

1、获取当前线程的成员变量map 2、map非空,则重新将ThreadLocal和新的value副本放入到map中 3、map空,则对线程的成员变量ThreadLocalMap进行初始化创建,并将ThreadLocal和value副本放入map中。

注意:

1、int i = key.threadLocalHashCode & (len - 1);,这里实际上是对 len-1 进行了取余操作。之所以能这样取余是因为 len 的值比较特殊,是 2 的 n 次方,减 1 之后低位变为全 1,高位变为全 0。例如 16,减 1 之后对应的二进制为: 00001111,这样其他数字中大于 16 的部分就会被 0 与掉,小于 16 的部分就会保留下来,就相当于取余了。

2、在 replaceStaleEntry 和 cleanSomeSlots 方法中都会清理一些陈旧的 Entry,防止内存泄漏(关于内存泄漏,下面会讲)。

3、threshold 的值大小为threshold = len * 2 / 3;

4、rehash 方法中首先会清理陈旧的 Entry,如果清理完之后元素数量仍然大于 threshold 的 3/4,则进行扩容操作(数组大小变为原来的 2倍)。

get方法

  
    
  
  
  1. public T get() {

  2. //1. 获取当前线程的实例对象

  3. Thread t = Thread.currentThread();

  4. //2. 获取当前线程的threadLocalMap

  5. ThreadLocalMap map = getMap(t);

  6. if (map != null) {

  7. //3. 获取map中当前threadLocal实例为key的值的entry

  8. ThreadLocalMap.Entry e = map.getEntry(this);

  9. if (e != null) {

  10. @SuppressWarnings("unchecked")

  11. //4. 当前entitiy不为null的话,就返回相应的值value

  12. T result = (T)e.value;

  13. return result;

  14. }

  15. }

  16. //5. 若map为null或者entry为null的话通过该方法初始化,并返回该方法返回的value

  17. return setInitialValue();

  18. }

大致流程如下:

1、获取当前线程的ThreadLocalMap对象threadLocals 2、从map中获取线程存储的K-V Entry节点。

3、从Entry节点获取存储的Value副本值返回。

4、map为空的话返回初始值null,即线程变量副本为null,在使用时需要注意判断NullPointerException。


实现原理

ThreadLocal每个线程维护一个 ThreadLocalMap 的映射表,映射表的 key 是 ThreadLocal 实例本身,value 是要存储的副本变量。ThreadLocal 实例本身并不存储值,它只是提供一个在当前线程中找到副本值的 key。如下图所示:

我们从下面三个方面看下 ThreadLocal 的实现:

  • 存储线程副本变量的数据结构

  • 如何存取线程副本变量

  • 如何对 ThreadLocal 的实例进行 Hash

ThreadLocalMap

线程使用 ThreadLocalMap 来存储每个线程副本变量,它是 ThreadLocal 里的一个静态内部类。ThreadLocalMap 也是采用的散列表(Hash)思想来实现的,但是实现方式和 HashMap 不太一样

我们首先看下散列表的相关知识:

散列表

理想状态下,散列表就是一个包含关键字的固定大小的数组,通过使用散列函数,将关键字映射到数组的不同位置。下面是理想散列表的一个示意图:

在理想状态下,哈希函数可以将关键字均匀的分散到数组的不同位置,不会出现两个关键字散列值相同(假设关键字数量小于数组的大小)的情况。但是在实际使用中,经常会出现多个关键字散列值相同的情况(被映射到数组的同一个位置),我们将这种情况称为散列冲突。为了解决散列冲突,主要采用下面两种方式:

  • 分离链表法(separate chaining)

  • 开放定址法(open addressing)

分离链表法

分散链表法使用链表解决冲突,将散列值相同的元素都保存到一个链表中。当查询的时候,首先找到元素所在的链表,然后遍历链表查找对应的元素。下面是一个示意图:

开放定址法

开放定址法不会创建链表,当关键字散列到的数组单元已经被另外一个关键字占用的时候,就会尝试在数组中寻找其他的单元,直到找到一个空的单元。探测数组空单元的方式有很多,这里介绍一种最简单的 -- 线性探测法。线性探测法就是从冲突的数组单元开始,依次往后搜索空单元,如果到数组尾部,再从头开始搜索(环形查找)。如下图所示:

ThreadLocalMap 中使用开放地址法来处理散列冲突,而 HashMap 中使用的分离链表法。之所以采用不同的方式主要是因为:在 ThreadLocalMap 中的散列值分散的十分均匀,很少会出现冲突,并且 ThreadLocalMap 经常需要清除无用的对象,使用纯数组更加方便。

解决Hash冲突的方式就是简单的步长加1或减1,寻找下一个相邻的位置。主要逻辑如下:

  
    
  
  
  1. /**

  2. * Increment i modulo len.

  3. */

  4. private static int nextIndex(int i, int len) {

  5. return ((i + 1 < len) ? i + 1 : 0);

  6. }


  7. /**

  8. * Decrement i modulo len.

  9. */

  10. private static int prevIndex(int i, int len) {

  11. return ((i - 1 >= 0) ? i - 1 : len - 1);

  12. }

每个线程只存一个变量,这样的话所有的线程存放到map中的Key都是相同的ThreadLocal,如果一个线程要保存多个变量,就需要创建多个ThreadLocal,多个ThreadLocal放入Map中时会极大的增加Hash冲突的可能。

我们知道 Map 是一种 key-value 形式的数据结构,所以在散列数组中存储的元素也是 key-value 的形式。ThreadLocalMap 使用 Entry 类来存储数据,下面是该类的定义:

  
    
  
  
  1. static class Entry extends WeakReference <ThreadLocal <?>> {

  2. /** The value associated with this ThreadLocal. */

  3. Object value;


  4. Entry(ThreadLocal <?> k, Object v) {

  5. super(k);

  6. value = v;

  7. }

  8. }

Entry 将 ThreadLocal 实例作为 key,副本变量作为 value 存储起来。

注意 Entry 中对于 ThreadLocal 实例的引用是一个弱引用(这里为啥用弱引用,稍后会解析。),该引用定义在 Reference 类(WeakReference的父类)中,下面是 super(k) 最终调用的代码:

  
    
  
  
  1. Reference(T referent) {

  2. this(referent, null);

  3. }


  4. Reference(T referent, ReferenceQueue <? super T> queue) {

  5. this.referent = referent;

  6. this.queue = (queue == null) ? ReferenceQueue.NULL : queue;

  7. }

副本变量存取

存取的基本流程就是首先获得当前线程的 ThreadLocalMap,将 ThreadLocal 实例作为键值传入 Map,然后就是进行相关的变量存取工作了。线程中的 ThreadLocalMap 是懒加载的,只有真正的要存变量时才会调用 createMap 创建

ThreadLocal 散列值

当创建了一个 ThreadLocal 的实例后,它的散列值就已经确定了,下面是 ThreadLocal 中的实现:

  
    
  
  
  1. /**

  2. * ThreadLocals rely on per-thread linear-probe hash maps attached

  3. * to each thread (Thread.threadLocals and

  4. * inheritableThreadLocals). The ThreadLocal objects act as keys,

  5. * searched via threadLocalHashCode. This is a custom hash code

  6. * (useful only within ThreadLocalMaps) that eliminates collisions

  7. * in the common case where consecutively constructed ThreadLocals

  8. * are used by the same threads, while remaining well-behaved in

  9. * less common cases.

  10. */

  11. private final int threadLocalHashCode = nextHashCode();


  12. /**

  13. * The next hash code to be given out. Updated atomically. Starts at

  14. * zero.

  15. */

  16. private static AtomicInteger nextHashCode =

  17. new AtomicInteger();


  18. /**

  19. * The difference between successively generated hash codes - turns

  20. * implicit sequential thread-local IDs into near-optimally spread

  21. * multiplicative hash values for power-of-two-sized tables.

  22. */

  23. private static final int HASH_INCREMENT = 0x61c88647;


  24. /**

  25. * Returns the next hash code.

  26. */

  27. private static int nextHashCode() {

  28. return nextHashCode.getAndAdd(HASH_INCREMENT);

  29. }

我们看到 threadLocalHashCode 是一个常量,它通过 nextHashCode()函数产生。nextHashCode()函数其实就是在一个 AtomicInteger 变量(初始值为0)的基础上每次累加 0x61c88647,使用 AtomicInteger 为了保证每次的加法是原子操作。而 0x61c88647 这个就比较神奇了,它可以使 hashcode 均匀的分布在大小为 2 的 N 次方的数组里。其实 0x61c88647就是 FibonacciHashing

ThreadLocalMap的问题

由于ThreadLocalMap的key是弱引用,而Value是强引用。这就导致了一个问题,ThreadLocal在没有外部对象强引用时,发生GC时弱引用Key会被回收,而Value不会回收,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。

如何避免泄漏

既然Key是弱引用,那么我们要做的事,就是在调用ThreadLocal的get()、set()方法时完成后再调用remove方法,将Entry节点和Map的引用关系移除,这样整个Entry对象在GC Roots分析后就变成不可达了,下次GC的时候就可以被回收。如果使用ThreadLocal的set方法之后,没有显示的调用remove方法,就有可能发生内存泄露,所以养成良好的编程习惯十分重要,使用完ThreadLocal之后,记得调用remove方法。

  
    
  
  
  1. ThreadLocal<Session> threadLocal = new ThreadLocal<Session>();

  2. try {

  3. threadLocal.set(new Session(1, "Misout的博客"));

  4. // 其它业务逻辑

  5. } finally {

  6. threadLocal.remove();

  7. }

应用场景

还记得Hibernate的session获取场景吗?

  
    
  
  
  1. private static final ThreadLocal<Session> threadLocal = new ThreadLocal<Session>();


  2. //获取Session

  3. public static Session getCurrentSession(){

  4. Session session = threadLocal.get();

  5. //判断Session是否为空,如果为空,将创建一个session,并设置到本地线程变量中

  6. try {

  7. if(session ==null&&!session.isOpen()){

  8. if(sessionFactory==null){

  9. rbuildSessionFactory();// 创建Hibernate的SessionFactory

  10. }else{

  11. session = sessionFactory.openSession();

  12. }

  13. }

  14. threadLocal.set(session);

  15. } catch (Exception e) {

  16. // TODO: handle exception

  17. }


  18. return session;

  19. }

为什么?每个线程访问数据库都应当是一个独立的Session会话,如果多个线程共享同一个Session会话,有可能其他线程关闭连接了,当前线程再执行提交时就会出现会话已关闭的异常,导致系统异常。此方式能避免线程争抢Session,提高并发下的安全性。

使用ThreadLocal的典型场景正如上面的数据库连接管理,线程会话管理等场景,只适用于独立变量副本的情况,如果变量为全局共享的,则不适用在高并发下使用。

总结

1、每个ThreadLocal只能保存一个变量副本,如果想要上线一个线程能够保存多个副本以上,就需要创建多个ThreadLocal。

2、ThreadLocal内部的ThreadLocalMap键为弱引用,会有内存泄漏的风险。

3、适用于无状态,副本变量独立后不影响业务逻辑的高并发场景。如果如果业务逻辑强依赖于副本变量,则不适合用ThreadLocal解决,需要另寻解决方案。

参考文章

https://www.cnblogs.com/zhangjk1993/archive/2017/03/29/6641745.html https://www.jianshu.com/p/98b68c97df9b

最后

如果对 Java、大数据感兴趣请长按二维码关注一波,我会努力带给你们价值。觉得对你哪怕有一丁点帮助的请帮忙点个赞或者转发哦。关注公众号【爱编码】,小编会一直更新文章的哦。


本文分享自微信公众号 - 爱编码(ilovecode)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

展开阅读全文
打赏
0
0 收藏
分享
加载中
更多评论
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部