ThreadLocal
- 提供线程内的局部变量,不同的线程之间不会互相干扰,只在线程的生命周期内起作用,减少同一个线程内多个函数或组件之间一些公共变量传递的复杂度
- 特点,应用了
弱引用
- 线程并发,多线程并发的场景下使用
- 传递数据,通过
ThreadLocal
在同一线程下,不同组件中传递- 线程隔离,每个线程变量都是独立的,不会互相影响
使用
- 一个线程往
ThreadLocal
放,另一个线程取不到,有隔离特点
static ThreadLocal<Person> tl = new ThreadLocal<>();
public static void main(String[] args) {new Thread(() -> {try {TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {e.printStackTrace();}//nullSystem.out.println(tl.get());}).start();new Thread(() -> {try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}tl.set(new Person());//com.java.threadlocal.Person@4ebf4ac7System.out.println(tl.get());}).start();
}
各部分关系
- 一个
Thread
有一个ThreadLocalMap
- 一个
ThreadLocalMap
包含多个Entry对
- 一个
Entry对
为一个ThreadLocal
对象和value
构成 - 一个
ThreadLocal
可以作为多个Thread
的ThreadLocalMap
的key
- 一个
Thread
只能通过自己的ThreadLocalMap
,根据ThreadLocal
获取对应的value
- JDK8后,这种设计方式每个
ThreadLocalMap
存储的键值对少,每个Thread
维护自己的ThreadLocalMap
,一个ThreadLocalMap
的键值对数量由ThreadLocal
决定,而实际开发中,并不是很多,避免哈希冲突
JDK8之间,由
ThreadLocal
维护一个Map
,Thread-value
作为键值对,个数由线程决定
- 当
Thread
销毁之后,ThreadLocalMap
也会随之销毁,减少内存使用
和sychronized区别
- 共同点,都能用于处理多线程并发访问变量的问题
sychronized
时间换空间,只提供一份变量,让不同线程排队访问,侧重点在于多个线程访问资源的同步
ThreadLocal
空间换时间,为每个线程都提供一个线程独享的变量,实现同时访问而不互相干扰,侧重点在于每个线程之间的数据隔离
spring事务中的应用
- 保证所有操作都在一个事务中,每个操作使用的连接都必须是同一个
数据层和服务层的
connection
是同一个
- 线程并发的情况下,每个线程只能操作各自的connection
- 普通解决方案,需要将连接作为参数传入,并且要用
synchronized
保证线程安全
增加代码耦合度,影响性能
源码实例
@Transactional
最终调用DataSourceTransactionManager
,利用ThreadLocal
传递connection
doBegin
首先检查是否有连接对象,没有则获取一个,并且会设置给newConnectionHolder
doBegin
会检查是否是新的连接,如果是将新连接通过TransactionSynchronizationManager
与ThreadLocalMap
绑定
- 设置给
resources
,以map
类型存储,key
是数据源,value
为连接,说明一个线程,对应的一个数据源,对应一个连接
resources
实际上就是个ThreadLocal
,里面的元素类型为Map<Object, Object>
在MyBatis的应用
- 关于分页
PageHelper
,会根据当前数据库连接,选择合适的分页方式
PageHelper.startPage(2, 1);
List<Account> accounts = accountMapper.findAll();
for (Account account : accounts) {System.out.println(account);
startPage
public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) {Page<E> page = new Page(pageNum, pageSize, count);page.setReasonable(reasonable);page.setPageSizeZero(pageSizeZero);//当执行过orderBy的时候Page<E> oldPage = getLocalPage();if (oldPage != null && oldPage.isOrderByOnly()) {page.setOrderBy(oldPage.getOrderBy());}//关键setLocalPage(page);return page;
}
setLocalPage
,给ThreadLocalMap
设置Page
对象
protected static void setLocalPage(Page page) {LOCAL_PAGE.set(page);
}
PageInterceptor
拦截器中,调用intercept
,完成所有操作调用afterAll
afterAll
,将当前线程对应的dialect
和page
清理,remove
掉
public void afterAll() {AbstractHelperDialect delegate = this.autoDialect.getDelegate();if (delegate != null) {delegate.afterAll();this.autoDialect.clearDelegate();}clearPage();
}
getDelegate
实际上也是从ThreadLocal
获取当前线程的AbstractHelperDialect
,应用了代理模式,最终由PageHelper
来增强删除
public AbstractHelperDialect getDelegate() {return this.delegate != null ? this.delegate : (AbstractHelperDialect)this.dialectThreadLocal.get();
}
clearPage
调用remove
public static void clearPage() {LOCAL_PAGE.remove();
}
Set
- 主要工作
- 设置值,如果没有
ThradLocalMap
就为其创建- 在实际设置的过程中,如果找到
k
相等的,就替换;如果找到k==null
,就进行一次清理工作,并在清理同时,如果找到k
相等的,同样替换,如果没有相等的,就放找到为null
的地方
- 获取到当前线程,放
value
是放入当前线程对于的map
里,map
的key
为当前ThreadLocal
对象
public void set(T value) {//当前线程Thread t = Thread.currentThread();//获取mapThreadLocalMap map = getMap(t);if (map != null)//key - 当前ThreadLocal的实例对象map.set(this, value);else//没有则创建createMap(t, value);
}
getMap
对应的为Thread
的成员变量threadLocals
,每出现一个线程,就会初始化一个ThreadLocalMap类型的threadLocals
,专属于的该线程的map
ThreadLocalMap getMap(Thread t) {return t.threadLocals;
}
createMap
初始化当前线程对应的ThreadLocalMap
void createMap(Thread t, T firstValue) {//当前ThreadLocal的实例对象作为keyt.threadLocals = new ThreadLocalMap(this, firstValue);
}
ThreadLocalMap
构造
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {//table 为 Entry(继承了WeakReference)数组 INITIAL_CAPACITY 默认 16//容量必须是2的整数次幂table = new Entry[INITIAL_CAPACITY];//线性探测法,找到一个下标int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);table[i] = new Entry(firstKey, firstValue);size = 1;//设置扩容阈值,当大于它时,需要扩容setThreshold(INITIAL_CAPACITY);
}
set
,实际进行set
的操作,在当前线程对应的map
中遍历
private void set(ThreadLocal<?> key, Object value) {Entry[] tab = table;int len = tab.length;int i = key.threadLocalHashCode & (len-1);//遍历所有的Entry 线性探测法for (Entry e = tab[i];e != null;//实际上是循环遍历 i + 1 < len ? i + 1 : 0e = tab[i = nextIndex(i, len)]) {ThreadLocal<?> k = e.get();if (k == key) { //相等则替换 不相等,就找下一个,此时hash冲突了e.value = value;return;}if (k == null) {//发现一个为null的key//1.第一次遍历做一次整体的清理,并保存第一个为null的地方,防止后续突然增加大量数据//2.第二次遍历找跟当前key是否有相等的,有或没有都放到i的位置,原先的位置置null//3.清理所有`entry`指向null的下标replaceStaleEntry(key, value, i);return;}}tab[i] = new Entry(key, value);int sz = ++size;//清理为null的元素if (!cleanSomeSlots(i, sz) && sz >= threshold)rehash();
}
get
- 主要工作
- 获取
ThreadLocal
实例对象对应的值,如果没有就返回null- 如果当前
Thread
没有ThreadLocalMap
为其创建,并将ThreadLocal-null
加入- 搜索时,第一次尝试直接命中,如果找不到,尝试遍历搜索,同时清理
k == null
的Entry
get
,范型写法
public T get() {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null) {//根据`ThreadLocal`实例对象获得`Entry`ThreadLocalMap.Entry e = map.getEntry(this);if (e != null) {//抑制没被使用的警告@SuppressWarnings("unchecked")//强转T result = (T)e.value;return result;}}//当前线程没有对应的`map` 或者 没有找到当前key对应的value 返回nullreturn setInitialValue();
}
getEntry
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 //找不到就遍历return getEntryAfterMiss(key, i, e);
}
getEntryAfterMiss
,遍历搜索,在遍历的同时,清除为null
的Entry
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {Entry[] tab = table;int len = tab.length;while (e != null) { //没找到 返回nullThreadLocal<?> k = e.get();if (k == key)return e;if (k == null) //找到为null的,直接清除expungeStaleEntry(i);elsei = nextIndex(i, len);e = tab[i];}return null;
}
setInitialValue
,为其创建一个map
private T setInitialValue() {//返回nullT value = initialValue();Thread t = Thread.currentThread();//一样的,返回`ThreadLocalMap`ThreadLocalMap map = getMap(t);if (map != null)//有map设置当前keymap.set(this, value);else//否则为其创建一个createMap(t, value);//实际上就是nullreturn value;
}
initialValue
,实际上仅是返回null
,可以继承ThreadLocal
重写此方法,自定义返回初始值- 不了解的可以看这篇博客:https://www.cnblogs.com/pxza/
protected T initialValue() {return null;
}
remove
remove
,存在map
,找到key
删除
public void remove() {ThreadLocalMap m = getMap(Thread.currentThread());if (m != null)m.remove(this);
}
ThreadLocalMap
的remove
,遍历map
进行删除
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();//清理这个Entry的同时,做一次整体清理expungeStaleEntry(i);return;}}
}
Entry
Entry
继承WeakReference
,实际上是指向ThreadLocal实例对象的虚引用
static class Entry extends WeakReference<ThreadLocal<?>> {Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}
}
为什么使用弱引用
- 在
set/get
方法中,会对k
为null
进行判断并清理 - 如果使用强引用,当不需要使用当前
ThreadLocal
,将当前ThreadLocal
的实例对象置为空即可被回收,但是在ThreadLocalMap
中的Entry
仍然指向当前ThreadLocal
,无法被回收,会产生内存泄漏
,只有ThreadMap
能被回收,才能回收 - 如果是弱引用,将
ThreadLocal
的生命周期和Thread
解绑,只需要把当前外部使用的ThreadLocal
的实例对象置为空即可,内部Entry
指向的为弱引用,只要GC
就会被回收,但是Entry
中的value
仍然存在,被Entry
对象指向,无法被回收,也会产生内存泄漏
在不使用当前
Entry
时,需要tl.remove();
,调用get/set
中仍然会remove
,但是存在长时间不调用get/set
的情况
- 当线程来自于线程池,在归还线程的时候,
ThreadLocalMap
没有被清理掉,会影响下次使用,并导致空间越来越大
内存泄漏
- 真实原因跟
Entry
是否是弱引用没有关系,根源是使用完ThreadLocal
没有及时remove
,导致Map
越来越大
- 没有手动删除
Entry
- 线程一直存在,
ThreadLocalMap
生命周期跟Thread
一样
- 使用弱引用,避免
ThreadLocalMap
中仍指向ThreadLocal
无法被回收 - 使用完毕后,要及时
remove
,防止Entry
指向的value
不能被及时回收
扩容
setThreshold
,初始化为2/3
private void setThreshold(int len) {threshold = len * 2 / 3;
}
rehash
,先清理
private void rehash() {//清掉空的位置expungeStaleEntries();//如果清空后,仍然大于 3/4,扩容if (size >= threshold - threshold / 4)resize();
}
resize
真正扩容,复制一份,扩容两倍
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;
}
哈希冲突
- 由于
ThreadLocalMap
本身就需要不断的进行整体遍历remove
,可以结合开放地址法
,解决哈希冲突 - 解决
哈希冲突
的核心 - https://www.cnblogs.com/pxza/
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
- 相关部分
private final int threadLocalHashCode = nextHashCode();
//Integer的原子操作,静态方法,从0开始,所以每个实例对象的哈希值都是不同的
private static AtomicInteger nextHashCode = new AtomicInteger();
//魔数,跟斐波那契数列有关,主要为了让哈希码能够均匀的分布在2的n次方数组内,不容易堆积在一起
private static final int HASH_INCREMENT = 0x61c88647;
//
private static int nextHashCode() {return nextHashCode.getAndAdd(HASH_INCREMENT);
}
- 相当于取模运算,
hashcode % size
& (INITIAL_CAPACITY - 1)
- 线性探测法
nextIndex ((i + 1 < len) ? i + 1 : 0);
- 一次探测下一个地址,知道有空的地址后插入,若整个空间都找不到空的地址会溢出
- 如果当前长度为16,计算出来的i为14,此时
tab[14]
上有值,且key
不相等,发生了hash
冲突,那就+1
找下一个,循环遍历