ThreadLocal
ThreadLocal
解决线程安全问题的另一种思路,之前是共享资源加锁或 CAS 重试,现在是线程隔离各用各的。
作用:
- 实现资源对象的线程隔离,让每个线程各用各的资源对象,避免争用引发的线程安全问题
- 实现了线程内的资源共享
局部变量:可以线程隔离,但是不能跨方法。Thread Local 主要解决的就是这个跨方法的问题
线程关联的原理
ThreadLocal 并不是一个独立的存在, 它与 Thread 类是存在耦合的, java.lang.Thread 类针对 ThreadLocal 提供了如下支持:
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class.
*/
ThreadLocal.ThreadLocalMap threadLocals = null;
Thread
类有一个类型为ThreadLocal.ThreadLocalMap
的实例变量threadLocals
,也就是说每个线程有一个自己的ThreadLocalMap
。
ThreadLocalMap
有自己的独立实现,可以简单地将它的key
视作ThreadLocal
,value
为代码中放入的值(实际上key
并不是ThreadLocal
本身,而是它的一个弱引用)。
每个线程在往ThreadLocal
里放值的时候,都会往自己的ThreadLocalMap
里存,读也是以ThreadLocal
作为引用,在自己的map
里找对应的key
,从而实现了线程隔离。
ThreadLocalMap
有点类似HashMap
的结构,只是HashMap
是由数组+链表实现的,而ThreadLocalMap
中并没有链表结构。
我们还要注意Entry
, 它的key
是ThreadLocal<?> k
,继承自WeakReference
, 也就是我们常说的弱引用类型。
为什么 Map 的 key 要设置成弱引用呢?
因为如果我们 ThreadLocalMap 中的 ThreadLocal 不设置成弱引用,设置成强引用的话,如果外界已经将所有引用 ThreadLocal 的地方设置为了 null(也就是不再使用了),但是我们的 Map 里的 key 还指向堆内存里的 ThreadLocal 呢,而我们又不能直接操控 Map。
并且这个线程始终在运行(比如说线程池复用连接),那么久而久之,堆内存里的 ThreadLocal 就无法被回收,造成内存泄露
。
而设计成弱引用的话,在每次 GC 时,发现没有其他强引用指向 ThreadLocal 了,便会将其回收。
概括说就是:在方法中新建一个线程本地对象,就有一个强引用指向它,在调用 set()后,线程的 ThreadLocaMap 对象里的 Entry 对象又有一个引用 k 指向它。如果后面这个引用 k 是强引用就会使方法执行完,栈帧中的强引用销毁了,对象还不能回收,造成严重的内存泄露。
那为什么不设置成软引用呢?
让没有引用的尽快被回收,而不用等到内存不够再回收
内存泄漏
ThreadLocal 在 ThreadLocalMap 中是以一个弱引用身份被 Entry 中的 Key 引用的,因此如果 ThreadLocal 没有外部强引用来引用它,那么 ThreadLocal 会在下次 JVM 垃圾收集时被回收。这个时候就会出现 Entry 中 Key 已经被回收,出现一个 null Key 的情况,外部读取 ThreadLocalMap 中的元素是无法通过 null Key 来找到 Value 的。因此如果当前线程的生命周期很长,一直存在,那么其内部的 ThreadLocalMap 对象也一直生存下来,这些 null key 就存在一条强引用链的关系一直存在:Thread --> ThreadLocalMap-->Entry-->Value,这条强引用链会导致 Entry 不会回收,Value 也不会回收,但 Entry 中的 Key 却已经被回收的情况,造成内存泄漏。
但是 JVM 团队已经考虑到这样的情况,并做了一些措施来保证 ThreadLocal 尽量不会内存泄漏:在 ThreadLocal 的 get()、set()、remove()方法调用的时候会清除掉线程 ThreadLocalMap 中所有 Entry 中 Key 为 null 的 Value,并将整个 Entry 设置为 null,利于下次内存
由于ThreadLocalMap的 key 是弱引用,而 Value 是强引用。这就导致了一个问题,ThreadLocal 在没有外部对象强引用时,发生 GC 时弱引用 Key 会被回收,而 Value 不会回收,如果创建 ThreadLocal 的线程一直持续运行,那么这个 Entry 对象中的 value 就有可能一直得不到回收,发生内存泄露。
线程池脏读问题
ThreadLocal 是利用独占资源的方式,来解决线程安全问题,那如果我们确实需要有资源在线程之间共享,应该怎么办呢?这时,我们可能就需要用到线程安全的容器了。
上个例子说明,ThreadLocal 用不好也会产生副作用,线程复用会产生脏数据。由于线程 池会重用 Thread 对象,那么与 Thread 绑定的类的静态属性 ThreadLocal 变量也会被重用。如果在实现的线程 run()方法体中不显式地调用于线程相关的 ThreadLocal 信息,那么倘若下一个线程不调用 set()设置初始值,就可能 get 到重用的线程信息,包括 ThreadLocal 所关联的线程对象的 value 值。
解决方案也很简单,在每个线程执行中,往 ThreadLocal 对象设置值后,执行完核心逻辑代码,最后对 ThreadLocal 对象进行清理。优化后的代码如下:
父子线程间共享
我们使用ThreadLocal
的时候,在异步场景下是无法给子线程共享父线程中创建的线程副本数据的。
为了解决这个问题,JDK 中还有一个InheritableThreadLocal
类,我们来看一个例子:
public class InheritableThreadLocalDemo {
public static void main(String[] args) {
ThreadLocal<String> threadLocal = new ThreadLocal<>();
ThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();
ThreadLocal.set("父类数据:threadLocal");
inheritableThreadLocal.set("父类数据:inheritableThreadLocal");
new Thread(() -> {
System.out.println("子线程获取父类ThreadLocal数据:" + threadLocal.get());
System.out.println("子线程获取父类inheritableThreadLocal数据:" + inheritableThreadLocal.get());
}).start();
}
}
打印结果:
- 子线程获取父类 ThreadLocal 数据:null
- 子线程获取父类 inheritableThreadLocal 数据:父类数据:inheritableThreadLocal
实现原理是子线程是通过在父线程中通过调用new Thread()
方法来创建子线程,Thread#init
方法在Thread
的构造方法中被调用。在init
方法中拷贝父线程数据到子线程中:
但InheritableThreadLocal
仍然有缺陷,一般我们做异步化处理都是使用的线程池,而InheritableThreadLocal
是在new Thread
中的init()
方法给赋值的,而线程池是线程复用的逻辑,所以这里会存在问题。
解决问题方案:阿里巴巴的TransmittableThreadLocal
组件就可以解决这个问题。
MDC
MDC(Mapped Diagnostic Context,映射调试上下文)是 Slf4J, log4j 和 logback 提供的一种方便在多线程条件下记录日志的功能,也可以说是一种轻量级的日志跟踪工具。
canal