1:什么是ThreadLocal?
ThreadLocal是为了解决多线程并发访问的一种方案,它不像Synchronized等其他锁方式,使变量或者代码只能被一个线程访问,而ThreadLocal是为了每个线程提供属于自己线程的变量副本,每个线程读取变量副本都是相互独立的,从而达到线程隔离作用。
2:ThreadLocal简单使用
- void set(T value):设置当前线程局部变量的值
- T get():获取当前线程
- void remove():将当前线程局部变量值移除
public class ThreadLocalDemo {
private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
String name = Thread.currentThread().getName();
threadLocal.set(name);
System.out.println(getName());
});
Thread thread2 = new Thread(() -> {
String name = Thread.currentThread().getName();
threadLocal.set(name);
System.out.println(getName());
});
thread1.start();
thread2.start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getName());
}
public static String getName(){
return threadLocal.get();
}
}
输出结果如下,我们可以从输出结果中看出线程隔离的效果。
3:ThreadLocal的分析
3.1 ThreadLocal是怎么实现线程隔离的
当我们threadLocal.set()时,是怎么每个线程设置自己副本变量的值的。我们从源码分析看:
每次设置都是往各自线程的ThreadLocalMap中存,从而实现线程隔离
3.1.1 set(T value)源码分析
public void set(T value) {
//获取到当前线程
Thread t = Thread.currentThread();
//获取ThreadLocalMap,每个线程都有各自维护的ThreadLocalMap
ThreadLocalMap map = getMap(t);
//不为空则往map中设置值,key为threadLocal的弱引用,value为要设置的值
if (map != null) {
map.set(this, value);
} else {
//创建ThreadLocalMap
createMap(t, value);
}
}
ThreadLocal.ThreadLocalMap threadLocals = null;
//根据线程获取属于自己的ThreadLocalMap
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
//根据线程创建自己的ThreadLocalMap
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
我们可以从上面代码中发现ThreadLocal本身并不存储数据,而是内部有一个ThreadLocalMap,并且每个线程都会有一个与之对应管理的ThreadLocalMap,让我们来解开ThreadLocalMap的神秘面纱
- 从下面代码上我们可以看出创建ThreadLocalMap,内部维护一个Entry[]数组,主要用于存储线程局部变量
- Entry类型,key为一个ThreadLocal的弱引用,value为要设置的值
static class ThreadLocalMap {
private static final int INITIAL_CAPACITY = 16;
private Entry[] table;
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
//初始化一个长度为16的数组
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
//Entry,key为threadLocal的弱引用,value为要添加的值
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}
3.1.2 ThreadLocalMap的set(ThreadLocal<?> key, Object value)方法源码分析
作用就是设置对象,如果存在则更新,不存在则创建新的。然后对一些无用条目的清除,优化空间使用。如果数组对象值超过数组长度的2/3则要进行扩容。
- 找到属于自己线程的Entry[ ]数组
- 然后根据传入的引用ThreadLocal<?> key计算对应的索引下标
- 循环查找数组中是否有对应的不为空的Entry对象,然后用这个Entry对象的引用key和传入的进行比较。如果相同,则把value进行更新,然后return。 如果Entry对象的引用key为null,则要及时清理无用条目(对象不为空,但是ThreadLocal<?> key为空)
- 如果没有对应的Entry对象,则创建新的Entry对象,然后把数组中对象长度+1。
- 然后判断清除一些无用对象是否成功,成功则结束流程,不成功则判断是否需要rehash()进行扩容。
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
//根据哈希取余法,把对应的key映射到对应的索引下标找对应的索引下表
int i = key.threadLocalHashCode & (len-1);
//找到索引后,然后去数组中查询是否存在数据
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
//获取到Entry的key
ThreadLocal<?> k = e.get();
//如果相同,则证明存在相同弱引用threadLocal对象,然后把value更新
if (k == key) {
e.value = value;
return;
}
//为null,则证明可能已经被垃圾回收期标记为可回收
if (k == null) {
/**
查找之前的陈旧条目
初始化一个变量slotToExpunge = i,slotToExpunge是要清理条目的开始位置
往前遍历,如果对象不为空但是引用为空的话,则更新slotToExpunge的值
然后从索引i往后循环遍历,如果table[nextIndex]不为空,且引用==传入的key,则更新查找到
条目的value值,把传入的value复制给他。
如果 slotToExpunge == i的话则再次更新slotToExpunge位置
然后清理从slotToExpunge开始的旧条目位置
*/
//这个方法是为了确保无用的条目要及时清理,优化内存使用和性能
replaceStaleEntry(key, value, i);
return;
}
}
//设置新的Entry
tab[i] = new Entry(key, value);
//然后+1
int sz = ++size;
//cleanSomeSlots清除一些旧的Entry值(如果根据索引找到的Entry对象不为空,但是Key引用为空)。如果
//清理失败,并且size大于threadhold,则进行rehash()扩容
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
3.1.3 get()源码分析
public T get() {
//获取到当前线程
Thread t = Thread.currentThread();
//获取ThreadLocalMap,每个线程都有各自维护的ThreadLocalMap
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();
}
3.1.4 TheadLocalMap.getEntry(ThreadLocal<?> key)源码分析
private Entry getEntry(ThreadLocal<?> key) {
//索引下标
int i = key.threadLocalHashCode & (table.length - 1);
//找到对应Entry对象
Entry e = table[i];
//找到对应Entry对象不为null,且引用 == 传入的引用
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
//对象不为空则一直循环
while (e != null) {
ThreadLocal<?> k = e.get();
//如果相同则返回对象
if (k == key)
return e;
//如果引用为空,则证明已经标记成功,等待垃圾回收器回收,并且清除一些旧的Entry
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
3.1.5 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);
//从i开始往后遍历,对象不为空,如果Entry引用 == key
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
//清除引用对象
e.clear();
//清除无用的Entry对象
expungeStaleEntry(i);
return;
}
}
}
3.1.6 清除无用条目方法源码分析
清除那些Entry[ ]数组中Entry对象不为空,但是Entry对象的ThradLocal<?> key引用对象为空的数据
/*
这个方法主要是为了查找Entry[]数组中哪些对象符合无用条目,expungeStaleEntry才是真正清除无用的条目的方法
*/
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];
//对象不为null且引用为空
if (e != null && e.get() == null) {
n = len;
removed = true;
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0);
return removed;
}
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
Entry e;
int i;
//遍历当前到len,直到遇到对象不为null,则停止遍历,然后把中间的全部回收
for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
//如果引用key为空,则把对象设置为null
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
//然后重新hash
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
3.2 ThreadLocal工作流程图
3.2.1 set(T value)流程图
3.2.2 get()流程图
3:使用注意事项
因为ThreadLocalMap的key是弱引用,只要JVM垃圾回收机制运行,不管空间是否充足,弱引用就会回收。但是value生命周期是和线程保持一致的,属于强引用。只有线程被回收后,这个value才会被回收。
但是如果key被回收,但是线程还存活,这时引用被回收,但是value还存活,这时就有内存泄漏问题。
在JVM中,栈内存是线程是私有的,存储了对象的引用,堆内存线程共享,存储了对象实例。
所以在我们使用ThradLocal时一定要注意remove()。