ThreadLocal

ThreadLocal 实例在类中通常是 private static 字段,这些类希望将状态与 Thread 相关联。

使用位置:

  • 多线程情况下,一般在线程的 run() 方法中使用

使用位置决定了,在设计的时候,要通过传递 ThreadLocal 实例自身作为 key,从而获取当前线程存储的 value,同时存储<key,value> 的 map 需要在 Thread 中。

创建 ThreadLocal实例的方法

方法一:使用 ThreadLocal.withInitial(Supplier<? extends S> supplier) 方法

1
2
3
例如:
ThreadLocal<String> threadLocalName = ThreadLocal
.withInitial(() -> Thread.currentThread().getName());

方法二:创建新的 ThreadLocal 或其子类,并重写 initialValue() 方法

1
2
3
4
5
6
7
8
9
10
11
例如:
ThreadLocal<String> threadLocalName = new ThreadLocal<String>() {
@Override
protected String initialValue() {
return Thread.currentThread().getName();
}
};

注:
其实 ThreadLocal.withINitial() 方法内部是通过创建 ThreadLocal 子类并重写initialValue() 方法来实现的,看源码部分:
static final class SuppliedThreadLocal<T> extends ThreadLocal<T>

为什么要重写 initialValue()方法?

具体分析:

因为创建了一个 ThreadLocal实例后,在线程内调用ThreadLocal#get() 方法的时候,首次调用时是需要创建 ThreadLocalMap 的,根据源码 get() -> setInitialValue() -> T value = initialValue(); 我们知道 value 的内容取决于 initialValue() 方法,而默认该方法返回值为 null,所以需要重写该方法。

问题:ThreadLocal#get() 是如何找到当前 Thread 中存储的Map的?

答:通过 Thread t = Thread.currentThread();方法获取对应 Thread,然后 Thread 中有 Map 的变量ThreadLocal.ThreadLocalMap threadLocals = null;,这样将 ThreadLocal本身作为key,传入Thread 内部独立的 Map,对于每个 Thread 来说,就可以实现相同的 key,获取 Thread 自身独自的 value 的功能。

如何设计 ThreadLocal?

在线程对象内部搞个 map,把 ThreadLocal对象自身作为 key,把它的值作为 map 的值。

ThreadLocal 作为一个容器来使用(对于线程来说是,资源本地化容器),重要的实现就是 get()set()方法和其内部的 ThreadLocalMap

ThreadLocal如何设计ThreadLocalMap中的Entry的?

1
2
3
4
5
6
7
8
9
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;

Entry(ThreadLocal<?> k, Object v) {
super(k); // k 被 WeakReference 引用,所以 k 是弱引用
value = v; // v 还是强引用
}
}

从上面代码可知,Entry 继承了 WeakReference类,在它的构造函数中 k 被 WeakReference 所引用即super(k),所以这个 key 才是弱引用,Entry 自己并不是弱引用。

ThreadLocal 为什么要用弱引用?

  1. Entry 对 key 是弱引用,那么为什么要使用弱引用呢?

    • 如果一个对象没有强引用,只有弱引用的情况下, 这个对象是活不过一次 GC 的(遇到GC就会被清除),所以这样的设计就是为了让当外部对 ThreadLocal对象没有强引用的时候,可以将 ThreadLocal对象 给清理掉。
  2. 那么为什么 Entry 中的 value 不弱引用?

    • Entry 中的 value 如果弱引用,那么只要一次 GC,value 就会被清理掉,这时如果通过 ThreadLocal 对象 查对应的值,就是 null 了。
      • (value 只有与 Entry 这一条引用链,而 key(ThreadLocal) 除了与 Entry 这条引用链,栈上还有 ThreadLocal 引用指向堆中的 ThreadLocal 对象,这个引用是强引用,只要这条强引用存在,那说明此时的 ThreadLocal 是有用的。)
  3. 那么为什么不能是 ThreadLocalMapEntry 是弱引用?

    • 原因同上,Entry 也只有ThreadLocalMap 这一条引用链,如果弱引用,那么 Entry 一次 GC 后就会被清理掉,无法正常使用了,所以只能强引用。

综上如果想及时清除无用的 ThreadLocal 对象,通过弱化引用的形式,只能操作 Entry 和 key 之间的引用,所以它们之间用弱引用来实现。

下图是 ThreadLocal 在堆栈用的完整引用链:

threadlocal_堆栈引用链

从上图可知,当随着方法的执行完毕,相应的栈帧也出栈了,此时 threadlocal引用threadlocal对象 这条强引用链也就没了,如果没有别的栈有对 threadlocal对象的引用,那么说明 threadlocal对象无法再被访问到(定义成静态变量的另说)。

那么此时 ThreadLocal 只存在于 Entry 之间的弱引用,那此时发生 GC,它就可以被清除了,因为它无法被外部使用了,就等于没用了,应该被处理来省空间。

因为平日线程的使用方式,基本都是线程池,所以线程的生命周期会很长,可能从你部署上线后一直存在,而 ThreadLocal对象的生命周期可能没有这么长。所以为了能让已经没用的 ThreadLocal对象得以回收,Entry 和 key 要设计成弱引用,不然 Entry 和 key 是强引用的话, ThreadLocal 对象会一直在内存中存在。

为什么这样的设计会有内存泄漏?

什么是内存泄漏

指:程序中已经无用的内存无法被释放,造成系统内存的浪费。

当 Entry 中的 key 即 ThreadLocal 对象被回收之后,会发生 Entry 中 key 为 null 的情况,其实这个 Entry 就已经没用了,但是又无法被回收,因为有 Thread -> ThreadLocalMap -> Entry 这条强引用在,这样没用的内存无法被回收,就是内存泄漏

ThreadLocal 是如何设计处理内存泄漏的?

设计者在多个地方都做了清理无用 Entry ,即 key 已经被回收的 Entry 的操作。

代码中表现为 expungeStaleEntry() 方法。

get() , set()rehash() 方法中都 expungeStaleEntry() 调用。

ThreadLocal 最佳实践

等着无用 Entry 被动回收不是最好方法,如果不调用 set 或 get方法,或者调用 get 都直接命中,或者不发生扩种,那无用的 Entry 岂不是一直存在了吗?expungeStaleEntry()只能防止一部分的内存泄漏。

所以最佳实践是用完之后,调用一下 remove()方法,手动把 Entry 清理掉,这样就不会发生内存泄漏了!

1
2
3
4
5
6
7
8
void foo() {
threadlocal.set(xxx);
try {
// do something
} finally {
threadlocal.remove();
}
}

即不需要的时候,显示的 remove 掉。

当然,如果不是线程池使用方法的话,其实不用关心内存泄漏,反正线程执行完就都回收了,但一般我们都是只用线程池的,比如你用了 tomcat,其实请求的执行用的就是 tomcat 的线程池,这就是隐式使用的线程池。

扩展知识:Java 的引用

扩展知识:Java 的引用

  • 强引用

    • 正常的引用,只要有强引用在,JVM 即使发生 OOM 也不会回收被引用的对象的。
  • 软引用

    • 与弱引用及其相似,除了回收时机不同(存活时间比弱引用可能长一些);

    • 只要内存足够,在 GC 时就不会清除软可达对象,这些对象就会一直保存在内存中。回收时机由 JVM 根据算法来决定。

    • 在触发OOM之前,垃圾收集器一定会清理掉所有所的软可达对象;

    • 适合用来做一些小的 cache,然后让 JVM 去决定什么时候把对象从缓存中清除;

      但是对于严重依赖缓存来提升性能的应用而言,软引用做缓存并不合适,此时应该使用更全面的缓存框架或者应用来处理。

    • 疑惑点:关于何时被回收?是不是可以理解为,在young GC 时并不会清除软引用,只有在 Full GC 时才会清除?只要堆内有足够内存,即使发生 GC ,也不会清除掉 软引用 ?毕竟 GC 是分带收集的?

  • 弱引用(WeakReference)

    • 一个WeakReference引用的对象,它被称为 weakly reachable object(弱可达性对象)。而这样的对象不能阻止垃圾收集器对它的回收

    • 注意:如果一个对象被用WeakReference引用,但是在其他方法中作为方法参数传入时(例如 WeakHashMap#get 方法),那么在该方法(get方法)中,key 并不会被垃圾回收。因为给定的key已经被方法参数所引用。当该方法执行完出栈后,该引用就会消失后才可被垃圾回收。

      1
      2
      WeakReference weakWidget = new WeakReference(widget);
      // 当有强引用指向 Widget 对象时,调用 weakWidget.get() 方法时,会获得真正的 Widget 对象;否则得到的就是 null
  • 虚引用

    • get() 方法返回 null,即,无法获取引用对象引用的真正的对象;

    • 唯一作用就是它可以监测到对象的死亡。即,当你的对象真正从内存中移除时,指向这个对象的 PhantomReference对象就会被加入到ReferenceQueue队列中。

      因为 PhantomReference类的构造器必须制定一个 ReferenceQueue 对象。

  • ReferenceQueue 的作用

    • 在构造 Soft Reference ,Weak Reference, Phantom Reference 时,如果关联一个 ReferenceQueue对象,那么当一个 Reference引用的对象被清除的时候,该Reference对象会被放入给定的队列当中。可以通过从该队列中获取Reference对象,然后做相关的工作。
    • WeakHashMap 中清除 key 为null 的Entry时使用ReferenceQueue,具体参考 WeakHashMap.expungeStaleEntries()方法

一个对象可以被不同引用类型引用,例如,ThreadLocal 对象可以同时被外部变量强引用,和被ThreadLocalMap 的Entry 作为 key 弱引用。

关于 finalize()方法的作用?

答:最佳实践就是不要使用 finalize()方法,因为它不能保证运行。JVM完全可以决定何时运行垃圾收集器以及收集什么,即使对象符合垃圾收集的条件。

参考:

深入理解 java 中的 Soft references & Weak references & Phantom reference

软引用与弱引用的不同?

软引用与弱引用唯一的不同就是,垃圾回收器(garbage collector)会通过算法决定是否回收软可达对象(softly reachable object),但是会直接回收弱可达对象(weakly reachable object)。

类有三种变量:

  • 在类中的成员变量 - fields(字段)
  • 在方法或者代码块中的变量 - local variables(本地变量)
  • 在方法定义中的变量 - parameters (参数)

首先要明白各种引用在遇到 GC 之后,引用与被引用对象之间的变化。

  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2022-2023 ligongzhao
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信