Java中ThreadLocal详解(一篇就够了)
前言
ThreadLocal直译为线程局部变量,或许将它命名为ThreadLocalVariable更为合适。其主要作用就是实现线程本地存储功能,通过线程本地资源隔离,解决多线程并发场景下线程安全问题。
ThreadLocal
接下来,通过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应用场景,比如下面所列举的例子:
- 代替参数显式传递(很少使用)
- 存储全局用户登录信息
- 存储数据库连接,以及Session等信息
- Spring事务处理方案
源码分析
通过使用案例的展示,接下来对ThreadLocal的实现原理进行简单分析。
WeakReference
在对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();
}
}
}
}
- finalize()是Object方法,当虚拟机在回收对象时,允许执行完该方法后再进行回收
- System.gc()会通知虚拟机进行垃圾回收,并不会立即进行垃圾回收
执行结果:
gc
...
关于弱引用的特性,为什么ThreadLocal中要使用弱引用来维护一个对象,后面会继续进行描述。
ThreadLocalMap
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);
}
threadLocalHashCode
在ThreadLocal的成员变量中包含了threadLocalHashCode
和HASH_INCREMENT
两个成员变量:
private final int threadLocalHashCode = nextHashCode();
// 连续生成的哈希码之间的差异
// 将隐式顺序线程本地 ID 转换为接近最佳传播的乘法哈希值,用于二次幂大小的表
private static final int HASH_INCREMENT = 0x61c88647;
关于threadLocalHashCode
的解释:
ThreadLocals 依赖于附加到每个线程(Thread.threadLocals 和inheritableThreadLocals)的每线程线性探针哈希映射。 ThreadLocal 对象充当键,通过 threadLocalHashCode 进行搜索。这是一个自定义哈希码(仅在 ThreadLocalMaps 中有用),它消除了在相同线程使用连续构造的 ThreadLocals 的常见情况下的冲突,同时在不太常见的情况下保持良好行为。
HASH_INCREMENT
是用来计算下一个哈希码(threadLocalHashCode)的哈希魔数,源码中用十六进制表示,其对应的十进制数为1640531527,至于为什么是这个数值并不是偶然,而是这个数在有符号的int范围内是黄金分割数。
如下代码所示,输出的结果刚好是-1640531527
,也就是说是32位有符号整数的黄金分割值。
public static void main(String[] args) {
long c = (long) ((1L << 32) * (Math.sqrt(5) - 1) / 2);
System.out.println(c);
//强制转换为带符号为的32位整型,值为-1640531527
int i = (int) c;
System.out.println(i);
System.out.println(Integer.MAX_VALUE);
}
ThreadLocal在ThreadLocalMap中是根据ThreadLocal对象的threadLocalHashCode进行索引的,查看下面一段源码,从源码可以看到在Entry表里求下标的算法为:
哈希key:keyIndex = key.threadLocalHashCode & (table.length - 1);
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
哈希key的求值也可以看作:keyIndex = ((i + 1) * HASH_INCREMENT) & (length - 1)
,i
为ThreadLocal实例的个数,HASH_INCREMENT
是哈希魔数0x61c88647
,length
为ThreadLocalMap
中可容纳的Entry的容量。初始容量为16,扩容后总是2的幂次方。
下面做个测试为什么使用HASH_INCREMENT
作为魔数,以及求取下标的算法巧妙之处:
public class TestThreadLocalHashCode {
public static void main(String[] args) {
hashCode(4);
hashCode(16);
hashCode(32);
hashCode(64);
}
private static void hashCode(int capacity) {
final int HASH_INCREMENT = 0x61c88647;
final AtomicInteger nextHashCode = new AtomicInteger(HASH_INCREMENT);
int keyIndex;
for (int i = 0; i < capacity; i++) {
keyIndex = nextHashCode.getAndAdd(HASH_INCREMENT) & (capacity - 1);
// keyIndex = ((i + 1) * HASH_INCREMENT) & (capacity - 1);
System.out.print(keyIndex + " ");
}
System.out.println();
}
}
在不触发二次扩容的场景下,每个ThreadLocalMap
中的元素分别为4,16,32,64,输出结果:
3 2 1 0
7 14 5 12 3 10 1 8 15 6 13 4 11 2 9 0
7 14 21 28 3 10 17 24 31 6 13 20 27 2 9 16 23 30 5 12 19 26 1 8 15 22 29 4 11 18 25 0
7 14 21 28 35 42 49 56 63 6 13 20 27 34 41 48 55 62 5 12 19 26 33 40 47 54 61 4 11 18 25 32 39 46 53 60 3 10 17 24 31 38 45 52 59 2 9 16 23 30 37 44 51 58 1 8 15 22 29 36 43 50 57 0
每组测试在进行散列算法后刚好填满了整个容器,实现了完美散列,这使得ThreadLocal在使用的过程中可以尽可能高的提高在ThreadLocalMap中获取元素的命中率,提高了ThreadLocal在使用中的效率。
此外从输出结果来看,ThreadLocal中使用了斐波那契散列法,保证哈希表的离散度。它选用的乘数值即是2^32的黄金分割比0x61c88647
。
注意事项
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的内存泄露。关于引用关系参考下图所示:
关于内存泄露这块的重点在于两部分:
- ThreadLocal被一强(tl = new强引用)一弱(WeakReference<ThreadLocal<?>>)两部分引用,强引用可以通过编码解决(tl = null),而弱引用部分在GC时会自动清理掉key部分的引用。
- 关于value部分的引用,如果是强引用类型的value通过remove方法可以清理,避免内存泄露。
具体细节查看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的核心逻辑了,这里首先再明确以下两个变量的意义:
- size:前面提到size是Entry的数量,即ThreadLocal中成员变量table的实际键值对数量
- i:table中与当前ThreadLocal对象相匹配的Entry的key值的下标
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对象(或者说ThreadLocalMap)之间的互相影响。
本文来自博客园,作者:星光Starsray,转载请注明原文链接:https://www.cnblogs.com/starsray/p/16220037.html