[多线程 ] ThreadLocal

222 阅读6分钟

1、面试必会必知:深入分析 ThreadLocal

2、ThreadLocal的使用及原理分析 (值得深入看原文)本文中多次引用,为加深印象

自己总结:

每个线程都有一个threadlocalmap,key是threadlocal,其内部是entryl[]数组 table;具体值存在entry中,
下标是threadlocal的 threadlocal.threadLocalHashCode&(INITIAL_CAPACITY-1);
每个线程可以有多个thresdlocal,根据threadlocal算出下标存在到对应位置上。

1、threadlocal是线程的内部存储类,可以在指定线程内存储数据。只有指定线程可以得到存储数据。

2、每个线程都有一个threadlocalMap的实例对象,并且通过threadlocal管理threadlocalMap。

3、threadlocalmap为每个线程thread都维护了一个数组table,而threadlocal确定了一个数组下标,而这个下标是vallue存储的对应位置。

4、对于不同线程,同一个threadlocal对应的是不同table的同一下标,即是table[i],不同线程间的table是相互独立的。

5、threadlocal是通过每个线程单独一份存储空间,牺牲空间来解决冲突。

一、threadlocal是什么

ThreadLocal虽然提供了一种解决多线程环境下成员变量的问题,但是它并不是解决多线程共享变量的问题。

该类提供了线程局部 (thread-local) 变量。这些变量不同于它们的普通对应物,
因为访问某个变量(通过其 get 或 set 方法)的每个线程都有自己的局部变量,它独立于变量的初始化副本。
ThreadLocal实例通常是类中的 private static 字段,它们希望将状态与某一个线程
(例如,用户 ID 或事务 ID)相关联

简单来说,就是ThreadLocal为共享变量在每个线程中都创建一个副本,每个线程可以访问自己内部的副本变量。这样做的好处是可以保证共享变量在多线程环境下访问的线程安全性

对于ThreadLocal需要注意的有两点:

  1. ThreadLocal实例本身是不存储值,它只是提供了一个在当前线程中找到副本值得key。

  2. 是ThreadLocal包含在Thread中,而不是Thread包含在ThreadLocal中,有些小伙伴会弄错他们的关系。

二、内部原理

从ThreadLocal的方法定义来看,还是挺简单的。就几个方法

  • get: 获取ThreadLocal中当前线程对应的线程局部变量

  • set:设置当前线程的线程局部变量的值

  • remove:将当前线程局部变量的值删除

1、set方法

  1. Thread.currentThread 获取当前执行的线程
  2. getMap(t) ,根据当前线程得到当前线程的ThreadLocalMap对象,这个对象具体是做什么的?稍后分析
  3. 如果map不为空,说明当前线程已经构造过ThreadLocalMap,直接将值存储到map中
  4. 如果map为空,说明是第一次使用,调用 createMap构造

2、ThreadLocalMap是什么

Map是一个集合,用来存储数据,那么在ThreadLocal中,就是用来存储线程的局部变量的。

ThreadLocalMapmap=getMap(t)获得一个ThreadLocalMap对象,那这个对象是干嘛的呢?

t.threadLocals实际上就是访问Thread类中的ThreadLocalMap这个成员变量

从上面的代码发现每一个线程都有自己单独的ThreadLocalMap实例,而对应这个线程的所有本地变量都会保存到这个map内

3、ThreadLocalMap是在哪里构造

在set方法中 createMap(), 通过 new ThreadLocalMap将当前线程中的 threadLocals做了初始化

ThreadLocalMap是一个静态内部类,内部定义了一个Entry对象用来真正存储数据

问题:如果同一个Thread中存储了多个值,是如何来区分存储的呢?

答案就在 firstKey.threadLocalHashCode&(INITIAL_CAPACITY-1)

关键点是 threadLocalHashCode,它相当于一个ThreadLocal的ID,实现的逻辑:

这里用到了一个非常完美的散列算法,可以简单理解为,对于同一个ThreadLocal下的多个线程来说,当任意线程调用set方法存入一个数据到Entry中的时候,其实会根据 threadLocalHashCode生成一个唯一的id标识对应这个数据,存储在Entry数据下标中。

4、set源码继续,map不为空时的执行逻辑

前面分析了set方法第一次初始化ThreadLocalMap的过程,也对ThreadLocalMap的结构有了一个全面的了解。
那么接下来看一下map不为空时的执行逻辑

主要逻辑

  • 根据key的散列哈希计算Entry的数组下标

  • 通过线性探索探测从i开始往后一直遍历到数组的最后一个Entry

  • 如果map中的key和传入的key相等,表示该数据已经存在,直接覆盖

  • 如果map中的key为空,则用新的key、value覆盖,并清理key=null的数据

  • rehash扩容

三、应用场景和问题

1)ThreadLocal的实际应用场景:

  1. 比如在线程级别,维护session,维护用户登录信息userID(登陆时插入,多个地方获取)数据库的链接对象
  2. Connection,可以通过ThreadLocal来做隔离避免线程安全问题

2)问题:内存泄漏

ThreadLocalMap中Entry的key使用的是ThreadLocal的弱引用,如果一个ThreadLocal没有外部强引用,当系统执行GC时,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现一个key为null的Entry,而这个key=null的Entry是无法访问的,当这个线程一直没有结束的话,那么就会存在一条强引用链:

Thread Ref - > Thread -> ThreadLocalMap - > Entry -> value  永远无法回收而造成内存泄漏

前提:在调用get或者set方法:过程中遇到的key为null的Entry都会被擦除,那么Entry内的value也就没有强引用链,自然会被回收。

但是不是所有场景都会满足这个场景的,所以为了避免这类的问题,我们可以在合适的位置手动调用ThreadLocal的remove函数删除不需要的ThreadLocal,防止出现内存泄漏

建议和方法:

  • 将ThreadLocal变量定义成private static的,这样的话ThreadLocal的生命周期就更长,由于一直存在ThreadLocal的强引用,所以ThreadLocal也就不会被回收,也就能保证任何时候都能根据ThreadLocal的弱引用访问到Entry的value值,然后remove它,防止内存泄露

  • 每次使用完ThreadLocal,都调用它的remove()方法,清除数据。

key为ThreadLocal对象 是弱引用,value是强引用,而如果把value改成弱引用,gc一次就直接拿不到了,而key外面有个强引用。

总结:Java里,每个线程都有自己的ThreadLocalMap,里边存着自己私有的对象。Map的Entry里,key为ThreadLocal对象,value即为私有对象T。

摘自:blog.csdn.net/vicoqi/arti…