ThreadLocal 结构内部
从上面的结构图, 我们已经窥见 ThreadLocal 的核心机制:
每个 Thread 线程内部都有一个 Map.
Map 里面存储线程本地对象 (key) 和线程的变量副本(value)
但是, Thread 内部的 Map 是由 ThreadLocal 维护的, 由 ThreadLocal 负责向 map 获取和设置线程的变量值.
所以对于不同的线程, 每次获取副本值时, 别的线程并不能获取到当前线程的副本值, 形成了副本的隔离, 互不干扰.
Thread 线程内部的 Map 在类中描述如下:
- public class Thread implements Runnable {
- /* ThreadLocal values pertaining to this thread. This map is maintained
- * by the ThreadLocal class. */
- ThreadLocal.ThreadLocalMap threadLocals = null;
- }
深入解析 ThreadLocal
ThreadLocal 类提供如下几个核心方法:
- public T get()
- public void set(T value)
- public void remove()
get()方法用于获取当前线程的副本变量值.
set()方法用于保存当前线程的副本变量值.
initialValue()为当前线程初始副本变量值.
remove()方法移除当前前程的副本变量值.
get()方法
- /**
- * Returns the value in the current thread's copy of this
- * thread-local variable. If the variable has no value for the
- * current thread, it is first initialized to the value returned
- * by an invocation of the {@link #initialValue} method.
- *
- * @return the current thread's value of this thread-local
- */
- public T get() {
- Thread t = Thread.currentThread();
- ThreadLocalMap map = getMap(t);
- if (map != null) {
- ThreadLocalMap.Entry e = map.getEntry(this);
- if (e != null)
- return (T)e.value;
- }
- return setInitialValue();
- }
- ThreadLocalMap getMap(Thread t) {
- return t.threadLocals;
- }
- 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;
- }
- protected T initialValue() {
- return null;
- }
步骤:
1. 获取当前线程的 ThreadLocalMap 对象 threadLocals
2. 从 map 中获取线程存储的 K-V Entry 节点.
3. 从 Entry 节点获取存储的 Value 副本值返回.
4.map 为空的话返回初始值 null, 即线程变量副本为 null, 在使用时需要注意判断 NullPointerException.
set()方法
- /**
- * Sets the current thread's copy of this thread-local variable
- * to the specified value. Most subclasses will have no need to
- * override this method, relying solely on the {@link #initialValue}
- * method to set the values of thread-locals.
- *
- * @param value the value to be stored in the current thread's copy of
- * this thread-local.
- */
- public void set(T value) {
- Thread t = Thread.currentThread();
- ThreadLocalMap map = getMap(t);
- if (map != null)
- map.set(this, value);
- else
- createMap(t, value);
- }
- ThreadLocalMap getMap(Thread t) {
- return t.threadLocals;
- }
- void createMap(Thread t, T firstValue) {
- t.threadLocals = new ThreadLocalMap(this, firstValue);
- }
步骤:
1. 获取当前线程的成员变量 map
2.map 非空, 则重新将 ThreadLocal 和新的 value 副本放入到 map 中.
3.map 空, 则对线程的成员变量 ThreadLocalMap 进行初始化创建, 并将 ThreadLocal 和 value 副本放入 map 中.
remove()方法
- /**
- * Removes the current thread's value for this thread-local
- * variable. If this thread-local variable is subsequently
- * {@linkplain #get read} by the current thread, its value will be
- * reinitialized by invoking its {@link #initialValue} method,
- * unless its value is {@linkplain #set set} by the current thread
- * in the interim. This may result in multiple invocations of the
- * <tt>initialValue</tt> method in the current thread.
- *
- * @since 1.5
- */
- public void remove() {
- ThreadLocalMap m = getMap(Thread.currentThread());
- if (m != null)
- m.remove(this);
- }
- ThreadLocalMap getMap(Thread t) {
- return t.threadLocals;
- }
remove 方法比较简单, 不做赘述.
ThreadLocalMap
ThreadLocalMap 是 ThreadLocal 的内部类, 没有实现 Map 接口, 用独立的方式实现了 Map 的功能, 其内部的 Entry 也独立实现.
ThreadLocalMap 类图
在 ThreadLocalMap 中, 也是用 Entry 来保存 K-V 结构数据的. 但是 Entry 中 key 只能是 ThreadLocal 对象, 这点被 Entry 的构造方法已经限定死了.
- static class Entry extends WeakReference<ThreadLocal> {
- /** The value associated with this ThreadLocal. */
- Object value;
- Entry(ThreadLocal k, Object v) {
- super(k);
- value = v;
- }
- }
Entry 继承自 WeakReference(弱引用, 生命周期只能存活到下次 GC 前), 但只有 Key 是弱引用类型的, Value 并非弱引用.
ThreadLocalMap 的成员变量:
- static class ThreadLocalMap {
- /**
- * The initial capacity -- MUST be a power of two.
- */
- private static final int INITIAL_CAPACITY = 16;
- /**
- * The table, resized as necessary.
- * table.length MUST always be a power of two.
- */
- private Entry[] table;
- /**
- * The number of entries in the table.
- */
- private int size = 0;
- /**
- * The next size value at which to resize.
- */
- private int threshold; // Default to 0
- }
Hash 冲突怎么解决
和 HashMap 的最大的不同在于, ThreadLocalMap 结构非常简单, 没有 next 引用, 也就是说 ThreadLocalMap 中解决 Hash 冲突的方式并非链表的方式, 而是采用线性探测的方式, 所谓线性探测, 就是根据初始 key 的 hashcode 值确定元素在 table 数组中的位置, 如果发现这个位置上已经有其他 key 值的元素被占用, 则利用固定的算法寻找一定步长的下个位置, 依次判断, 直至找到能够存放的位置.
ThreadLocalMap 解决 Hash 冲突的方式就是简单的步长加 1 或减 1, 寻找下一个相邻的位置.
- /**
- * Increment i modulo len.
- */
- private static int nextIndex(int i, int len) {
- return ((i + 1 <len) ? i + 1 : 0);
- }
- /**
- * Decrement i modulo len.
- */
- private static int prevIndex(int i, int len) {
- return ((i - 1>= 0) ? i - 1 : len - 1);
- }
显然 ThreadLocalMap 采用线性探测的方式解决 Hash 冲突的效率很低, 如果有大量不同的 ThreadLocal 对象放入 map 中时发送冲突, 或者发生二次冲突, 则效率很低.
所以这里引出的良好建议是: 每个线程只存一个变量, 这样的话所有的线程存放到 map 中的 Key 都是相同的 ThreadLocal, 如果一个线程要保存多个变量, 就需要创建多个 ThreadLocal, 多个 ThreadLocal 放入 Map 中时会极大的增加 Hash 冲突的可能.
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 方法.
- ThreadLocal<Session> threadLocal = new ThreadLocal<Session>();
- try {
- threadLocal.set(new Session(1, "Misout 的博客"));
- // 其它业务逻辑
- } finally {
- threadLocal.remove();
- }
应用场景
还记得 Hibernate 的 session 获取场景吗?
- private static final ThreadLocal<Session> threadLocal = new ThreadLocal<Session>();
- // 获取 Session
- public static Session getCurrentSession(){
- Session session = threadLocal.get();
- // 判断 Session 是否为空, 如果为空, 将创建一个 session, 并设置到本地线程变量中
- try {
- if(session ==null&&!session.isOpen()){
- if(sessionFactory==null){
- rbuildSessionFactory();// 创建 Hibernate 的 SessionFactory
- }else{
- session = sessionFactory.openSession();
- }
- }
- threadLocal.set(session);
- } catch (Exception e) {
- // TODO: handle exception
- }
- return session;
- }
为什么? 每个线程访问数据库都应当是一个独立的 Session 会话, 如果多个线程共享同一个 Session 会话, 有可能其他线程关闭连接了, 当前线程再执行提交时就会出现会话已关闭的异常, 导致系统异常. 此方式能避免线程争抢 Session, 提高并发下的安全性.
使用 ThreadLocal 的典型场景正如上面的数据库连接管理, 线程会话管理等场景, 只适用于独立变量副本的情况, 如果变量为全局共享的, 则不适用在高并发下使用.
总结
每个 ThreadLocal 只能保存一个变量副本, 如果想要上线一个线程能够保存多个副本以上, 就需要创建多个 ThreadLocal.
ThreadLocal 内部的 ThreadLocalMap 键为弱引用, 会有内存泄漏的风险.
适用于无状态, 副本变量独立后不影响业务逻辑的高并发场景. 如果如果业务逻辑强依赖于副本变量, 则不适合用 ThreadLocal 解决, 需要另寻解决方案.
来源: https://www.cnblogs.com/jay-wu/p/11313501.html