前言
对于ThreadLocal,那是众所周知的,因为在项目中它是常常被使用的,先简单说下平常使用的场景:
- 设置每个线程所需要的独享数据
- 设置内存中需要的全局变量(譬如拦截其中的用户信息)
在ThreadLocal类的注释中,作者给的便是这样的定义,如图:
这段注释大体的意思是说(笔者英语极差):ThreadLocal这个类提供了thread-local变量,这些变量与普通变量不同,每个线程可以根据get或者set方法来操作自己独立初始化的变量副本。ThreadLocal 实例通常是类中的 private static 字段,是要将状态与线程相关联的。
那么上面说的两种使用场景其实都是基于这样的一个定义去做的,这里就第一种场景来提供一下代码实现:
public class SimpleDateFormatterDemo {
public static ExecutorService executorService = Executors.newFixedThreadPool(10);
public static void main(String[] args) {
for (int i = 0; i< 10; i++) {
int j = i;
executorService.submit(new Runnable() {
@Override
public void run() {
String date = new SimpleDateFormatterDemo().date(j);
System.out.println(date);
}
});
}
executorService.shutdown();
}
public String date(int seconds) {
Date date = new Date(1000 * seconds);
SimpleDateFormat dateFormat = SafeDateFormatter.dateFormatThreadLocal.get();
return dateFormat.format(date);
}
static class SafeDateFormatter {
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
};
}
}
二、ThreadLocal的设计
先不多说,直接上ThreadLocal的UML图,如下:
从上面的图中可以得出,ThreadLocal内部有 SuppliedThreadLocal 和 ThreadLocalMap这两个类,在 ThreadLocalMap 内部还有一个 Entry 内部类,这个类它是一个键值对(键:就是当前的类ThreadLocal,值是实际的成员变量,譬如上面那个示例代码中的SimpleDateFormat),Entry是基于 WeakReference 的,这样的设计也给ThreadLocal带来了一些相应的问题,我们后面再讨论这个。这里的 ThreadLocalMap 其实就是一个定制的 HashMap。
其实ThreadLocal是基于Thread类的,而在Threa中还有一个inheritableThreadLocals(另一个是threadLocals),在默认情况下,每个线程中的这两个变量都为null,只有在当前线程第一次调用ThreadLocal的 set 或 get 方法的时候才会创建他们,每个线程的本地变量并不是放在ThreadLocal实例中的,而是放在 threadLocals 中的,当线程调用get方法的时候,再从当前线程的threadLocals 变量中将其拿出来使用,当不需要的时候在调用remve方法,从当前线程的threadLocals中删除数据。在这里ThreadLocal的作者将Thread的threadLocals设计为map结构,其实是为了使每个线程可以关联多个ThreadLocal变量。
三、ThreadLocalMap源码
在上面说了下ThreadLocalMap的代码实现,这里我们来看看ThreadLocalMap的源码,注:ThreadLocalMap中是采用线性探测法,也就是如果发生冲突,就会继续找下一个空位置,而不是用链表拉链。
成员变量
// 初始容量,必须是2次幂
private static final int INITIAL_CAPACITY = 16;
// 存放数据的table,Entry类的定义在下面分析,table.length必须是2的冥
private Entry[] table;
// 数组里面entrys的个数,可以用于判断table当前使用量是否超过负因子
private int size = 0;
// 进行扩容的阈值,表使用量大于它的时候进行扩容
private int threshold; // Default to 0
// 定义为长度的2/3
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
存储方式:Entry
/**
* Entry继承WeakReference,并且用ThreadLocal作为key.如果key为null
* (entry.get() == null)表示key不再被引用,表示ThreadLocal对象被回收
* 因此这时候value也可以从table从清除。
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
前面说了这里的Entry是继承自WeakReference这个类,且在Entry的构造函数中,关于key的赋值方式是通过WeakReference去赋值的,而弱引用的特点是如果这个对象只被弱引用关联,那么这个对象就可以被回收,所以弱引用不会阻止GC。ThreadLocalMap的每个Entry都是一个对key的弱引用,同时每个Entry又包含一个对value的强引用,因此在正常的情况下,当线程终止,保存在ThreadLocal里的value会被垃圾回收,因为没有任何强引用。但是当这个线程一直不终止,那么key对应的value就不能被回收,因此就会产生这样的调用连:Thread——> ThreadLocalMap ——> Entry(key为null)——> Value。因为value和Thread值阿健存在强引用关系,所以会导致value无法回收,以至于可能出现OOM。但是JDK已经帮我们想到了这一点,看源码:
private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
int newLen = oldLen * 2;
Entry[] newTab = new Entry[newLen];
int count = 0;
for (int j = 0; j < oldLen; ++j) {
Entry e = oldTab[j];
if (e != null) {
ThreadLocal<?> k = e.get();
if (k == null) {
// 这里判断当前的key为null时,就会把value的值设为null,一次来帮助GC
e.value = null; // Help the GC
} else {
int h = k.threadLocalHashCode & (newLen - 1);
while (newTab[h] != null)
h = nextIndex(h, newLen);
newTab[h] = e;
count++;
}
}
}
setThreshold(newLen);
size = count;
table = newTab;
}
四、set方法源码
public void set(T value) {
// 获取当前线程
Thread t = Thread.currentThread();
// 将当前线程作为key,去查找对应的线程变量,找到了之后便进行设置
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
// 第一次调用便创建当前线程对应的HashMap
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
这段代码很简单,不过在前面有说过,ThreadLocalMap是在第一次调用get()或者set()方法的时候才会进行初始化的。代码如下:
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
// 初始化 Entry
table = new Entry[INITIAL_CAPACITY];
// 计算索引值
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
// 根据索引值进行设置
table[i] = new Entry(firstKey, firstValue);
size = 1;
// 设置阈值
setThreshold(INITIAL_CAPACITY);
}
这计算索引值的步骤是比较重要的:firstKey.threadLocalHashCode & (INITIAL_CAPACITY – 1),这里firstKey.threadLocalHashCode是基于原子类AtomicInteger,先看具体代码:
private final int threadLocalHashCode = nextHashCode();
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
private static AtomicInteger nextHashCode = new AtomicInteger();
// 基于 unsafe 中的 CAS
public final int getAndAdd(int delta) {
return unsafe.getAndAddInt(this, valueOffset, delta);
}
private static final int HASH_INCREMENT = 0x61c88647;
关于& (INITIAL_CAPACITY – 1),通过AtomicInteger并与其相关的阈值获得hashCode,然后来进行取模,这里是以2次幂作为模数进行取模,用%代替代替2^n。
五、get方法源码
public T get() {
// 获取当前线程
Thread t = Thread.currentThread();
// 获取当前线程的 threadLocals变量
ThreadLocalMap map = getMap(t);
// 如果threadLocals不为null,则返回对应的本地变量的值
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// threadLocals 为 null,则初始化当前线程的threadLocals变量
return setInitialValue();
}
private T setInitialValue() {
// 初始化为 null
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
// 如果当前线程的 threadLocals 变量不为null
if (map != null)
map.set(this, value);
else
// 如果当前线程的 threadLocals 变量为null
createMap(t, value);
return value;
}
protected T initialValue() {
return null;
}
结束语
本文是基于源码分析jdk1.8.0_131,本文主要从以下几点进行源码分析的:
- 简单的说了一下ThreadLocal的设计理念和基本使用
- 分析了ThreadLocal的源码实现与其实现的意义
- ThreadLocalMap源码分析
- set方法源码分析
- get方法源码分析
原创文章,作者:奋斗,如若转载,请注明出处:https://blog.ytso.com/19403.html