Java-JUC-ThreadLocal 刺骨的言语ヽ痛彻心扉 2023-09-30 09:37 19阅读 0赞 # ThreadLocal # * 提供线程内的局部变量,不同的线程之间不会互相干扰,只在线程的生命周期内起作用,减少同一个线程内多个函数或组件之间一些公共变量传递的复杂度 * 特点,应用了`弱引用` > 1. 线程并发,多线程并发的场景下使用 > 2. 传递数据,通过`ThreadLocal`在同一线程下,不同组件中传递 > 3. 线程隔离,每个线程变量都是独立的,不会互相影响 ## 使用 ## * 一个线程往`ThreadLocal`放,另一个线程取不到,有隔离特点 static ThreadLocal tl = new ThreadLocal<>(); public static void main(String\[\] args) \{ new Thread(() -> \{ try \{ TimeUnit.SECONDS.sleep(2); \} catch (InterruptedException e) \{ e.printStackTrace(); \} //null System.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@4ebf4ac7 System.out.println(tl.get()); \}).start(); \} ## 各部分关系 ## * 一个`Thread`有一个`ThreadLocalMap` * 一个`ThreadLocalMap`包含多个`Entry对` * 一个`Entry对`为一个`ThreadLocal`对象和`value`构成 * 一个`ThreadLocal`可以作为多个`Thread`的`ThreadLocalMap`的`key` * 一个`Thread`只能通过自己的`ThreadLocalMap`,根据`ThreadLocal`获取对应的`value` ![format_png][] * 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` ![format_png 1][] * `doBegin`会检查是否是新的连接,如果是将新连接通过`TransactionSynchronizationManager`与`ThreadLocalMap`绑定 ![format_png 2][] * 设置给`resources`,以`map`类型存储,`key`是数据源,`value`为连接,说明一个线程,对应的一个数据源,对应一个连接 ![format_png 3][] * `resources`实际上就是个`ThreadLocal`,里面的元素类型为`Map<Object, Object>` ![format_png 4][] ## 在MyBatis的应用 ## * 关于分页`PageHelper`,会根据当前数据库连接,选择合适的分页方式 PageHelper.startPage(2, 1); List accounts = accountMapper.findAll(); for (Account account : accounts) \{ System.out.println(account); * `startPage` public static Page startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) \{ Page page = new Page(pageNum, pageSize, count); page.setReasonable(reasonable); page.setPageSizeZero(pageSizeZero); //当执行过orderBy的时候 Page 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); \} ![format_png 5][] * `PageInterceptor`拦截器中,调用`intercept`,完成所有操作调用`afterAll` ![format_png 6][] * `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 ## * 主要工作 > 1. 设置值,如果没有`ThradLocalMap`就为其创建 > 2. 在实际设置的过程中,如果找到`k`相等的,就替换;如果找到`k==null`,就进行一次清理工作,并在清理同时,如果找到`k`相等的,同样替换,如果没有相等的,就放找到为`null`的地方 * 获取到当前线程,放`value`是放入当前线程对于的`map`里,`map`的`key`为当前`ThreadLocal`对象 public void set(T value) \{ //当前线程 Thread t = Thread.currentThread(); //获取map ThreadLocalMap 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的实例对象作为key t.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 : 0 e = 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 ## * 主要工作 > 1. 获取`ThreadLocal`实例对象对应的值,如果没有就返回null > 2. 如果当前`Thread`没有`ThreadLocalMap`为其创建,并将`ThreadLocal-null`加入 > 3. 搜索时,第一次尝试直接命中,如果找不到,尝试遍历搜索,同时清理`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 返回null return 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) \{ //没找到 返回null ThreadLocal<?> k = e.get(); if (k == key) return e; if (k == null) //找到为null的,直接清除 expungeStaleEntry(i); else i = nextIndex(i, len); e = tab\[i\]; \} return null; \} * `setInitialValue`,为其创建一个map private T setInitialValue() \{ //返回null T value = initialValue(); Thread t = Thread.currentThread(); //一样的,返回`ThreadLocalMap` ThreadLocalMap map = getMap(t); if (map != null) //有map设置当前key map.set(this, value); else //否则为其创建一个 createMap(t, value); //实际上就是null return value; \} * `initialValue`,实际上仅是返回`null`,可以继承`ThreadLocal`重写此方法,自定义返回初始值 * 不了解的可以看这篇博客:[https://www.cnblogs.com/pxza/][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`越来越大 > 1. 没有手动删除`Entry` > 2. 线程一直存在,`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/][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);` > 1. 一次探测下一个地址,知道有空的地址后插入,若整个空间都找不到空的地址会溢出 > 2. 如果当前长度为16,计算出来的i为14,此时`tab[14]`上有值,且`key`不相等,发生了`hash`冲突,那就`+1`找下一个,循环遍历 ## 总结 ## **写到这里也结束了,在文章最后放上一个小小的福利,以下为小编自己在学习过程中整理出的一个关于 java开发 的学习思路及方向。从事互联网开发,最主要的是要学好技术,而学习技术是一条慢长而艰苦的道路,不能靠一时激情,也不是熬几天几夜就能学好的,必须养成平时努力学习的习惯,更加需要准确的学习方向达到有效的学习效果。** **由于内容较多就只放上一个大概的大纲,需要更及详细的学习思维导图的 [点击我的Gitee获取][Gitee]。 还有 高级java全套视频教程 java进阶架构师 视频+资料+代码+面试题!** **全方面的java进阶实践技术资料,并且还有技术大牛一起讨论交流解决问题。** [format_png]: https://img-blog.csdnimg.cn/img_convert/1d2e37d0ed16bbfdd80078b914fcb9c2.webp?x-oss-process=image/format,png [format_png 1]: https://img-blog.csdnimg.cn/img_convert/47a6d3150ac190af310540e2745febe6.webp?x-oss-process=image/format,png [format_png 2]: https://img-blog.csdnimg.cn/img_convert/e3d4b3e21aeb7ab1fde46e337d525bba.webp?x-oss-process=image/format,png [format_png 3]: https://img-blog.csdnimg.cn/img_convert/75495b059256a86d2ba9913a177c8177.webp?x-oss-process=image/format,png [format_png 4]: https://img-blog.csdnimg.cn/img_convert/6c3f7ef891ae4e10af23e0a07b78a52f.webp?x-oss-process=image/format,png [format_png 5]: https://img-blog.csdnimg.cn/img_convert/6729a6615d5352978f8192c898623671.webp?x-oss-process=image/format,png [format_png 6]: https://img-blog.csdnimg.cn/img_convert/6ff3022f6ca7d1bdd9dcc4c7f9059bab.webp?x-oss-process=image/format,png [https_www.cnblogs.com_pxza]: https://www.cnblogs.com/pxza/ [Gitee]: https://gitee.com/java-p7/vip1024p/blob/master/README.md
还没有评论,来说两句吧...