线程隔离,保证多线性访问安全
每个线程拿到的值私有,相互不干扰
ThreadLocal是JDK包提供的,它提供线程本地变量,如果创建一乐ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个副本,在实际多线程操作的时候,操作的是自己本地内存中的变量,从而规避了线程安全问题
线程内的资源共享
1.多线程并发场景
2.通过ThreadLocal在同一个线程,不同组件中传递公共变量
3.每个线程变量独立,不会相互影响
使用场景
1、在进行对象跨层传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的约束。
2、线程间数据隔离
3、进行事务操作,用于存储线程事务信息。
4、数据库连接,Session会话管理。一个线程用自己的connection
基本方法
ThreadLoacl()创建对象
set设置当前线程绑定的局部变量
get得到当前线程绑定的局部变量
remove移除当前线程绑定的局部变量
有人说可以用synchronized,用这个效率低。只能一个个执行,会造成性能下降,并发下降
ThreadLocal和synchronized区别
synchronized时间换空间,ThreadLocal是空间换时间,每个线程提供变量副本,自己用自己的,每个线程相互隔离。
ThreadLocal中都会存在ThreadLoaclMap集合。集合里面存储的是key,value形式的数据,key代表的是threadlocal的一个成员对象,value对应的是当前线程里面的一个数据。
ThreadLocal是弱引用,允许被回收
ThreadLocal内部设计
这样设计好处是
- 每个Map存储的Entry数量变少,避免hash冲突
- Thread销毁时,ThreadLocalMap也会销毁,减少内存使用。
每个Thread都有个ThreadLocalMap,map的key是ThreadLocal本身,value才是真正存储的值object。即Map里面存储的是ThreadLocal对象和变量副本value
基于内部结构看源码
set方法
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
//当前线程不存在ThreadLocalMap,开始初始化map对象,将当前线程和value作为第一个key放到map中
createMap(t, value);
}
}
//初始化
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
//初始化
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
//16
table = new Entry[INITIAL_CAPACITY];
//通过线程计算下标值
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
//new Entry,放入线程thread】和value
table[i] = new Entry(firstKey, firstValue);
//size修改为1,第一次初始化大小1
size = 1;
//threshold赋值,threshold = 16 * 2 / 3;初始容量的三分之二
setThreshold(INITIAL_CAPACITY);
}
获取当前线程维护的ThreadLocalMap
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
get方法
首先获取当前线程,根据当前线程获取一个Map
如果获取的Map不为空,则在map中以ThreadLocal的引用作为key来在map中获取对应的entry,
如果entry不为空,返回value
否则map为空或者entry为空,通过initialValue函数获取初始值value,然后用ThreadLocal的引用和value作为key和value创建一个新的Map
public T get() {
Thread t = Thread.currentThread();
//获取当前t线程的map
ThreadLocalMap map = getMap(t);
//不为null
if (map != null) {
//以当前ThreadLocal为key,得到entry
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
//获取value
T result = (T)e.value;
return result;
}
}
//初始化,map不存在,表示此线程没有维护的map对象,
//map存在,但是没有当前ThreadLocal关联的entry
return 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);
}
if (this instanceof TerminatingThreadLocal) {
TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
}
return value;
}
remove方法,获取map,移除
protected T initialValue() {
return null;
}
ThreadLocalMap源码分析
map里面有Entry
static class ThreadLocalMap {
//继承弱引用
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
//key是ThreadLocal
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}
弱引用和内存泄露
弱引用,垃圾回收器一旦发现了只具有弱引用的对象,不管当前内存空间是否足够,都会回收
内存泄露,指程序已经动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存浪费,导致程序运行减慢甚至崩溃,最终内存溢出。
如果key为null(弱引用,会回收)。
突然我们ThreadLocal是null了,也就是要被垃圾回收器回收了,但是此时我们的ThreadLocalMap生命周期和Thread的一样,它不会回收,这时候就出现了一个现象。那就是ThreadLocalMap的key没了,但是value还在,这就造成了内存泄漏。
解决办法:使用完ThreadLocal后,执行remove操作,避免出现内存溢出情况。
为什么要使用弱引用
强弱都有可能有问题
只是多一层保障,彻底避免还是要使用完后remove
Hash冲突
初始化map时候
//通过线程计算下标值
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
这个特殊的hash值和斐波那契数列有关,主要目的就是为了让hash码能均匀分布在2的n次方的数组里,也就是Entry[]中,这样做可以尽量避免hash冲突
hashcode&(size-1),hash发生冲突次数减少
private void set(ThreadLocal<?> key, Object value) {
// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.
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;
}
//如果key是空,但是value不是,说明已经被回收,当前是个旧的无用的
if (k == null) {
//用新元素替换旧元素,垃圾清理,防止内存泄露
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
//cleanSomeSlots清除null元素
if (!cleanSomeSlots(i, sz) && sz >= threshold)
//超过阈值,再hash
rehash();
}
扩容
在ThreadLocalMap.set()方法的最后,如果执行完启发式清理工作后,未清理到任何数据,且当前散列数组中Entry的数量已经达到了列表的扩容阈值(len*2/3),就开始执行rehash()逻辑:
private void rehash() {
expungeStaleEntries();
通过判断size >= threshold - threshold / 4 也就是size >= threshold* 3/4 来决定是否扩容
// Use lower threshold for doubling to avoid hysteresis
if (size >= threshold - threshold / 4)
resize();
}
这里首先是会进行探测式清理工作,从table的起始位置往后清理。清理完成之后,table中可能有一些key为null的Entry数据被清理掉,所以此时通过判断size >= threshold - threshold / 4 也就是size >= threshold* 3/4 来决定是否扩容。我们还记得上面进行rehash()的阈值是size >= threshold,所以当面试官套路我们ThreadLocalMap扩容机制的时候 我们一定要说清楚这两个步骤
这里有扩容代码
扩容后的tab的大小为oldLen * 2,然后遍历老的散列表,重新计算hash位置,然后放到新的tab数组中,如果出现hash冲突则往后寻找最近的entry为null的槽位,遍历完成之后,oldTab中所有的entry数据都已经放入到新的tab中了。重新计算tab下次扩容的阈值。
private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
int newLen = oldLen * 2;
Entry[] newTab = new Entry[newLen];
int count = 0;
for (int j = 0; j < oldLen; ++j) {
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;
}
如何正确的使用ThreadLocal
1、将ThreadLocal变量定义成private static的,这样的话ThreadLocal的生命周期就更长,由于一直存在ThreadLocal的强引用,所以ThreadLocal也就不会被回收,也就能保证任何时候都能根据ThreadLocal的弱引用访问到Entry的value值,然后remove它,防止内存泄露
2、每次使用完ThreadLocal,都调用它的remove()方法,清除数据。