「这是我参与2022首次更文挑战的第14天,活动详情查看:2022首次更文挑战」
前言
- 关于作者:励志不秃头的一个CURD的Java农民工,想挑战看看自己能完成多少天的更文挑战
- 关于文章:以下内容单纯为作者了解的,如有不对,欢迎各路大神指导,下面简单聊聊ThreadLocal常用的知识点
ThreadLocal
ThreadLocal与线程同步机制不同,线程同步机制是多个线程共享同一个变量,而ThreadLocal是为每一个线程创建一个单独的变量副本,故而每个线程都可以独立地改变自己所拥有的变量副本,而不会影响其他线程所对应的副本。
四个方法
- get():返回此线程局部变量的当前线程副本中的值
- initialValue():返回此线程局部变量的当前线程的“初始值”
- remove():移除此线程局部变量当前线程的值
- set(T value):将此线程局部变量的当前线程副本中的值设置为指定值
关键
ThreadLocal内部还有一个静态内部类ThreadLocalMap,该内部类才是实现线程隔离机制的关键,get()、set()、remove()都是基于该内部类操作。ThreadLocalMap提供了一种用键值对方式存储每一个线程的变量副本的方法,key为当前ThreadLocal对象,value则是对应线程的变量副本。
ThreadLocal和同步机制的比较
- ThreadLocal和线程同步机制都是为了解决多线程中相同变量的访问冲突问题。
- 在同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量,这时候变量是多个线程共享的
- ThreadLocal会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突
- 同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响
ThreadLocal源码
- set方法源码解析
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
- 首先通过getMap(Thread t)方法获取一个和当前线程相关的ThreadLocalMap,然后将变量的值设置到这个ThreadLocalMap对象中,当然如果获取到的ThreadLocalMap对象为空,就通过createMap方法创建
- getMap与createMap
ThreadLocalMap getMap(Thread t) { //ThreadLocal类内的方法
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
- 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();
}
- get方法,先获取当前线程的thread对象,再获取thread对象的threadLocalMap对象,然后根据当前的threadLocal对象取得table数组对应下标的Entry对象(因为存储的时候就是根据当前的ThreadLoca对象的Hash值存储的)
ThreadLoalMap
在ThreadLoalMap中,也是初始化一个大小16的Entry数组,Entry对象用来保存每一个key-value键值对,只不过这里的key永远都是ThreadLocal对象,通过ThreadLocal对象的set方法,结果把ThreadLocal对象自己当做key,放进了ThreadLoalMap中。
ThreadLoalMap的Entry是继承WeakReference,和HashMap很大的区别是,Entry中没有next字段,所以就不存在链表的情况了
没有链表结构,那发生hash冲突了怎么办?
hash冲突
ThreadLoalMap中插入一个key-value的实现
private void set(ThreadLocal<?> key, Object value) {
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)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
每个ThreadLocal对象都有一个hash值threadLocalHashCode,每初始化一个ThreadLocal对象,hash值就增加一个固定的大小
在插入过程中,根据ThreadLocal对象的hash值,定位到table中的位置i,过程如下:
- 如果当前位置是空的,那么正好,就初始化一个Entry对象放在位置i上;
- 不巧,位置i已经有Entry对象了,如果这个Entry对象的key正好是即将设置的key,那么重新设置Entry中的value;
- 很不巧,位置i的Entry对象,和即将设置的key没关系,那么只能找下一个空位置;
这样的话,在get的时候,也会根据ThreadLocal对象的hash值,定位到table中的位置,然后判断该位置Entry对象中的key是否和get的key一致,如果不一致,就判断下一个位置
所以,set和get如果冲突严重的话,效率很低,因为ThreadLoalMap是Thread的一个属性,所以即使在自己的代码中控制了设置的元素个数,但还是不能控制其它代码的行为
内存泄露
ThreadLocal可能导致内存泄漏
Entry的实现:
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
通过之前的分析已经知道:
- 当使用ThreadLocal保存一个value时,会在ThreadLocalMap中的数组插入一个Entry对象,按理说key-value都应该以强引用保存在Entry对象中,但在ThreadLocalMap的实现中,key被保存到了WeakReference对象中
- 导致了一个问题,ThreadLocal在没有外部强引用时,发生GC时会被回收,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。
如何避免内存泄露
在调用ThreadLocal的get()、set()可能会清除ThreadLocalMap中key为null的Entry对象,这样对应的value就没有GCRoots可达了,下次GC的时候就可以被回收,当然如果调用remove方法,肯定会删除对应的Entry对象。
如果使用ThreadLocal的set方法之后,没有显示的调用remove方法,就有可能发生内存泄露,所以养成良好的编程习惯十分重要,使用完ThreadLocal之后,记得调用remove方法。
ThreadLocal<String> localName = new ThreadLocal();
try {
localName.set("Java");
// 其它业务逻辑
} finally {
localName.remove();
}
线程池中使用ThreadLocal一定要注意清理
我们知道ThreadLocal是Thread中的本地变量,如果我们在线程的运行过程中用到了ThreadLocal,那么当线程被回收之后再次执行其他的任务的时候就会读取到之前被设置的变量,从而产生未知的问题。
正确的使用方法就是在线程每次执行完任务之后,都去调用一下ThreadLocal的remove操作。
或者在自定义ThreadPoolExecutor中,重写beforeExecute(Thread t, Runnable r)方法,在其中加入ThreadLocal的remove操作。
好了,以上就是ThreadLocal有关的知识点,希望对大家有一定的认识和帮助,我是新生代农民工L_Denny,我们下篇文章见。