细说ThreadLocal

206 阅读7分钟

上次在jvm垃圾回收算法中有提到,Java中有强、软、弱、虚四种引用方式,这四种引用方式会影响到一个对象被回收的时机,在弱引用的部分,留了一个疑问,就是弱引用在ThreadLocal中的使用,这次主要说一下这个ThreadLocal,到底是有什么申通

简介

This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable. ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID).

以上是oracle官方文档对于ThreadLocal类的说明,其中主要的介绍就是说,这个类提供了线程本地变量,每个线程都有自己独立的变量副本,互相隔离。简单点就是说,这个类,就是用来定义变量的,跟普通变量的区别就是说,这个类定义的变量,在线程之间,是隔离的互相不可见。并且使用ThreadLocal不会遇到线程安全问题

public class Main {
    static final ThreadLocal<Integer> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) {
        threadLocal.set(1);
        new Thread(() -> {
            threadLocal.set(2);
        }).start();

        new Thread(() -> {
            threadLocal.set(3);
        }).start();

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(threadLocal.get());
    }
}
1

可以看一下这个示例,主线程设置变量值为1,并且等待子线程处理结束,最终打印的结果还是1,也就是说,不论其他线程如何处理,都不可能改变当前线程ThreadLocal存的变量值

用途

知道了它是干什么的,现在说一下他能做什么,下边简单介绍几个

1. 生成线程唯一ID

这个是直接拿oracle官方文档的一个例子,就是可以使用ThreadLocal做一个工具类,在每个线程调用的时候,生成当前线程的唯一标识,并且在同一个线程里面,使用ThreadId.get(),就可以拿到相同的值

 import java.util.concurrent.atomic.AtomicInteger;

 public class ThreadId {
     // Atomic integer containing the next thread ID to be assigned
     private static final AtomicInteger nextId = new AtomicInteger(0);

     // Thread local variable containing each thread's ID
     private static final ThreadLocal<Integer> threadId =
         new ThreadLocal<Integer>() {
             @Override protected Integer initialValue() {
                 return nextId.getAndIncrement();
         }
     };

     // Returns the current thread's unique ID, assigning it if necessary
     public static int get() {
         return threadId.get();
     }
 }

2. 参数传递

在我们写代码的时候,在很多时候,都会遇到一个情况,就是说,可能业务比较复杂,我们会将一个业务拆分,并且可能各个业务之间会有调用,这会导致一个情况就是一些方法的调用栈会比较深,可能调用了n层,在这种情况下,如果我们想在当前线程传递一个参数,并且在每个调用栈中都想访问到,那么一般做法都是,去所有调用的方法栈上加这个方法的入参,操作相当复杂。我们可以增加一个工具类,或者全局ThreadLocal,直接在第一个方法的时候,进行set,后续只要是在使用的地方进行get就可以,同一个线程得到的变量一定是相同的

3. Spring中的@Transaction注解

我们知道,在使用spring的时候,事物操作只需要在调用入口添加@Transaction注解就可以实现,那这个事物最终在提交或者回滚的时候,那么多线程在同时操作,如何知道是针对哪个connection进行操作呢,也没有进行显示的进行传递,其实内部就是使用ThreadLocal进行线程隔离并且传递,有兴趣可以去看下源码,后续会专门写一篇Spring中@Transaction注解的实现

使用注意事项

使用ThreadLocal的时候,有个重要的操作就是,在使用完变量以后,一定要进行remove!!!否则可能会产生内存溢出的问题。下面会进行说明原因

内存溢出问题*

ThreadLocal中,其实有两处可能会导致内存溢出,一处作者使用弱引用内部解决,另外一处需要我们显示的用remove进行处理

弱引用是如何解决内存溢出的

想了解这个问题,就需要了解一下ThreadLocal的内部实现,下面拿出ThreadLocal中最重要的几个方法片段进行说明

  public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
 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);
        }
static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

下边用流程来说一下ThreadLocal使用过程,以及方法调用流程

  1. 当前线程调用set方法
  2. set方法中获取当前线程信息,并创建了一个ThreadLocalMap对象赋值给当前线程中的threadLocals变量(每个线程Thread类都有自己内部的一个ThreadLocalMap)
  3. ThreadLocalMap创建的时候,内部创建一个Entry对象接收K,V
  4. Entry对象创建的时候使用super方法将Key包装成弱引用(解决内存溢出的关键)
  5. 最终传进entry的key是ThreadLocal对象,value是set的值

所以最终的引用关系为:

  • 当前线程强引用ThreadLocal以及ThreadLocalMap

  • ThreadLocalMap强引用Entry

  • Entry强引用ThreadLocal以及要set的value

如果把第4步想想成强引用,结合我们之前说过的引用的概念,我们想一下后果,ThreadLocal这个对象同时被当前线程强引用了两次,一次是直接引用,一次是间接引用,当我们不用ThreadLocal,想让他被回收的时候,将ThreadLocal设置为null,这样减少了一次强引用,但是间接引用呢?没办法处理,如果当前线程一直不销毁,那么会一直存在一个不能被回收的ThreadLocal,那累计下来不就溢出了么。所以这里作者将间接引用改为弱引用,当强引用和弱引用同时存在的时候,对象不会被回收,但是当强引用不在了,弱引用引用的对象立马被回收,这样就可以把ThreadLocal回收了。但是这样依然有问题,这就是ThreadLocal遗留的另外一个问题

为什么还会有内存溢出的问题?

上边说到作者使用弱引用,就可以将ThreadLocal回收掉,解决了ThreadLocal的内存溢出,但是还存在另外一个一个问题,就是当ThreadLocal回收之后,Entry中的key就是null,value依然存在,Entry依然存在,ThreadLocalMap依然存在,那么问题和上边一样,如果这个线程一直存在,那么这个Entry中的value永远无法被回收。

如何处理

虽然在get,set方法的时候,如果判断key是null的话,都会去清理这个value,但是这些都是显式的调用,如果这个线程一直存在,并且一直不做这个get,set等操作,那么这个value是不是就会一直存在?所以这就回到了我们上边说到的注意事项,ThreadLocal给我们提供了一个显式调用的remove方法,专门去处理这个问题。去清理需要被回收的value

答疑

什么情况下线程会一直存在?

上边有提到很多次,线程如果一直存在,我们知道一个线程的生命周期一般来说,调用栈结束之后就会销毁,销毁之后,其实所有的内存都会被回收,那么什么情况下会一直存在呢?这个场景就比较多了,比如我们经常用到的线程池,一个线程是被重复使用的,还有就是我们平时单独开辟一个线程去跑批处理任务,或者一直阻塞等待处理一些任务等等。

为什么说ThreadLocal是线程安全的

上边看过“弱引用是如何解决内存溢出的”部分的话,就会有一个概念,就是说其实最终当前线程调用ThreadLocal的set方法的时候,其实是将ThreadLocal当K,Value当V,存到当前线程自己的一个Map中,操作的都是线程自己内部的东西,当然是安全的,不会遇到多个线程竞争的问题

总结

本节文章主要介绍了ThreadLocal的用法,用途以及注意事项和原因,了解这些内容,在做多线程操作的时候,可以让我们有更多的方案去选择。处理起来得心应手,不出纰漏。