ThreadLocal
用来解决对某一变量访问的冲突问题。ThreadLocal维护变量时,为每个使用该变量的线程提供一个独立的变量副本。因此多线程访问变量时,不会相互影响,因此不存在多线程安全问题,也不影响程序的执行能力
由于在每个线程中都创建了副本,所以需要考虑它对资源的消耗。
线程的局部变量, 是每一个线程所单独持有的**,其他线程不能对其进行访问, **通常是类中的 private static 字段。对该字段初始值的一个拷贝,将状态与某一个线程相关联
方法使用详解
// 用来获取ThreadLocal在当前线程中保存的变量副本
public T get() { }
// set()用来设置当前线程中变量的副本
public void set(T value) { }
//remove()用来移除当前线程中变量的副本
public void remove() { }
//用来在使用时进行重写的
protected T initialValue() { }
Thread内部通过名为threadLocals的变量(ThreadLocalMap类)来维护ThreadLocal变量表
threadLocals为每个线程储存自身的ThreadLocal变量,最小单位是Entry,使用ThreadLocal变量为key,ThreadLocal要保存的副本变量作为value。一个线程中,可能存在多个ThreadLocal变量
基本使用
public class ThreadLocalTest {
public static void main(String[] args) throws InterruptedException {
final ThreadLocalTest test = new ThreadLocalTest();
//在主线程进行了set操作
test.set();
System.out.println(test.getLong()); //1
System.out.println(test.getString()); //main
Thread thread1 = new Thread() {
public void run() {
// 将ThreadLocal变量存储到该线程的threadLocals中
test.set();
System.out.println(test.getLong()); //9
System.out.println(test.getString()); //Thread-0
};
};
thread1.start();
thread1.join();
System.out.println(test.getLong()); //1
System.out.println(test.getString()); //main
}
ThreadLocal<Long> longLocal = new ThreadLocal<Long>();
ThreadLocal<String> stringLocal = new ThreadLocal<String>();
public void set() {
longLocal.set(Thread.currentThread().getId());
stringLocal.set(Thread.currentThread().getName());
}
public long getLong() {
return longLocal.get();
}
public String getString() {
return stringLocal.get();
}
}
应用场景
数据库连接
Class A implements Runnable{
private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() {
public Connection initialValue() {
return DriverManager.getConnection(DB_URL);
}
};
public static Connection getConnection() {
return connectionHolder.get();
}
}
Session管理
private static final ThreadLocal threadSession = new ThreadLocal();
public static Session getSession() throws InfrastructureException {
Session s = (Session) threadSession.get();
try {
if (s == null) {
s = getSessionFactory().openSession();
threadSession.set(s);
}
} catch (HibernateException ex) {
throw new InfrastructureException(ex);
}
return s;
}
源码分析
https://blog.csdn.net/qq32933432/article/details/80212873
https://blog.csdn.net/v123411739/article/details/78698834
ThreadLocal的定义
// hash code
private final int threadLocalHashCode = nextHashCode();
// AtomicInteger类型,从0开始
private static AtomicInteger nextHashCode =
new AtomicInteger();
// hash code每次增加1640531527
private static final int HASH_INCREMENT = 0x61c88647;
// 下一个hash code
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
//ThreadLocal的hashcode()从0开始,每新建一个ThreadLocal,对应的hashCode加0x61c88647
ThreadLocalMap的定义
//ThreadLocalMap为自定义hash映射,仅用于维护线程本地变量值
//ThreadLocal的内部类,内部维护一个Entry数组,数组的key为ThreadLocal,value为ThreadLocal
//每个线程都有一个ThreadLocalMap类型的threadLocals变量
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
set()
public void set(T value) {
Thread t = Thread.currentThread();
// 获取当前线程的ThreadLocalMap
ThreadLocalMap map = getMap(t);
// this为调用该方法的ThreadLocal对象
if (map != null)
map.set(this, value);
// 创建一个新的ThreadLocalMap, 并新建一个Entry放入该ThreadLocalMap
else
createMap(t, value);
}
//相关引用
ThreadLocalMap getMap(Thread t) {
// 返回线程t的threadLocals属性
return t.threadLocals;
}
//set()
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
// 计算出索引的位置
int i = key.threadLocalHashCode & (len-1);
// 从索引位置开始遍历,由于不是链表结构,因此通过nextIndex方法来寻找下一个索引位置
// 当遍历到的Entry为空时结束遍历
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
// 拿到Entry的key,也就是ThreadLocal
ThreadLocal<?> k = e.get();
// 如该Entry的key和传入的key相等, 则用传入的value替换掉原来的value
if (k == key) {
e.value = value;
return;
}
// 该Entry的key为空, 则代表该Entry需要被清空,
if (k == null) {
// 该方法会继续寻找传入key的安放位置, 并清理掉key为空的Entry
replaceStaleEntry(key, value, i);
return;
}
}
// 放在数组的最后一个位置
tab[i] = new Entry(key, value);
int sz = ++size;
// cleanSomeSlots()用来清理掉key为空的Entry,如返回true,则至少清理了1个元素, 则不需要扩容;如返回false则判断sz是否大于阈值
if (!cleanSomeSlots(i, sz) && sz >= threshold)
// 扩容
rehash();
}
//replaceStaleEntry()
private void replaceStaleEntry(ThreadLocal<?> key, Object value,int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
// 清除元素的开始位置(记录索引位置最前面的)
int slotToExpunge = staleSlot;
// 向前遍历,直到遇到Entry为空
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)
// 记录最靠前的一个key为null的索引位置
slotToExpunge = i;
// 向后遍历,直到遇到Entry为空
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
// 如该Entry的key和传入的key相等, 则将传入的value替换掉该Entry的value
if (k == key) {
e.value = value;
// 将i位置和staleSlot位置的元素对换
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
if (slotToExpunge == staleSlot)
slotToExpunge = i;
// 从slotToExpunge位置开始清除key为空的Entry
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
// 如果第一次遍历到key为null的元素,并且上面的向前寻找key为null的遍历没有找到,
// 则将slotToExpunge设置为当前的位置
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
// 如果key没有找到,则新建一个Entry,放在staleSlot位置
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
// 如果slotToExpunge!=staleSlot,代表除了staleSlot位置还有其他位置的元素需要清除
// 需要清除的定义:key为null的Entry,调用cleanSomeSlots方法清除key为null的Entry
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
//cleanSomeSlots()
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];
// 遍历到key为null的元素
if (e != null && e.get() == null) {
// 重置n的值
n = len;
// 标志有移除元素
removed = true;
// 移除i位置及之后的key为null的元素
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0);
return removed;
}
get()
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
//获取对应的Entry
ThreadLocalMap.Entry e = map.getEntry(this);
//找到对应的值
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//该线程的ThreadLocalMap为空则初始化一个
return setInitialValue();
}
//setInitialValue()
private T setInitialValue() {
// 默认null,需要用户自己重写该方法,
T value = initialValue();
// 当前线程
Thread t = Thread.currentThread();
// 拿到当前线程的threadLocals
ThreadLocalMap map = getMap(t);
// threadLocals不为空则将当前的ThreadLocal作为key,null作为value,插入到ThreadLocalMap
if (map != null)
map.set(this, value);
// threadLocals为空则调用创建一个ThreadLocalMap,并新建一个Entry放入该ThreadLocalMap,
// 调用set方法的ThreadLocal和value作为该Entry的key和value
else
createMap(t, value);
return value;
}
//getEntry()
private Entry getEntry(ThreadLocal<?> key) {
//根据hash code计算出索引位置
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
// 如果该Entry的key和传入的key相等, 则为目标Entry, 直接返回
if (e != null && e.get() == key)
return e;
// 否则,e不是目标Entry, 则从e之后继续寻找目标Entry
else
return getEntryAfterMiss(key, i, e);
}
//getEntryAfterMiss()
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
ThreadLocal<?> k = e.get();
// 找到目标Entry,直接返回
if (k == key)
return e;
// 调用expungeStaleEntry清除key为null的元素
if (k == null)
expungeStaleEntry(i);
else
// 下一个索引位置
i = nextIndex(i, len);
// 下一个遍历的Entry
e = tab[i];
}
return null;
}
remove()
public void remove() {
// 获取当前线程的ThreadLocalMap
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
//remove()
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
// 根据hashCode计算出当前ThreadLocal的索引位置
int i = key.threadLocalHashCode & (len-1);
// 从位置i开始遍历,直到Entry为null
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
// 如果找到key相同的
if (e.get() == key) {
// 则调用clear方法, 该方法会把key的引用清空
e.clear();
//调用expungeStaleEntry方法清除key为null的Entry
expungeStaleEntry(i);
return;
}
}
}
// 从staleSlot开始, 清除key为空的Entry, 并将不为空的元素放到合适的位置,最后返回Entry为空的位置
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// 将tab上staleSlot位置的对象清空
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// Rehash until we encounter null
Entry e;
int i;
// 遍历下一个元素, 即(i+1)%len位置的元素
for (i = nextIndex(staleSlot, len);
// 遍历到Entry为空时, 跳出循环并返回索引位置
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
// 当前遍历Entry的key为空, 则将该位置的对象清空
if (k == null) {
e.value = null;
tab[i] = null;
size--;
// 当前遍历Entry的key不为空
} else {
// 重新计算该Entry的索引位置
int h = k.threadLocalHashCode & (len - 1);
// 如果索引位置不为当前索引位置i
if (h != i) {
// 则将i位置对象清空, 替当前Entry寻找正确的位置
tab[i] = null;
// 如果h位置不为null,则向后寻找当前Entry的位置
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
rehash()
private void rehash() {
// 调用expungeStaleEntries方法清理key为空的Entry
expungeStaleEntries();
// 如果清理后size超过阈值的3/4, 则进行扩容
if (size >= threshold - threshold / 4)
resize();
}
private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
// 新表长度为旧表2倍
int newLen = oldLen * 2;
// 创建新表
Entry[] newTab = new Entry[newLen];
int count = 0;
// 遍历所有元素
for (int j = 0; j < oldLen; ++j) {
// 拿到对应位置的Entry
Entry e = oldTab[j];
if (e != null) {
ThreadLocal<?> k = e.get();
// 如果key为null,将value清空
if (k == null) {
// Help the GC
e.value = null;
} else {
// 通过hash code计算新表的索引位置
int h = k.threadLocalHashCode & (newLen - 1);
// 如果新表的该位置已经有元素,则调用nextIndex方法直到寻找到空位置
while (newTab[h] != null)
h = nextIndex(h, newLen);
// 将元素放在对应位置
newTab[h] = e;
count++;
}
}
}
// 设置新表扩容的阈值
setThreshold(newLen);
// 更新size
size = count;
// table指向新表
table = newTab;
}
问题
为什么会造成内存泄漏
ThreadLocal本身并不存储值,它只是作为一个(弱引用)key保存到ThreadLocalMap中,但entry有强引用链。因此key可能会被回收,但entry不会,这样ThreadLocalMap中存在一些key为null的键值对。而ThreadLocalMap生命周期与线程一致,如果没有手动删除此类键值对的机制,其内存不会被回收,且无法方位,最终导致内存泄露
防护措施:在ThreadLocal的get(),set(),remove()的时候都会清除线程ThreadLocalMap里所有key为null的value。但不能完全保证,如线程被放回线程池后一直闲置,或使用时没有调用上述三个方法
为什么使用弱引用,内存泄漏是否是弱引用的过?
如key使用强引用,引用的ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏
如key使用弱引用,引用的ThreadLocal的对象被回收了,由于ThreadLocalMap还持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value在下一次ThreadLocalMap调用set、get、remove的时候会被清除
因此,内存泄漏的原因是ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏
如何避免内存泄漏?
每次使用完ThreadLocal,都调用它的remove()方法,清除数据
注意:并不是所有使用ThreadLocal的地方,都在最后remove(),如他们的生命周期需要和项目的生存周期一样长