threadLocal源码解析

1、threadLocal运用的场景

最常见的ThreadLocal使用场景为 用来解决 数据库连接、Session管理等

public class ThradLocalTest {

    private static ThreadLocal<Connection> connectionHolder
            = new ThreadLocal<Connection>() {
        @Override
        public Connection initialValue() {
            try {
                return DriverManager.getConnection("123");
            } catch (SQLException e) {
                e.printStackTrace();
            }
            return null;
        }
    };

    public static Connection getConnection() {
        return connectionHolder.get();
    }
}

Session管理:

http://www.iteye.com/topic/103804

private static final ThreadLocal threadSession = new ThreadLocal();
 
public static Session getSession() throws InfrastructureException {
    Session s = (Session) threadSession.get();
    try {
        if (s == null) {
            s = getSessionFactory().openSession();
            threadSession.set(s);
        }
    } catch (HibernateException ex) {
        throw new InfrastructureException(ex);
    }
    return s;
}

2、分析单线程下数据库连接写法的问题

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

public class ThradLocalTest {
    private static Connection connect = null;

    public static Connection openConnection() throws SQLException {
        if (connect == null) {
            connect = DriverManager.getConnection("abc");
        }
        return connect;
    }

    public static void closeConnection() throws SQLException {
        if (connect != null) {
            connect.close();
        }
    }
}

假设有这样一个数据库链接管理类,这段代码在单线程中使用是没有任何问题的,但是如果在多线程中使用呢?很显然,在多线程中使用会存在线程安全问题:第一,这里面的2个方法都没有进行同步,很可能在openConnection方法中会多次创建connect;第二,由于connect是共享变量,那么必然在调用connect的地方需要使用到同步来保障线程安全,因为很可能一个线程在使用connect进行数据库操作,而另外一个线程调用closeConnection关闭链接。

  所以出于线程安全的考虑,必须将这段代码的两个方法进行同步处理,并且在调用connect的地方需要进行同步处理。

  这样将会大大影响程序执行效率,因为一个线程在使用connect进行数据库操作的时候,其他线程只有等待。

  那么大家来仔细分析一下这个问题,这地方到底需不需要将connect变量进行共享?事实上,是不需要的。假如每个线程中都有一个connect变量,各个线程之间对connect变量的访问实际上是没有依赖关系的,即一个线程不需要关心其他线程是否对这个connect进行了修改的。

3、源码剖析

3.1、先看ThreadLocal类

public class ThreadLocal<T> {
}

说明:可见它就是一个普通的类,并没有实现任何接口、也无父类继承。

3.2、构造器

 /**
     * Creates a thread local variable.
     * @see #withInitial(java.util.function.Supplier)
     */
    public ThreadLocal() {
    }

3.3、ThreadLocalMap

3.3.1、主要代码

public class ThreadLocal<T> {
  //要给出的下一个哈希码。 原子更新。 开始于零。
  private static AtomicInteger nextHashCode =
    new AtomicInteger();
    // 生成 ThreadLocal 的哈希码,用于计算在 Entry 数组中的位置
    private final int threadLocalHashCode = nextHashCode();

    private static final int HASH_INCREMENT = 0x61c88647;

    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

    // ...
    
    static class ThreadLocalMap {

        static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
    
        // 初始容量,必须是 2 的次幂
        private static final int INITIAL_CAPACITY = 16;
    
        // 存储数据的数组
        private Entry[] table;

        // table 中的 Entry 数量
        private int size = 0;

        // 扩容的阈值
        private int threshold; // Default to 0
    
        // 设置扩容阈值
        private void setThreshold(int len) {
            threshold = len * 2 / 3;
        }    
    
        // 第一次添加元素使用的构造器
        ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }
        
        // ...
    }
}

ThreadLocalMap 的内部结构其实跟 HashMap 很类似,可以对比前面「https://www.cnblogs.com/lhicp/p/14918116.html」对 HashMap 的分析。

二者都是「键-值对」构成的数组,对哈希冲突的处理方式不同,导致了它们在结构上产生了一些区别:

  1. HashMap 处理哈希冲突使用的「链表法」。也就是当产生冲突时拉出一个链表,而且 JDK 1.8 进一步引入了红黑树进行优化。
  2. ThreadLocalMap 则使用了「开放寻址法」中的「线性探测」。即,当某个位置出现冲突时,从当前位置往后查找,直到找到一个空闲位置。

3.3.2 注意事项

  • 弱引用
ThreadLocalMap中的代码
 static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

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

点开super引用父类的Reference

  /* -- Constructors -- */

    Reference(T referent) {
        this(referent, null);
    }

    Reference(T referent, ReferenceQueue<? super T> queue) {
        this.referent = referent;
        this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
    }

有个值得注意的地方是:ThreadLocalMap 的 Entry 继承了 WeakReference 类,也就是弱引用类型。

跟进 Entry 的父类,可以看到 ThreadLocal 最终赋值给了 WeakReference 的父类 Reference 的 referent 属性。即,可以认为 Entry 持有了两个对象的引用:ThreadLocal 类型的「弱引用」和 Object 类型的「强引用」,其中 ThreadLocal 为 key,Object 为 value。如图所示:

threadLocal源码解析

 

 

ThreadLocal 在某些情况可能产生的「内存泄漏」就跟这个「弱引用」有关,后面再展开分析。

  • 寻址

Entry 的 key 是 ThreadLocal 类型的,它是如何在数组中散列的呢?

ThreadLocal 有个 threadLocalHashCode 变量,每次创建 ThreadLocal 对象时,这个变量都会增加一个固定的值 HASH_INCREMENT,即 0x61c88647,这个数字似乎跟黄金分割、斐波那契数有关,但这不是重点,有兴趣的朋友可以去深入研究下,这里我们知道它的目的就行了。与 HashMap 的 hash 算法的目的近似,就是为了散列的更均匀。

threadLocal源码解析
魔数0x61c88647
生成hash code间隙为这个魔数,可以让生成出来的值或者说ThreadLocal的ID较为均匀地分布在2的幂大小的数组中。
private static final int HASH_INCREMENT = 0x61c88647;
 
private static int nextHashCode() {
   return nextHashCode.getAndAdd(HASH_INCREMENT);
}
复制代码
可以看出,它是在上一个被构造出的ThreadLocal的ID/threadLocalHashCode的基础上加上一个魔数0x61c88647的。
这个魔数的选取与斐波那契散列有关,0x61c88647对应的十进制为1640531527。
斐波那契散列的乘数可以用(long) ((1L << 31) * (Math.sqrt(5) - 1))可以得到2654435769,如果把这个值给转为带符号的int,则会得到-1640531527。换句话说 (1L << 32) - (long) ((1L << 31) * (Math.sqrt(5) - 1))得到的结果就是1640531527也就是0x61c88647 。
通过理论与实践,当我们用0x61c88647作为魔数累加为每个ThreadLocal分配各自的ID也就是threadLocalHashCode再与2的幂取模,得到的结果分布很均匀。
ThreadLocalMap使用的是线性探测法,均匀分布的好处在于很快就能探测到下一个临近的可用slot,从而保证效率。。为了优化效率。
View Code

 

4. 内存泄漏分析

首先说明一点,ThreadLocal 通常作为成员变量或静态变量来使用(也就是共享的),比如前面应用场景中的例子。因为局部变量已经在同一条线程内部了,没必要使用 ThreadLocal。

为便于理解,这里先给出了 Thread、ThreadLocal、ThreadLocalMap、Entry 这几个类在 JVM 的内存示意图:

threadLocal源码解析

简单说明:

  • 当一个线程运行时,栈中存在当前 Thread 的栈帧,它持有 ThreadLocalMap 的强引用。

  • ThreadLocal 所在的类持有一个 ThreadLocal 的强引用;同时,ThreadLocalMap 中的 Entry 持有一个 ThreadLocal 的弱引用。

4.1 场景一

若方法执行完毕、线程正常消亡,则 Thread 的 ThreadLocalMap 引用将断开,如图:

threadLocal源码解析

以后 GC 发生时,弱引用也会断开,整个 ThreadLocalMap 都会被回收掉,不存在内存泄漏。

4.2 场景二

如果是线程池中的线程呢?也就是线程一直存活。经过 GC 后 Entry 持有的 ThreadLocal 引用断开,Entry 的 key 为空,value 不为空,如图所示:

threadLocal源码解析

此时,如果没有任何 remove 或者 get 等清理 Entry 数组的动作,那么该 Entry 的 value 持有的 Object 就不会被回收掉。这样就产生了内存泄漏。

这种情况其实也很容易避免,使用完执行 remove 方法就行了。

 

下面分析 ThreadLocal 的主要方法实现。

 

threadLocal源码解析

上一篇:npm ERR! code EEXIST的问题


下一篇:第四章 部署K8s前准备工作