ThreadLocal在平时开发中是用的比较多的一个类,主要用于存储线程的数据。下面我对ThreadLocal进行一下总结;
dreamcatcher-cx大佬对ThreadLocal的设计总结,写的比较深刻,很有帮助。
主要使用场景:
-
多数据源切换,记录当前线程访问的数据源
-
spring框架事务管理,用于存放事务数据;
-
springsecurity安全框架,用于存储用户登录信息;
除了以上还有很多,不限于以上。
解读源码
数据存储ThreadLockMap
-
通过Entry数组进行存储的;
-
Entry继承Reference类,是弱关联的类,当ThreadLocal的实例为空时,GC会快速收回;一般用于处理数据量较大且维持时间较短的业务。
-
Entry节点存储的是ThreadLocal对象和对象值
//是通过Entry数组进行存储的
private Entry[] table;
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
set 方法
-
存放ThreadLocal<?>的是弱关联的容器,GC会等存储满的时候处理;
-
存放的过程中,会对key判断,调整数组位置;
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);//获取存放的位置
/**
* 当i位置刚好已经存储了数据
*/
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {//向i+1<len遍历
ThreadLocal<?> k = e.get();//获取存入的ThreadLock
if (k == key) {//判断是否一致,如果一致,替换值并返回
e.value = value;
return;
}
if (k == null) {//如果i节点获取到的ThreadLocal为空的话
replaceStaleEntry(key, value, i);
return;
}
}
//没有存储数据的情况
tab[i] = new Entry(key, value);//新增信的节点
int sz = ++size;
/**
* 判断是不是要进行扩容
* Entry数组的扩容是2倍扩容的;
*/
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
// 判断i之前的非空节点的位置
int slotToExpunge = staleSlot;
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;
//从staleSlot又向后循环,找到非空节点,查看是否又相同key
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
/**
* 判断和查询的实例是否同一个,如果key相同,则和staleSlot节点进行替换位置
*/
if (k == key) {
e.value = value;
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
if (slotToExpunge == staleSlot)
slotToExpunge = i;
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);//这里进行rehash
return;
}
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
// 如果key没找到那就在staleSlot位置插入一个新节点
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
// 判断是不是要重新尽心hash
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
get方法
- 获取存储的Entry节点
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
/**
* 从i位置往后逐个遍历查找如果找到了就返回,没找到返回null;
*/
return getEntryAfterMiss(key, i, e);
}
remove方法
- 查找对应数据节点进行删除就行
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)]) {
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}
ThreadLocal类的方法
set方法数据存储
-
首次存储,新建一个ThreadLockMap,以当前ThreaLock为key;
-
如果线程的存储区域已经初始化过,则更新存储区域中的数据;
public void set(T value) {
Thread t = Thread.currentThread();// 获取当前线程
ThreadLocalMap map = getMap(t);//获取存储数据的MAP
if (map != null)//判断存储数据的map是否为空,如果为空则创建map,如果不为空,则存入数据
map.set(this, value);
else
createMap(t, value);//如果没有就创建一个,当前Thread
}
//获取当前线程的值存储区域
ThreadLocalMap getMap(Thread t) {
return t.threadLocals; //线程的成员变量
}
//给当前线程创建一个新的ThreadLocalMap,并存储以当前ThreaLocal实例为key的键值对。
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
get方法
-
如果在访问之前没有存储任何数据,则对ThreadLockMap进行初始化,绑定当前线程。并进行初始值的创建,和当前ThreadLock实例进行绑定,kv存储
-
如果获取到之前存储的值,则进行返回存入的值;
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);//获取当前线程的存储map
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);//获取当前的ThreadLock的节点,具体上面有介绍
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();//如果不存在则进行初始化
}
//进行一些初始化操作
private T setInitialValue() {
T value = initialValue();//ThreaLock提供的一个可继承的方法,进行初始化操作
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);//获取当前线程的
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
remove 方法
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)//在存储的map不为空的情况下,移除当前ThreadLock实例
m.remove(this);
}
总结
注意点:
1. ThreadLocalMap存储的key值为当前的ThreadLocal实例,所以一般要求ThreadLocal是单例的
2. 单个线程可以有多个ThreadLocal实例,存储数据。
总结分析:
1.ThreadLocal的设计思路:
-
原理:相当给当前线程的创建独有数据域,方便当前线程访问。这样避免了多线程环境中,访问当前线程锁的相关问题
-
之所以使用ThreadLocalMap进行存储,而不用HashMap,上面源码分析说了,源于Reference类的特性。
-
弱引用,当对象无用的时候,会快速被GC回收。符合线程的特点,一般一个线程时间不会很长,当伴随大量数据时能快速回收
-
当前线程结束的时候,要通过调用ThreadLocal的remove方法,及时将ThreadLocalMap中的对应实例key设置为空,方便GC快速回收
-
-
ThreadLocal操作更有优势,下面我具体分析以下
为什么用ThreadLocal类直接操作存储数据 ?
1.简化代码:
试想一下上面的源码,如果直接从Thread中存放数据,
首先,要用Thread.currentThread()获取当前线程,
然后,再通过当前线程thread获取存储的Map
再根据key获取对那个的操作
从上面的源码可以看出,这些操作细节,都已经在ThreadLocal类中进行了隐藏;
1.避免每次使用时的重复代码;
2.而使用ThreadLocal实例获取数据,相当于拿key直接获取数据,隐藏了获取细节
2.限制和统一管理存储值
1. 内部的Entry数组,进行了泛型约束,避免了直接在Thread类中Map操作key的不规范性;
2. 避免了开发人员直接操作当前线程,而使用“变量副本”进行操作