threadlocal get为空_Java基础—ThreadLocal

23e5edae9a8bbb4f505df55695649baa.png  愿你越努力越幸运  23e5edae9a8bbb4f505df55695649baa.png 9c0306244430b45680845e9004bad827.png 「歇一歇,继续奔跑」

7d0b64f3ba4e673cebc3e923f3ae2558.png

今天蓝猫讲解Java中ThreadLocal的相关知识,文章分别从常见面试点ThreadLocal是什么内部结构及原理应用情景总结阐述,有误之处望多多海涵、指导纠正。

常见面试点

  • 如何避免ThreadLocal导致内存泄露
  • ThreadLocal的实现原理与应用情景

  • ThreadLocal结合线程池使用的问题

什么是ThreadLocal

JDK源码中是这样描述的:ThreadLocal提供了线程局部变量,而线程局部变量与普通变量不同,它是每个线程都有一个变量的副本相互隔离,通过get或set方法操作线程对应的变量。

 This class provides thread-local variables.  These variables differ from their normal counterparts in that each thread that accesses one (via its {@code get} or {@code set} method) has its own, independently initialized copy of the variable.
ThreadLocal提供了一种线程安全问题的新解决方案,通过以空间换时间的方式保存变量副本到每个线程,防止线程之间对共享变量的读写冲突,相比synchronized关键字来解决共享资源的冲突更加轻便、简洁。 内部结构及原理

44ebd612421bc3474550f6c10f1f5ff7.png

通过观察类图及关系引用可以发现每个Thread里包含对ThreadLocalMap的引用,而ThreadLocalMap是ThreadLocal的内部类,在ThreadLocalMap内部里拥有Entry[]数组,正是通过Entry数组来保存每个线程与变量之间的关联。

//ThreadLocalMap内部静态类,/* Entry继承弱引用的原因是当ThreadLocal对象无强引用时,ThreadLocalMap里的Entry对应的Key就没有强引用,在JVM GC时就可以回收线程中关联的Entry的Key对象进行回收 */static class Entry extends WeakReference<ThreadLocal>> {  /** The value associated with this ThreadLocal. */  Object value;  Entry(ThreadLocal> k, Object v) {      super(k);      value = v;  }}
  • 强引用(Strong Reference):代码中引用了new Object()构建的对象都属于强引用,即使虚拟机内存不足时,宁愿抛出OutOfMemoryError异常,也不会回收强引用的对象。
  • 软引用(Soft Reference):当虚拟机内存充足时触发GC不会回收软引用对象,但当内存不足且触发GC时则会回收软引用对象及占用的内存。
  • 弱引用(Weak Reference):当GC触发时不管虚拟机内存是否充足,都会回收弱引用对象及内存。
  • 虚引用(Phantom Reference):虚引用并不决定对象的生命周期,一个对象持有虚引用与没持有差不多,主要跟踪垃圾回收时对象的记录。

注意:当线程副本引用被置为null后,ThreadLocal对象就没有强引用存在,在JVM发生GC时可对Entry的key对象(即ThreadLocal)回收,但建议在使用完threadLocal后通过remove方法把Entry数组中的内容移除,避免发生内存泄露。

64f4ef9bd343360015d7c060412f04b3.png

ThreadLocal类提供以下几个核心方法:

T get():获取线程的变量副本值

void set(T value):设置线程变量副本值

void remove():删除线程的变量副本

get()方法

  • 获取当前用的线程,并找到线程关联的threadLocalMap

  • 当threadLocalMap为空时初始化一个新的并直接返回,否则根据key查找对应的Entry并返回值

public T get() {  // 获取当前线程  Thread t = Thread.currentThread();  ThreadLocalMap map = getMap(t);  //若当前线程关联的ThreadLocal不为空则查询  if (map != null) {      //根据threadLocal查询对应的Entry      ThreadLocalMap.Entry e = map.getEntry(this);      if (e != null) {          @SuppressWarnings("unchecked")          T result = (T)e.value;          return result;      }  }  return setInitialValue();}privateT setInitialValue() {   //默认返回null值   T value = initialValue();   Thread t = Thread.currentThread();   ThreadLocalMap map = getMap(t);   //如果当前调用线程关联的ThreadLocalMap为空则创建,否则设置值进去   if (map != null)       map.set(this, value);   else       //new ThreadLocalMap(this,value)       createMap(t, value);   return value;}private Entry getEntry(ThreadLocal> key) {  //根据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;  //数组下标的Entry不为空且关联的threadlocal与查找的threadlocal不一致  while (e != null) {      ThreadLocal> k = e.get();      //entry关联的threadlocal与查找的相等则直接返回      if (k == key)          return e;      if (k == null)          //关联的threadlocal为空,则触发清理key为null的Entry并重新进行rehash旧Entry数组的元素          //threadLocalMap的hash冲突与hashMap的冲突处理方式不一致,hashMap使用的是链表地址法,          //而threadLocalMap使用的开放地址法——线性探测,即顺序查找下一位置或者遍历全表,效率较低          expungeStaleEntry(i);      else          //递增下标i的值进行下一轮的查找          i = nextIndex(i, len);      e = tab[i];  }  return null;}

小插曲:hash冲突的解决方法

  • 开放地址法:当关键字的哈希地址N出现冲突时,以冲突地址为基础生成另一个哈希地址N1,如果N1仍然冲突,再以其为基础,生成另一个新的哈希地址N2,直到找出不冲突的哈希地址并将相应元素存入

    • 线性探测再散列:冲突发生时顺序查找下一个地址,直到找出一个空的或查遍全表,如ThreadLocalMap

    • 二次探测再散列:冲突发生时在表的左右进行跳跃式探测

    • 伪随机探测再散列:当发生冲突时,通过加入随机数2、3、5、9等作为偏移量定位下一个地址

  • 再哈希法:构造多个哈希函数且当关键字哈希地址冲突时执行下一个hash函数,直至不在出现地址冲突为止

  • 链地址法:把有哈希地址冲突的关键字构建成链表结构,适用于删除、插入较多情景,如HashMap

  • 公共溢出区:这种方式的思想是通过建立基础区、溢出区,没冲突的放到基础区,冲突的放到溢出区

set(T value)方法

  • 获取当前用的线程,并找到线程关联的threadLocalMap

  • 若threadLocalMap不为空则把更新对应的值,否则创建一个新的

public void set(T value) {  //1、获取到当前调用线程  Thread t = Thread.currentThread();  //2、获取当前线程中的ThreadLocalMap并是否为空,若不为空则调用set方法,否则创建新的ThreadLocalMap  ThreadLocalMap map = getMap(t);  if (map != null)      map.set(this, value);  else      createMap(t, value);}ThreadLocalMap getMap(Thread t) {  return t.threadLocals;}void createMap(Thread t, T firstValue) {  t.threadLocals = new ThreadLocalMap(this, firstValue);}
  • remove()方法

  • 获取当前用的线程,并找到线程关联的threadLocalMap

  • 若threadLocalMap不为空则删除关联的值,否则啥也不做

//ThreadLocalpublic void remove() {  ThreadLocalMap m = getMap(Thread.currentThread());  if (m != null)      //删除当前threadLocal对象关联的Entry      m.remove(this);}//ThreadLocalMapprivate 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;      }  }}

应用情景

大名鼎鼎的Spring框架的事务管理中使用ThreadLocal来管理连接,每个线程是单独的连接,当事务失败时不能影响到其他线程的事务过程或结果,还有大家耳闻目睹的ORM框架Mybatis同样也是用ThreadLocal管理SqlSession。

//Spring TransactionSynchronizationManager类@Overrideprotected void doBegin(Object transaction, TransactionDefinition definition) {  DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;  Connection con = null;  try {      //此处省略N行代码      if (txObject.isNewConnectionHolder()) {          //绑定数据库连接到线程中          TransactionSynchronizationManager.bindResource(obtainDataSource(), txObject.getConnectionHolder());      }  }  catch (Throwable ex) {      if (txObject.isNewConnectionHolder()) {          //当发生异常时,移除线程中的连接          DataSourceUtils.releaseConnection(con, obtainDataSource());          txObject.setConnectionHolder(null, false);      }      throw new CannotCreateTransactionException("Could not open JDBC Connection for transaction", ex);  }}

通常我们是使用如下的方式操作ThreadLocal,在操作完后一定要remove掉线程关联的Entry,防止内存泄露。

private static final ThreadLocal loginUserLocal = new ThreadLocal();public static LoginUser getLoginUser() {  return loginUserLocal.get();}public static void setLoginUser(LoginUser loginUser) {  loginUserLocal.set(loginUser);}public static void clear() {  loginUserLocal.remove();}//在使用完后一定要清理防止内存泄露try{  loginUserLocal.set(loginUser);  //执行其他业务逻辑}finally{  loginUserLocal.remove();}

总结

  • ThreadLocal提供一种多线程并发安全的新解决方案,通过以空间换取时间及线程的变量副本隔离来实现安全

  • ThreadLocalMap里的Entry类使用虚引用作为key,当threadLocal对象没有强引用时,GC便会回收调threadLocal对象,但此时扔需要remove清理Entry类的value

  • 每个ThreadLocal中只能只能保存一个变量副本,若需要保存多个变量副本则需要建立多个ThreadLocal

  • 当结合线程池使用ThreadLocal时,一定要在使用完后执行remove操作,否则会导致下一个线程再次使用ThreadLocal发现遗留历史的数据在Entry里,导致业务错误等错误

  谢谢大家看我逼叨了,今天就到这里~ 下一篇:Java基础-CAS

5c4c03188dafcb7f068c96b6b051f059.png

蓝猫出品|点击阅读? ◆ 干货 | Java基础—线程池的原理与使用 作者:GoQeng,有两只猫的攻城狮。 个人公众号:蓝猫有话要说 11d26265c16273269ebd50a2234f9952.png
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值