本篇文章主要讲解 ThreadLocal 的用法和内部的数据结构及实现。有时候我们写代码的时候,不太注重类之间的职责划分,经常造出一些上帝类,也就是什么功能都往这个类里放。虽然能实现功能但是并不优雅且不好维护。这篇文章就介绍 ThreadLocal 中如何设计优雅的数据结构以及类之间的职责划分,至于怎么跟农夫山泉广告语扯上关系,相信你读完便有了答案,文末也有解释。
ThreadLocal 对象可以当做每个线程局部变量。也就是不同线程同时读写同一个 ThreadLocal 对象,其实操作的是 线程本地 的数据, 所以不存在线程安全问题。听起来跟我们常规的理解有点矛盾,下面就举个例子看看
package com.cnblogs.duma.thread; public class ThreadLocalTest { // 定义 ThreadLocal 对象 static ThreadLocal<String> var1 = new ThreadLocal<String>(); public static void main(String args[]) { new Thread1().start(); new Thread1().start(); } public static class Thread1 extends Thread { @Override public void run() { // 写同一个 ThreadLocal 对象 —— var1 var1.set(getName()); while (true) { System.out.println(getName() + " get var1: " + var1.get()); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } } } } }
控制台输出
Thread-0 get var1: Thread-0 Thread-1 get var1: Thread-1 Thread-1 get var1: Thread-1 Thread-0 get var1: Thread-0 Thread-1 get var1: Thread-1 Thread-0 get var1: Thread-0
本例中,我们定义了 ThreadLocal 对象 static ThreadLocal<String> var1 = new ThreadLocal<String>(); ,一般定义 ThreadLocal 对象都用 static 修饰,因为它的作用是提供读写线程本地属性的能力,与对象没关系,所以定义成 static 即可。从控制台输出可以看出,所有线程都读写 var1 变量, 但互不干扰。看到这里,你可能会猜想,是不是 ThreadLocal 的 set 方法将线程做 key(每个线程都是独一无二的),将我们要设置的值作为 value,存到一个 公共的 map 结构中,如果这样想只对了一半, 下面我会对照源码详细介绍。
看到这里你可以又有疑问,为什么不在线程内部定义个局部变量,不是也能实现 ThreadLocal 的功能。为了解答这个问题,我们来看看下面的例子
package com.cnblogs.duma.thread; public class ThreadLocalTest { // 定义 ThreadLocal 对象 static ThreadLocal<String> var1 = new ThreadLocal<String>(); public static void main(String args[]) { new Thread1().start(); new Thread1().start(); new Thread2().start(); } public static class Thread1 extends Thread { @Override public void run() { // 写同一个 ThreadLocal 对象 —— var1 var1.set(getName()); while (true) { System.out.println(getName() + " get var1: " + var1.get()); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } } } } public static class Thread2 extends Thread { @Override public void run() { // 写同一个 ThreadLocal 对象 —— var1 var1.set(getName()); while (true) { System.out.println(getName() + " get var1: " + var1.get()); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } } } } }
控制台输出
Thread-0 get var1: Thread-0 Thread-2 get var1: Thread-2 Thread-1 get var1: Thread-1 Thread-0 get var1: Thread-0 Thread-2 get var1: Thread-2 Thread-1 get var1: Thread-1
这个例子是在第一个基础上又定义了一个线程类 —— Thread2,它同样可以操作 var1 并且也是操作本地的变量,与 Thread1 线程互不干扰。所以,我们就知道 ThreadLocal 与局部变量的区别了, 它可以为所有线程提供本地数据的读写能力。
既然用法了解了,那我们就深入到 ThreadLocal 类的内部一探究竟,看看它的实现跟我们刚才的猜测有什么区别。在 ThreadLocal 中最重要的两个方法是 set 和 get,下面我们分别看下这两个方法的源码
public void set(T value) { // 获得当前线程 Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); } // 获取当前线程的 threadLocals 属性 ThreadLocalMap getMap(Thread t) { return t.threadLocals; } void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }
在 set 方法中, 首先调用了 getMap 方法,getMap 方法返回的是当前线程的 threadLocals 属性,如果当前线程的 threadLocals 属性为空,需要初始化,即调用 createMap 方法,在该方法中,new 一个 ThreadLocalMap 对象,该类是 ThreadLocal 的内部类。因此,我们可以看到并不是有一个全局的 map 保存数据,而确实是每个线程本地的局部变量(threadLocals)。既然每个线程操作是属于自己的变量,那么把 ThreadLocalMap 对象放在线程本地自然就更合理。接下来看看 ThreadLocalMap 的构造方法
public class ThreadLocal<T> { private final int threadLocalHashCode = nextHashCode(); private static AtomicInteger nextHashCode = new AtomicInteger(); private static final int HASH_INCREMENT = 0x61c88647; private static int nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT); } static class ThreadLocalMap { private static final int INITIAL_CAPACITY = 16; private java.lang.ThreadLocal.ThreadLocalMap.Entry[] table; private int size = 0; static class Entry extends WeakReference<java.lang.ThreadLocal<?>> { Object value; Entry(java.lang.ThreadLocal<?> k, Object v) { super(k); value = v; } } ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) { // table 用来存储不同 ThreadLocal 对象对应的值 table = new Entry[INITIAL_CAPACITY]; // 每一个 ThreadLocal 对象都有一个 threadLocalHashCode 属性,即 hash 编码,与之对应 int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); // Entry 对象存储 ThreadLocal 对象以及与该 ThreadLocal 对象对应的值 table[i] = new Entry(firstKey, firstValue); size = 1; setThreshold(INITIAL_CAPACITY); } } }
该构造方法中会用 INITIAL_CAPACITY 属性初始化 table,该属性是个数组,且元素是 Entry 类型(ThreadLocalMap 的内部静态类)。这里不知道你是不是有疑问,我们调用 set 是只传一个值,为什么这里初始化一个数组存放数据。是因为当程序中不止一个 var1,还有 var2、var3 等多个 ThreadLocal 变量时,就需要一个数组(table)来存储不同 ThreadLocal 对象及其对应的值。
接下来分别说说 Entry 类和 ThreadLocal 类中的属性。Entry类包含两个属性,一个是 ThreadLocal 对象,另一个是需要 set 的值。ThreadLocal 中有一个属性——threadLocalHashCode,代表 ThreadLocal 对象的 hash 编码。由源码可知,threadLocalHashCode 属性是由 nextHashCode 生成,nextHashCode 是 AutomicInteger 类型,是线程安全的。
在 firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); 这段代码中,用 firstKey(即 ThreadLocal 对象)的 hash 编码与 (INITIAL_CAPACITY - 1) 做按位与操作,就能生产 [0,INITIAL_CAPACITY - 1] 区间的一个数字 i, 将生成的 Entry 对象存入 table[i] 中。小结一下这里的逻辑:每个 ThreadLocal 对应一个 hash 编码,每个 hash 编码可以生成一个下标 i,将 ThreadLocal 对象与待 set 的值生成 Entry 对象,存储到 table[i] 中。这一切都是在操作当前线程的局部属性 —— threadLocals,因此跟其他线程没有关系。
当 getMap(t) 方法返回的 map 不为空,则执行 map.set(this, value); 方法,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(); // 当前位置是之前设置的,value 直接覆盖 if (k == key) { e.value = value; return; } if (k == null) { replaceStaleEntry(key, value, i); return; } } // 此时 tab[i] 为空,存储 Entry 对象 tab[i] = new Entry(key, value); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }
该方法大致处理逻辑为:首先,生成 key 对象的下标 i,生成方法与之前介绍的一样。因为不同的 ThreadLocal 都生成 [0, len-1] 区间的下标,可能会导致多个不同的 ThreadLocal 对象生成的下标 i 是一样的,产生 Hash 碰撞,也就是说两个 hash 编码抢占同一个位置,对于该例子处理 hash 碰撞的方法为开放地址法,即:从当前的位置继续向下寻找下一个位置(例子中 for 循环的 nextIndex(i, len) 语句),直到找到的位置为空,则存下当前的值。解决 Hash 碰撞的方法除了开发地址法,还有再 Hash 法和拉链法有兴趣的朋友可以自行查阅。
理解 set 方法后,在看 get 方法就容易了,思路都是一致的。ThreadLocal 中 get 方法如下 :
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(); } 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; }
如果 getMap(t) 返回值为空,这说明没有初始化过,则调用 setInitialValue 方法进行初始化。否则,执行 map.getEntry(this); 语句,代码如下:
private ThreadLocal.ThreadLocalMap.Entry getEntry(ThreadLocal<?> key) { int i = key.threadLocalHashCode & (table.length - 1); ThreadLocal.ThreadLocalMap.Entry e = table[i]; if (e != null && e.get() == key) return e; else return getEntryAfterMiss(key, i, e); }
与 set 代码类似,首先生成下标 i,因为存在 Hash 碰撞,也就是说下标 i 存储的 ThreadLocal 对象不一定是方法形参 key,因此需要在 if 语句中增加 e.get() == key 判断。
本篇文章介绍了 ThreadLocal 的使用及原理。每个线程设置 ThreadLocal 时,其实都是设置每个线程本地的属性,而 ThreadLocal 不存储与线程相关的变量。ThreadLocal 只提供了读写方法,这样做的好处是职责清晰,降低耦合度。有点像农夫山泉的广告语,ThreadLocal 不生产数据,只是线程的搬运工。写这篇文章的时候还感着冒,如表述不当,烦请指正。
欢迎关注公众号 「渡码」 ,分享更多优秀书籍的笔记