乐趣区

ThreadLocal详解

想要获取更多文章可以访问我的博客 – 代码无止境。

什么是 ThreadLocal

ThreadLocal 在《Java 核心技术 卷一》中被称作 线程局部变量(PS:关注公众号 itweknow,回复“Java 核心技术”获取该书),我们可以利用 ThreadLocal 创建只能由同一线程读和写的变量。因此就算两个线程正在执行同一段代码,并且这段代码具有对 ThreadLocal 变量的引用,这两个线程也无法看到彼此的 ThreadLocal 变量。

简单使用

1. 创建 ThreadLocal,只需要 new 一个 ThreadLocal 对象即可。

private ThreadLocal<String> myThreadLocal = new ThreadLocal<String>();

2. 设置值

myThreadLocal.set("I'm a threadLocal");

3. 获取值

myThreadLocal.get();

4. 清除,有些情况下我们在使用完线程局部变量后,需要即时清理,否则会导致程序运行错误。

myThreadLocal.remove();

假如我们现在要利用 AOP 打印方法的耗时,这个时候我们需要在 @Before 方法中记录方法开始执行的时间,然后在 @AfterReturning 方法中打印出来耗时时间。我们写在切面里的方法可能慧在多个线程中同时执行,所以此时我们需要 ThreadLocal 来记录开始执行的时间。

1. 我们需要在切面类中定义一个 ThreadLocal。

private ThreadLocal<Long> threadLocal = new ThreadLocal();

2. 在 @Before 方法中记录开始时间。

long startTime = System.currentTimeMillis();
threadLocal.set(startTime);

3. 在 @AfterReturning 方法中取出开始时间,并计算耗时。

long startTime = threadLocal.get();
long spendTime = System.currentTimeMillis() - startTime;
threadLocal.remove();
System.out.println("方法执行时间:" + spendTime + "ms");

这里只是借这个场景和大家一起熟悉一下 ThreadLocal 的用法,整个打印方法耗时的实现你可以在 Github 上找到,如果你想了解 AOP 可以参考这篇文章《使用 Spring Boot AOP 实现 Web 日志处理和分布式锁》。

原理解析

其实 ThreadLocal 是个数据结构,下面我们就一起通过源码来剖析一下 ThreadLocal 的运行原理。

public T get() {Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {@SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();}

public void set(T value) {Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

上面是 ThreadLocal 的 get()set()方法的源码,可以看到 ThreadLocal 是将值存放在 ThreadLocalMap 中。其实在每个线程中都维护着一个 threadLocals 变量(ThreadLocalMap 类型),当使用 set() 方法的时候实际上是将值存在当前线程的 threadLocals 中的,调用 get() 方法也是从当前线程中取值的,这样就做到了线程间的隔离。
看到这里想必你也奇怪,在设置值和取值的时候都没有任何与 key 有关的东西,那么当一个线程有多个 ThreadLocal 的时候是如何做到一一对应的呢?那我们就一起来看下这个 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
}

由上面可见在 ThreadLocalMap 中维护着 tablesize 以及 threshold 三个属性。table是一个 Entry 数组主要用来保存具体的数据,sizetable 的大小,而 threshold 这表示当 table 中元素数量超过该值时,table就会扩容。了解了 ThreadLocalMap 的结构之后,我们就来看下其 set 方法吧。

private void set(ThreadLocal<?> key, Object value) {Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);

    for (Entry e = tab[i];
        e != null;
        e = tab[i = nextIndex(i, len)]) {ThreadLocal<?> k = e.get();

        if (k == key) {
            e.value = value;
            return;
        }

        if (k == null) {replaceStaleEntry(key, value, i);
            return;
        }
    }

    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();}

通过上面的代码分析得出,整个的设值过程如下:

  1. 通过 ThreadLocal 的 threadLocalHashCode 值定位到 table 中的位置 i。
  2. 如果 table 中 i 这个位置是空的,那么就新创建一个 Entry 对象放置在 i 这个位置。
  3. 如果 table 中 i 这个位置不为空,则取出来 i 这个位置的 key。
  4. 如果这个 key 刚好就是当前 ThreadLocal 对象,则直接修改该位置上 Entry 对象的 value。
  5. 如果这个 key 不是当前 TreadLocal 对象,则寻找下一个位置的 Entry 对象,然后重复上述步骤进行判断。

对于 get 方法也是同样的原理从 ThreadLocalMap 中获取值。那么 ThreadLocal 是如何生成 threadLocalHashCode 值的呢?

public class ThreadLocal<T> {private final int threadLocalHashCode = nextHashCode();
    private static final int HASH_INCREMENT = 0x61c88647;
    private static int nextHashCode() {return nextHashCode.getAndAdd(HASH_INCREMENT);
    }
}

可见我们在初始化一个 ThreadLocal 对象的时候都为其会生成一个 threadLocalHashCode 值,每初始化一个 ThreadLocal 该值就增加 0x61c88647。这样就可以做到每个 ThreadLocal 在 ThreadLocalMap 中找到一个存储值的位置了。

结束语

在文章的最后分享一次之前遇到的一个与 ThreadLocal 有关的坑,有一次在写分页的时候使用了 PageHeler 插件,引包的时候错误地引用了 MybatisPlus 下的 PagerHelper,而 MybatisPlus 下的 PageHelper 在 ThreadLocal 中存储了 SQL 分页信息在使用之后没有移除,所以执行了分页的 SQL 之后在当前线程中执行的 SQL 都会出现问题。所以大家在使用 ThreadLocal 的过程中千万要注意在适当的时候需要清除。本文主要介绍了 Java 中的线程局部变量 ThreadLocal 的使用,并且和大家一起稍微了解了一下源码。希望对大家能够有所帮助。

PS: 学习不止,码不停蹄!如果您喜欢我的文章,就关注我吧!

退出移动版