ThreadLocal是什么?
在Java体系中有一个ThreadLocal类,它用于提供线程内部的局部变量,这些变量与它们的正常对象不同,每个线程拥有一个单独属于自己的,独立的变量的初始副本。
简单来说,通过ThreadLocal可以实现多线程中数据隔离效果。
ThreadLocal如何使用?
通过ThreadLocal的简单使用,来验证数据隔离效果:
public class Main {
public static void main(String[] args) {
ThreadLocal<String> threadLocal = new ThreadLocal<>(); // 使用同一个threadLocal对象
new Thread(new Runnable() {
@Override
public void run() {
try {
threadLocal.set("a"); // 设置字符串“a”
Thread.currentThread().sleep(3000); // 此处睡眠3秒钟,让其他线程拥有足够时间更新threadLocal中的数据
System.out.println(threadLocal.get()); // 查看设置的字符串是否还是当前线程中设置的值
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
threadLocal.set("b"); // 设置字符串“b”
}
}).start();
}
}
ThreadLocalMap内部类
ThreadLocalMap是ThreadLocal中的一个静态内部类,它实现了ThreadLocal类中大部分的底层操作,也是实现各个线程数据隔离的关键所在。
ThreadLocalMap这个类本质上是一个map,和HashMap之类的实现相似,保存数据依然是key-value的形式。
所以在全面了解ThreadLocal之前,先来简单了解下ThreadLocalMap的各个重要属性:
/**
* ThreadLocalMap其中还有一个静态内部类Entry,可以将其看做保存的数据结点
* 其中key可以看做是ThreadLocal实例,但是其本质是持有ThreadLocal实例的弱引用
* 其中value就是对应保存的数据了
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
// 保存数据结点的数组
private Entry[] table;
// 数据初始容量
private static final int INITIAL_CAPACITY = 16;
// 记录数据结点数量
private int size = 0;
// 数组扩容的临界值,默认是0
private int threshold;
从上面的注释可以知道,这个Entry内部类其实和HashMap的Node内部类十分类似,都是用于封装数据的类。
但是这里有个疑惑,对于这个Entry类中的保存key值的方式是什么呢?我们来看下它的构造方法:
Entry(ThreadLocal<?> k, Object v) {
super(k); // 调用父类的构造方法,将ThreadLocal实例作为key值传递进去
value = v;
}
public WeakReference(T referent) {
super(referent); // 再次调用父类构造方法,将ThreadLocal实例作为参数传递进去
}
Reference(T referent) {
this(referent, null);
}
Reference(T referent, ReferenceQueue<? super T> queue) {
this.referent = referent; // 从这里可以看到,最终ThreadLocal实例赋值给了当前实例的referent属性
this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
}
到此,以上内容就是ThreadLocalMap内部类的初步认识。
ThreadLocal重点方法源码解析
set
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t); // 获取ThreadLocalMap实例对象map
if (map != null) // map对象不为空,说明已初始化
map.set(this, value); // 直接设置添加数据
else
createMap(t, value); // 否则,先初始化ThreadLocalMap,再添加数据
}
- getMap——获取ThreadLocalMap实例对象
/**
* 其实通过返回值就可以看到,所谓的getMap获取ThreadLocalMap实例对象中的实例对象,其实就是Thread线程类中的一个属性
* 说明,不同的线程就会拥有不同的ThreadLocalMap实例对象
* 也正是如此,才实现了不同线程之间的数据隔离
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
- map.set——为ThreadLocalMap中设置添加数据
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1); // 根据ThreadLocal的哈希码和table的长度,再通过路由算法找出应该将数据存放在table数组的位置索引
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { // 遍历整个table,查看是否有可替换的数据
ThreadLocal<?> k = e.get(); // 获取元素中的ThreadLocal实例对象k
if (k == key) { // k与key双方地址相等,说明找到同个ThreadLocal实例对象
e.value = value; // 替换value值
return;
}
if (k == null) { // k为空,说明原本数据中的key值为空,原数据已失效
replaceStaleEntry(key, value, i); // 替换无效数据,并清除其他无效数据
return;
}
}
// 无可替换的数据,则添加数据
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold) // 计划清除从i位置往后的无效数据,但是不存在无效数据,而且数据结点个数大于等于table扩容临界值
rehash(); // 调整table大小
}
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0); // 由此可看出,当获取下一个索引超过table的长度时,就会重新归0。说明,可以将table看作是一个环形数组
}
public T get() {
return this.referent; // 还记的之前保存数据的key值时候的referent属性吗?是的,这里就是获取当初保存的ThreadLocal实例对象
}
- replaceStaleEntry——替换无效数据
private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
int slotToExpunge = staleSlot; // slotToExpunge之后会作为清除无效数据的起点
for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len)) // 往前遍历,寻找第一个key为空的无效数据,在之后作为清除无效数据的起始索引位置
if (e.get() == null)
slotToExpunge = i;
for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == key) { // 有已存在key和需要保存的key相等,则更新其value值,并替换掉staleSlot索引位置中key为空的数据
e.value = value;
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
if (slotToExpunge == staleSlot) // 相等,说明往前遍历寻找清除无效数据起始索引没有找到,staleSlot位置前面的数据都是有效的
slotToExpunge = i; // 因为进行了数据替换,现在无效数据处于i索引位置,所以更新slotToExpunge
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); // 清除无效数据
return;
}
if (k == null && slotToExpunge == staleSlot) // 往前查找没有找到无效数据,而且当前i位置中的是无效数据,更新slotToExpunge(对于staleSlot中的无效数据会在之后的操作中替换成有效数据)
slotToExpunge = i;
}
// 没有找到可替换的有效数据,则直接将staleSlot位置的无效数据进行更新
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
if (slotToExpunge != staleSlot) // 不相等,说明在存在slotToExpunge之后无效数据
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
- expungeStaleEntry——从指定的索引位置往后开始清除无效数据,并返回从staleSlot之后的第一个空数据的索引位置
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// 首先清除第一个无效数据
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
Entry e;
int i;
for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) { // 往后遍历,清除无效数据
ThreadLocal<?> k = e.get();
if (k == null) { // k为空,清除该位置的无效数据
e.value = null;
tab[i] = null;
size--;
} else { // k不为空,则根据k值重新计算索引
int h = k.threadLocalHashCode & (len - 1);
if (h != i) { // 计算后的索引与当前索引不相等
tab[i] = null; // i索引位置置空
while (tab[h] != null) // 从h往后寻找到为空的位置,将原i索引位置的数据移动过去
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
- cleanSomeSlots——清除从指定位置之后的无效数据,若存在无效数据则返回true,否则返回false
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
do { // 循环遍历,删除失效数据
i = nextIndex(i, len);
Entry e = tab[i];
if (e != null && e.get() == null) {
n = len;
removed = true;
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0);
return removed;
}
- rehash——调整table大小
private void rehash() {
expungeStaleEntries(); // 删除table中存在的无效数据
if (size >= threshold - threshold / 4) // 删除完无效数据后,仍然超过扩容临界值
resize(); // 扩容
}
private void expungeStaleEntries() {
Entry[] tab = table;
int len = tab.length;
for (int j = 0; j < len; j++) { // 从头开始遍历,删除所有无效数据
Entry e = tab[j];
if (e != null && e.get() == null)
expungeStaleEntry(j);
}
}
- resize——table扩容
private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
int newLen = oldLen * 2; // 扩容为原table的2倍
Entry[] newTab = new Entry[newLen];
int count = 0;
for (int j = 0; j < oldLen; ++j) { // 将原table中的数据迁移到新table中
Entry e = oldTab[j];
if (e != null) {
ThreadLocal<?> k = e.get();
if (k == null) {
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;
}
- createMap——创建ThreadLocalMap
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY]; // 初始化table
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); // 计算索引
table[i] = new Entry(firstKey, firstValue); // 保存数据
size = 1;
setThreshold(INITIAL_CAPACITY); // 设置临界值
}
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(); // map为空,或者获取到的e为空,则初始化map,或者初始化数据
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals; // 返回当前线程的ThreadLocalMap实例对象
}
- getEntry——根据指定ThreadLocal获取value
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1); // 计算索引
Entry e = table[i];
if (e != null && e.get() == key) // key值相等,说明找到指定数据,并返回
return e;
else
return getEntryAfterMiss(key, i, e); // e等于空,或者key值不相等,说明原位置数据失效被删除,或者被迁移到其他位置去了
}
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) { // e不等于空,说明是key值不相等,说明原位置数据被迁移。则往后寻找指定数据
ThreadLocal<?> k = e.get();
if (k == key)
return e;
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null; // e等于空,说明原位置数据失效被删除,返回空
}
- 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;
}
protected T initialValue() {
return null; // 此处返回空,该方法是用于子类覆盖的
}
remove
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
private void remove(ThreadLocal<?> key) {
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)]) { // 循环遍历table
if (e.get() == key) {
e.clear(); // 清空指定数据
expungeStaleEntry(i); // 清除无效数据
return;
}
}
}
public void clear() {
this.referent = null;
}
总体来说,ThreadLocal实现数据隔离的关键就是内部类ThreadLocalMap。从源码分析的角度来看,ThreadLocalMap其实是Thread线程类的一个属性,也就是说不同的线程拥有不同的ThreadLocalMap,而ThreadLocalMap中的Key又是相同的ThreadLocal对象,由此达到数据隔离的效果
ThreadLocal内存泄漏问题
对于ThreadLocal来说,虽然可以实现数据隔离,但使用不当的话会存在内存泄漏问题。
基于上面的源码解析,可以知道ThreadLocal在内存方面上的实现如下:
在讲解ThreadLocalMap如何存储以ThreadLocal作为key值存储的时候,有说到是作为弱引用来存储。正因为是弱引用,在GC垃圾回收机制工作的时候,会自动回收这个key。自然地,当再次调用set()方法设置数据时候,就无法根据key值找到这个Entry进行替换了,而是执行添加数据的操作。而原本Entry的key虽然被回收,但是value的引用却是强引用,GC不会回收,导致了已经无法获取的Entry仍然会存在于内存中。当这样的Entry越来越多,占用的内存也就越多,就会导致内存泄漏问题。
因此需要及时调用remove()方法。
ThreadLocal脏数据问题
从以上分析可知,之所以使用ThreadLocal可以达到各个线程数据隔离的效果,是因为每个线程都有自己的ThreadLocalMap,以ThreadLocal作为key值存储。也就是说,只要你是同个线程,那么你的ThreadLocalMap就是同一个,自然地其中保存的数据也是相同的。
那么,如果我使用线程池呢?
了解线程池的都知道,使用线程池就是为了线程复用,而使用线程复用就会导致复用了同一个ThreadLocalMap,导致这次线程任务get到的数据有可能是上次任务的,这样就会导致脏数据问题。对于这个问题,没有什么特别的办法,只能说是针对业务谨慎使用ThreadLocal。