java并发编程ThreadLocal数据结构,隔离原理,防止内存泄露和项目实际使用

作用

ThreadLocal的作用主要是做数据隔离,填充的数据只属于当前线程,变量的数据对别的线程而言是相对隔离的

Spring采用Threadlocal的方式,来保证单个线程中的数据库操作使用的是同一个数据库连接,同时,采用这种方式可以使业务层使用事务时不需要感知并管理connection对象,通过传播级别,巧妙地管理多个事务配置之间的切换,挂起和恢复。

敖丙举的例子:

在这里插入图片描述
抽出对象做线程隔离会比传给每个方法要好许多
在这里插入图片描述

源码

ThreadLocal<String> localName = new ThreadLocal();
localName.set("张三");
String name = localName.get();
localName.remove();

set源码很简单,其实ThreadLocal就是一个map,key是该线程对象,value是set的值
在这里插入图片描述

getMap方法取的是线程类自己的threadLocals,从而做到了数据的隔离
在这里插入图片描述

在这里插入图片描述
ThreadLocalMap的entry实现有点不一样,他的Entry是继承WeakReference(弱引用)的,也没有看到HashMap中的next,所以不存在链表了。
在这里插入图片描述
结构如图

在这里插入图片描述
实现用数组不用的原因:用数组是因为,我们开发过程中可以一个线程可以有多个TreadLocal来存放不同类型的对象的,但是他们都将放到你当前线程的ThreadLocalMap里,所以肯定要数组来存。

换言之,一个线程大不了new多个TreadLocal来避免产生hash冲突
在这里插入图片描述

如果产生hash冲突的话,看一下set源码

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();
        }

通过int i = key.threadLocalHashCode & (len-1)算出hash值来定位到table中的位置i

如果当前位置是空的,就初始化一个Entry对象放在位置i上;

if (k == null) {
    replaceStaleEntry(key, value, i);
    return;
}

如果位置i不为空,如果这个Entry对象的key正好是即将设置的key,那么就刷新Entry中的value;

if (k == key) {
    e.value = value;
    return;
}

总计:就是先看hash冲突,冲突的话再判断线程,线程如果相同则替换,不相同就再寻找下一个为空的位置
在这里插入图片描述
set和get如果冲突严重的话,效率还是很低的。

get的源码

 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);
        }

 private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
            Entry[] tab = table;
            int len = tab.length;
// get的时候一样是根据ThreadLocal获取到table的i值,然后查找数据拿到后会对比key是否相等  if (e != null && e.get() == key)。
            while (e != null) {
                ThreadLocal<?> k = e.get();
              // 相等就直接返回,不相等就继续查找,找到相等位置。
                if (k == key)
                    return e;
                if (k == null)
                    expungeStaleEntry(i);
                else
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;
        }

JVM中,栈内存归属于单个线程,每个线程都会有一个栈内存,其存储的变量只能在其所属线程中可见,即栈内存可以理解成线程的私有内存,而堆内存中的对象对所有线程可见,堆内存中的对象可以被所有线程访问。

但ThreadLocal实例实际上也是被其创建的类持有(更顶端应该是被线程持有),而ThreadLocal的值其实也是被线程实例持有,它们都是位于堆上,只是通过一些技巧将可见性修改成了线程可见。

如果我想共享线程的ThreadLocal数据怎么办?

关于InheritableThreadLocal我之前也写过一篇,可以看这个:
三分钟搞懂InheritableThreadLocal以及实战父子线程间的共享数据

其实就是thread中还有另一个变量

在这里插入图片描述
全局搜一下代码只有这里调到了
在这里插入图片描述

因为父线程的inheritThreadLocals也存在,那么我就把父线程的inheritThreadLocals给当前线程的inheritThreadLocals就ok了

内存泄露问题

ThreadLocal在保存的时候会把自己当做Key存在ThreadLocalMap中,正常情况应该是key和value都应该被外界强引用才对,但是现在key被设计成WeakReference弱引用了。
在这里插入图片描述

在这里插入图片描述
解决办法
在这里插入图片描述

这里写的比较简单,详细可以看这一篇:
为何每次用完 ThreadLocal 都要调用 remove()?——内存泄漏

在这里插入图片描述
在执行 ThreadLocal 的 set、remove、rehash 等方法时,它都会扫描 key 为 null 的 Entry,如果发现某个 Entry 的 key 为 null,则代表它所对应的 value 也没有作用了,所以它就会把对应的 value 置为 null,这样,value 对象就可以被正常回收了

但是假设 ThreadLocal 已经不被使用了,那么实际上 set、remove、rehash 方法也不会被调用,与此同时,如果这个线程又一直存活、不终止的话,那么刚才的那个调用链就一直存在,也就导致了 value 的内存泄漏

项目中使用的场景

我们用到的地方像是动态数据源的配置
在这里插入图片描述

配置相应的数据源后set
在这里插入图片描述
get和remove的封装
在这里插入图片描述
试一下
在这里插入图片描述

用到的场景就是saas情况下数据源(db,mq)的切换
在这里插入图片描述

remove的使用,确保不会内存泄露(这里的clean方法其实就是直接封装remove)
在这里插入图片描述
在这里插入图片描述

参考:
拼多多面试官没想到ThreadLocal我用得这么溜,人直接傻掉

为何每次用完 ThreadLocal 都要调用 remove()?——内存泄漏

juc的复习:
java–JUC快速入门(彻底搞懂JUC)

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值