ThreadLocal直译为线程局部变量,或许将它命名为ThreadLocalVariable更为合适。其主要作用就是实现线程本地存储功能,通过线程本地资源隔离,解决多线程并发场景下线程安全问题。
接下来,通过ThreadLocal的使用案例、应用场景、源码分析来进行深层次的剖析,说明如何避免使用中出现问题以及解决方案。
前面提到关于ThreadLocal的线程隔离性,通过下面一个简单的例子来演示ThreadLocal的隔离性。
package com.starsray.test.tl; import java.util.ArrayList; import java.util.List; public class ThreadLocalTest { // 声明一个ThreadLocal成员变量 private final ThreadLocal<Person> tl = new ThreadLocal<>(); // 声明一个List作为参照对象 private final List<Person> list = new ArrayList<>(); public static void main(String[] args) { new ThreadLocalTest().test(); } public void test() { // 创建测试Person对象 Person person = new Person(); person.setName("张三"); person.setAge(24); // 创建线程一:再启动1s后,分别添加person对象到tl、list对象中 new Thread(() -> { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } tl.set(person); list.add(person); System.out.println(Thread.currentThread().getName() + " [thread] get():" + tl.get()); System.out.println(Thread.currentThread().getName() + " [list] get():" + list.get(0)); },"thread-1").start(); // 创建线程二:在启动2s后,分别去tl、list对象中取person对象 new Thread(() -> { try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " [thread] get():" + tl.get()); System.out.println(Thread.currentThread().getName() + " [list] get():" + list.get(0)); },"thread-2").start(); } // 测试静态内部类Person static class Person { private String name; private int age; public void setName(String name) { this.name = name; } public void setAge(int age) { this.age = age; } @Override public String toString() { return "Person{" + "name='" + name + '\'' + ", age=" + age + '}'; } } }
案例中使用两个线程同时对List和ThreadLocal对象进行操作,通过对照实验,从输出结果可以看到,ThreadLocal实现了线程间数据隔离,这也说明每一个Thread对象维护了自己的一份数据。
hread-1 [thread] get():Person{name='张三', age=24} thread-1 [list] get():Person{name='张三', age=24} thread-2 [thread] get():null thread-2 [list] get():Person{name='张三', age=24}
针对ThreadLocal而言,由于其适合隔离、线程本地存储等特性,因此天然的适合一些Web应用场景,比如下面所列举的例子:
通过使用案例的展示,接下来对ThreadLocal的实现原理进行简单分析。
在对ThreadLocal的源码展开描述之前,首先简单提一下Java中四种引用类型,强、软、若、虚之一的弱引用,这四种引用关系引用程度依次降低。Java中弱引用通过WeakReference表示,在JDK1.2引入。
public class WeakReference<T> extends Reference<T> { // 创建一个给定类型的对象弱引用 public WeakReference(T referent) { super(referent); } // 创建一个给定类型的对象弱引用,并注册到队列 public WeakReference(T referent, ReferenceQueue<? super T> q) { super(referent, q); } }
弱引用用来描述非必须对象,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。如果发生垃圾收集,无论内存空间是否满足,都会回收掉被弱引用关联的对象。
例如下面代码模拟,为了便于模拟出效果,指定虚拟机启动参数:-Xms4m -Xmx4m
public class WeakRefTest { @Override protected void finalize() { System.out.println("gc"); } public static void main(String[] args) { for (int i = 0; i < 500; i++) { WeakRefTest weakRefTest = new WeakRefTest(); new WeakReference<>(weakRefTest); if (i >= 450) { System.gc(); } } } }
执行结果:
gc ...
关于弱引用的特性,为什么ThreadLocal中要使用弱引用来维护一个对象,后面会继续进行描述。
ThreadLocalMap是ThreadLocal的一个静态内部类。每一个Thread对象实例中都维护了ThreadLocalMap对象,对象本质存储了一组以ThreadLocal为key(this对象实际使用的是唯一threadLocalHashCode值),以本地线程包含变量为value的K-V键值对。
在ThreadLocalMap内部还维护了一个Entry静态内部类,该类继承了WeakReference,并指定其所引用的泛型类为ThreadLocal类型。Entry是一个键值对结构,使用ThreadLocal类型对象作为引用的key。
static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }
查看Entry源码。Entry之所以使用数组结构,一个Thread在运行的过程中会存在多个ThreadLocal对象的场景,ThreadLocalMap作为ThreadLocal的静态内部类,需要维护多个ThreadLocal对象所存储的value值。
// 初始化默认容量为 16 private static final int INITIAL_CAPACITY = 16; // 数据存储结构底层实现为Entry数组,其长度必须为2的倍数 private Entry[] table; // table中Entry的实际数量,初始值为0 private int size = 0; // 存储的阈值 private int threshold; // Default to 0 // resize扩容阈值加载因子为2/3 private void setThreshold(int len) { threshold = len * 2 / 3; }
整个ThreadLocal类中核心内容都是对ThreadLocalMap进行操作,而ThreadLocalMap的核心内容都是围绕Entry组成的Map存储结构进行操作。关于ThreadLocal、ThreadLocalMap、Entry之间的关系如图所示:
ThreadLocal对象是当前线程的ThreadLocalMap的访问入口,Thread类中维护了两个关于ThreadLocalMap的成员变量。
// ThreadLocal变量 ThreadLocal.ThreadLocalMap threadLocals = null; // InheritableThreadLocal变量,该类继承自ThreadLocal ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
threadLocals在Thread类中作为成员变量,初始化线程对象时并不会被赋予值,只有在使用ThreadLocal时进行赋值。查看ThreadLocal中的get方法
public T get() { // 获取当前操作线程 Thread t = Thread.currentThread(); // 调用getMap方法,返回当前线程的实例变量threadLocals值 ThreadLocalMap map = getMap(t); // 如果返回map不为空,返回map中所存储的以当前ThreadLocal对象为key的值 if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } // 如果map为空进行map值的初始化 return setInitialValue(); } ThreadLocalMap getMap(Thread t) { // 返回传入线程(当前线程)中成员变量的threadLocals值 return t.threadLocals; } private T setInitialValue() { // 调用initialValue()方法设置初始值,默认不设置任何值,可以在创建ThreadLocal // 对象时被重写进行初始化,只会进行一次初始化。 T value = initialValue(); Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); return value; } void createMap(Thread t, T firstValue) { // 初始化当前线程对象实例变量threadLocals的值,Map所对应的key为当前ThreadLocal对象 t.threadLocals = new ThreadLocalMap(this, firstValue); }
接下来查看set方法
public void set(T value) { // 获取当前线程对象 Thread t = Thread.currentThread(); // 调用getMap方法,传入当前对象的值,获取当前线程的实例变量threadLocals值 ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else // 如果map为空,创建ThreadLocalMap createMap(t, value); }
而inheritableThreadLocals会在创建线程时,根据线程构造方法传参,确定是否进行初始化。
// 该init方法为Thread内部线程初始化方法,inheritThreadLocals是否继承父类变量,默认false private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, boolean inheritThreadLocals); // 如果inheritThreadLocals为true并且parent(为当前线程,视为要被继承线程的父线程) // 的ThreadLocal不为null,调用createInheritedMap方法进行继承初始化 if (inheritThreadLocals && parent.inheritableThreadLocals != null) this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); // 为子线程创建一个新的ThreadLocalMap并初始化parentMap中的变量实现继承 static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) { return new ThreadLocalMap(parentMap); }
ThreadLocal提供了便利的同时当然也需要注意在使用过程中的一些细节问题。下面进行简单总结
ThreadLocal默认情况下不会进行子线程对父线程变量的传递性,在开启异步线程的时候需要注意这一点,关于这一点可以通过Thread类构造方法提供的inheritThreadLocals参数进行封装,或者使用Spring根据装饰器模式进行封装的TaskDecorator类实现跨线程传递方法。
线程池中线程调用使用ThreadLocal 需要注意,由于线程池中对线程管理都是采用线程复用的方法,在线程池中线程非常难结束甚至于永远不会结束,这将意味着线程持续的时间将不可预测。另外重复使用可能导致ThreadLocal
对象未被清理,在ThreadLocalMap中进行值操作时被覆盖,或取到旧值。如下代码所示:
package com.starsray.test.thread; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicInteger; public class ThreadLocalPoolTest { private final static ThreadLocal<AtomicInteger> tl = ThreadLocal.withInitial(() -> new AtomicInteger(0)); static class Task implements Runnable{ @Override public void run() { System.out.println(tl.get().incrementAndGet()); } } public static void main(String[] args) { ExecutorService pool = Executors.newFixedThreadPool(2); for (int i = 0; i < 5; i++) { pool.execute(new Task()); } pool.shutdown(); } }
期待的输出结果应该是1,实际输出结果,由于超出后线程被复用,输出结果也会取到旧值。
1 1 2 2 3 3
当然,如果必须要在线程池中使用ThreadLocal也不是不能使用,在线程池类ThreadPoolExecutor中定义了钩子函数,可以在初始化或者任务执行完做特殊处理,如初始化ThreadLocal或者记录日志。重写beforeExecute方法:
protected void beforeExecute(Thread t, Runnable r) { }
ThreadLocal对象不仅提供了get、set方法,还提供了remove方法。虽然get、set已经对空值进行清理,但在实际使用时,手动调用remove方法养成良好的编程习惯是非常有必要的。
ThreadLocal中主要的存储单元Entry类继承了WeakReference,该类的引用在虚拟机进行GC时会被进行清理,但是对于value如果是强引用类型,就需要进行手动remove,避免value的内存泄露。关于引用关系参考下图所示:
关于内存泄露这块的重点在于两部分:
具体细节查看remove部分的源码:
public void remove() { // 获取当前线程中threadLocals对应的ThreadLocalMap ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) // 如果ThreadLocalMap不为空,继续调用remove(this)方法 m.remove(this); }
查看remove(this)具体内容
private void remove(ThreadLocal<?> key) { // 创建新都tab数组,引用指向当前ThreadLocal对象中的table Entry[] tab = table; // tab的长度 int len = tab.length; // 根据当前ThreadLocal对象的唯一threadLocalHashCode值并通过与操作 // 获取当前ThreadLocal对象在table中value所在的下标值i int i = key.threadLocalHashCode & (len-1); for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { // 对table进行遍历,如果Entry所对应的key为当前ThreadLocal对象,执行clear方法 if (e.get() == key) { // 将引用置为null e.clear(); // 清楚陈旧的键值对 expungeStaleEntry(i); return; } } }
查看clear()方法,Clear方法位于Reference类中,由于Entry类继承了WeakReference(继承WeakReference),此处的clear属于多态的应用。
public void clear() { this.referent = null; }
接下来expungStaleEntry(i)方法则是整个remove的核心逻辑了,这里首先再明确以下两个变量的意义:
private int expungeStaleEntry(int staleSlot) { // 创建新都tab数组,引用指向当前ThreadLocal对象中的table Entry[] tab = table; // tab的长度 int len = tab.length; // 将tab中下标staleSlot(i)对应的value引用置为null tab[staleSlot].value = null; // 将tab中下标staleSlot的Entry置为null tab[staleSlot] = null; // Entry对应的长度减1 size--; // Rehash until we encounter null 直到遇到null开始rehash Entry e; int i; // 从staleSlot后以索引开始遍历,直到遇到某个Entry不为空为止 for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) { // 获取Entry对应的ThreadLocal对象的引用key值 ThreadLocal<?> k = e.get(); if (k == null) { // 如果为空,将value和键值对同时置空,size减1 e.value = null; tab[i] = null; size--; } else { // 如果k不为null,说明弱引用未被GC回收,获取table中k对应的下标 int h = k.threadLocalHashCode & (len - 1); // 判断传入下标,与当前k对象的下标是否一直 if (h != i) { // 如果不一致,需要对tab中的值进行更新,直接清空 tab[i] = null; // Unlike Knuth 6.4 Algorithm R, we must scan until // null because multiple entries could have been stale. while (tab[h] != null) // 采用R算法的变种,从当前h开始寻找一个为null的值存储e h = nextIndex(h, len); tab[h] = e; } } } // 返回第一个entry为null的下标 return i; }
关于expungeStaleEntry中原作者对关键地方进行了英文注释,源码提及了Knuth 6.4 Algorithm R算法,R算法主要说明了如何从使用线性探测的散列表中删除一个元素。
与Knuth 6.4算法R不同,这里必须扫描到null,可能出现空的Entry,多个条目可能已经过时,由于不使用引用队列,因此只有在表开始空间不足时才能保证删除过时的条目。
Thread对象中通过维护了一个ThreadLocal.ThreadLocalMap类型的threadLocals变量实现线程间变量隔离,并维护了一个ThreadLocal.ThreadLocalMap类型的inheritableThreadLocals变量实现线程间变量的继承,是否继承由线程初始化时inheritThreadLocals参数进行决定,默认不继承。
ThreadLocal中核心存储的类为ThreadLocalMap类,ThreadLocalMap类本身是一个定制化的Map,这个Map以当前ThreadLocal对象作为key值进行K-V存储。ThreadLocalMap的初始化容量为16,扩容因子为2/3。
ThreadLocalMap在进行存储时,会获取当前this对象的threadLocalHashCode值(这也是为什么使用ThreadLocal作为key的原因),该值是唯一的,只在ThreadLocalMap中有用,使用Unsafe提供的AtomicInt类操作获取。
ThreadLocalMap中进行存储的基本单位为Entry数组,数组下标通过threadLocalHashCode进行&运算并根据当前数组长度进行自动扩容。
说明:为什么ThreadLocal的key要使用当前ThreadLocal对象或者说是threadLocalHashCode的值,而不是使用当前线程对象?
一个ThreadLocal对象只会对应一个线程对象,但是一个Thread对象会存在多个ThreadLocal对象,之所以不使用Thread对象作为key,是为了避免多个ThreadLocal对象(或者说T、、hreadLocalMap)之间的互相影响。