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 的分析。
二者都是「键-值对」构成的数组,对哈希冲突的处理方式不同,导致了它们在结构上产生了一些区别:
- HashMap 处理哈希冲突使用的「链表法」。也就是当产生冲突时拉出一个链表,而且 JDK 1.8 进一步引入了红黑树进行优化。
- 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 在某些情况可能产生的「内存泄漏」就跟这个「弱引用」有关,后面再展开分析。
- 寻址
Entry 的 key 是 ThreadLocal 类型的,它是如何在数组中散列的呢?
ThreadLocal 有个 threadLocalHashCode 变量,每次创建 ThreadLocal 对象时,这个变量都会增加一个固定的值 HASH_INCREMENT,即 0x61c88647,这个数字似乎跟黄金分割、斐波那契数有关,但这不是重点,有兴趣的朋友可以去深入研究下,这里我们知道它的目的就行了。与 HashMap 的 hash 算法的目的近似,就是为了散列的更均匀。
魔数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,从而保证效率。。为了优化效率。
4. 内存泄漏分析
首先说明一点,ThreadLocal 通常作为成员变量或静态变量来使用(也就是共享的),比如前面应用场景中的例子。因为局部变量已经在同一条线程内部了,没必要使用 ThreadLocal。
为便于理解,这里先给出了 Thread、ThreadLocal、ThreadLocalMap、Entry 这几个类在 JVM 的内存示意图:
简单说明:
-
当一个线程运行时,栈中存在当前 Thread 的栈帧,它持有 ThreadLocalMap 的强引用。
-
ThreadLocal 所在的类持有一个 ThreadLocal 的强引用;同时,ThreadLocalMap 中的 Entry 持有一个 ThreadLocal 的弱引用。
4.1 场景一
若方法执行完毕、线程正常消亡,则 Thread 的 ThreadLocalMap 引用将断开,如图:
以后 GC 发生时,弱引用也会断开,整个 ThreadLocalMap 都会被回收掉,不存在内存泄漏。
4.2 场景二
如果是线程池中的线程呢?也就是线程一直存活。经过 GC 后 Entry 持有的 ThreadLocal 引用断开,Entry 的 key 为空,value 不为空,如图所示:
此时,如果没有任何 remove 或者 get 等清理 Entry 数组的动作,那么该 Entry 的 value 持有的 Object 就不会被回收掉。这样就产生了内存泄漏。
这种情况其实也很容易避免,使用完执行 remove 方法就行了。
下面分析 ThreadLocal 的主要方法实现。