ThreadLocal源码解读一

简介

 

ThreadLocal是什么呢?其实ThreadLocal并非是一个线程的本地实现版本,它并不是一个Thread,而是threadlocalvariable(线程局部变量)。
也许把它命名为ThreadLocalVar更加合适。线程局部变量(ThreadLocal)其实的功用非常简单,就是为每一个使用该变量的线程都提供一个
变量值的副本,是Java中一种较为特殊的线程绑定机制,是每一个线程都可以独立地改变自己的副本,而不会和其它线程的副本冲突。

为什么使用threadLocal

解决了线程上下文中的变量传递问题。

ThreadLocal的内部结构

 

先看set方法

public  void  set(T value) {
     Thread t = Thread.currentThread();
     ThreadLocalMap map = getMap(t);
     if  (map !=  null )
         map.set( this , value);
     else
  createMap(t, value);
}

 

先获取本线程 t,然后getMap获取一个ThreadLocalMap ,如果ThreadLocalMap存在就把把当前threadLocal作为key,值value作为value,放到

此ThreadLocalMap中,如果不存在此map就创建

那么ThreadLocalMap到底是什么东东呢

 

static  class  ThreadLocalMap {
 
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
  static  class  Entry  extends  WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
  Object value;
 
Entry(ThreadLocal<?> k, Object v) {
super (k);
value = v;
}
}
 
/**
* The initial capacity -- MUST be a power of two.
*/
  private  static  final  int  INITIAL_CAPACITY =  16 ;
 
/**
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/
  private  Entry[] table;
 
/**
* The number of entries in the table.
*/
  private  int  size =  0 ;
 
/**
* The next size value at which to resize.
*/
  private  int  threshold;  // Default to 0
 
 
}

 

初看跟hashmap的结构差不多

先放张图,理解下

 

 

java.lang.Thread 内有个 ThreadLocalMap :

1
ThreadLocal.ThreadLocalMap threadLocals =  null;

ThreadLocalMap 存储了所有跟该Thread绑定的threadLocal对象到该ThreadLocal对象对应value的映射。每次在读取一个ThreadLocal对象的时候

就是通过ThreadLocalMap来查看ThreadLocal对应对象对应的值是什么

 

因为每个Thread都有自己的ThreadLocalMap,所以统一ThreadLocal对象在不同的Thread里能对应不同的value

核心讲下ThreadLocalMap

 核心的ThreadLocalMap

从上文的讨论中,我们发现,不管是ThreadLocal的底层实现结构,还是ThreadLocal的API中,最核心的就是这个ThreadLocalMap

下面我们就来好好分析下这个ThreadLocalMap。

 Hash冲突处理

在详细讲解ThreadLocalMap之前,我们先要了解ThreadLocalMap的Hash冲突处理,因为这是整个ThreadLocalMap最核心的地方,理解了这个

ThreadLocalMap其他的内容也就比较好理解了。

首先我们回顾下Java中的HashMap,我们知道HashMap的实现方式是 数组 + 链表,其中数组用于Hash桶定位,链表用于解决Hash冲突。

ThreadLocalMap,本质上来讲,也是一个Map,也用到了Hash算法,那么它在实现上与HashMap有什么区别呢?这里先把结论给出来:

Hash冲突的处理方式不一样,HashMap使用 链地址法 来解决Hash冲突,而ThreadLocalMap使用 开放地址法 来解决Hash冲突。

每一个 ThreadLocal 对象都有一个 threadLocalHashCode,在将 ThreadLocal 对象及其对应 Value 放入 ThreadLocalMap 时,先根据

threadLocalHashCode 和 ThreadLocalMap 内数组大小用类似于 threadLocalHashCode % ThreadLocalMap.length() 的方法计算出来

该 threadLocalHashCode 对应的哪一个 Slot 的 index,再构造 Entry 放入该 index 指向的 ThreadLocalMap 的 Slot 中。

因为 ThreadLocalMap 内数组大小有限,类似于 threadLocalHashCode % ThreadLocalMap.length() 计算 index 的方法可能

出现两个不同的 ThreadLocal 对象带着两个不同的 threadLocalHashCode 但被 Hash 到同一个 Slot 的情况(即发生Hash冲突)

如下图 ThreadLocalA ThreadLocalB ThreadLocalC 都具有相同的 threadLocalHashCode,在插入 ThreadLocalC 时,根据其

threadLocalHashCode 先被 Hash 到 ThreadLocalA 的 Slot,发现 Slot 不为空,于是 index + 1 再判断临近的已经存了 ThreadLocalB

的 Slot 是否为空,不为空则继续 index + 1 直到找到一个空的 Slot 将 ThreadLocalC 存入。

也就是说,ThreadLocalMap发生Hash冲突时,会顺着当前Hash桶往下找(index + 1),直到找到一个为空的桶,然后将Entry放入。

这种碰撞处理方式也就导致:

每个 ThreadLocal 对象的 threadLocalHashCode 不能挨得太近,不然冲突会很多。

其计算方法如下:

1
2
private  static AtomicInteger nextHashCode =  new AtomicInteger();
private  final  int threadLocalHashCode = nextHashCode.getAndAdd( 0x61c88647)

 

0x61c88647 是个很神奇的数字,据说是来自斐波那契散列,用这个数字,产生Hash冲突的概率较低。

ThreadLocalMap 一定不能完全装满,内置的数组一定要比实际存入的 ThreadLocal 对象至少大 1。事实上 ThreadLocalMap 的 Load Factor

是 2/3,超过之后会 rehash 并扩容。不能完全装满的原因是比如还是上面那张图,要获取一个 ThreadLocal 对象对应的 Value,并且这个目标

ThreadLocal 没有存入该 ThreadLocalMap,它的 threadLocalHashCode 刚好也 Hash 到了 ThreadLocalA 对象所在的 Slot,获取的时候先判断

目标 ThreadLocal 是不是等于 ThreadLocalA,不是的话再判断是不是等于 ThreadLocalB,依次类推直到获取到一个空 Slot 从而才能知道该

ThreadLocal 没有存储在当前 ThreadLocalMap 中。如果 ThreadLocalMap 完全装满,就不能依赖这个 Slot 是否为空的判断了;

 ThreadLocalMap API

讲完了ThreadLocalMap最核心的Hash冲突处理后,我们来看看相关的API。

从上文分析ThreadLocal的API过程中,我们发现,与ThreadLocal相关的的API有:

  • ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue)
  • set(ThreadLocal<?> key, Object value)
  • getEntry(ThreadLocal<?> key)
  • remove(ThreadLocal<?> key)

下面将详细介绍这些API以及相关的源码。

ThreadLocalMap构造函数

在前面分析ThreadLocal的set方法中,我们知道,如果当前Thread对应的ThreadLocalMap为null,

则会调用createMap方法创建ThreadLocalMap:


 

void  createMap(Thread t, T firstValue) {
t.threadLocals =  new  ThreadLocalMap( this , firstValue);
}
即调用了ThreadLocalMap的构造函数,我们来看看构造函数源码:
 
/**
* Construct a new map initially containing (firstKey, firstValue).
* ThreadLocalMaps are constructed lazily, so we only create
* one when we have at least one entry to put in it.
*/
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table =  new  Entry[INITIAL_CAPACITY];
int  i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY -  1 );
table[i] =  new  Entry(firstKey, firstValue);
size =  1 ;
setThreshold(INITIAL_CAPACITY);
}

 

同样,注释里面说的很明白:创建一个新的map,并将firstKey、firstValue存入map。

我们看这里的代码,是不是跟HashMap很类似?包括Entry数组table、Hash桶定位过程中的按位与。
这里我们需要特别的关注下Entry对象。

static  class  Entry  extends  WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super (k);
value = v;
}
}

 

我们可以看到,Entry类继承了WeakReference,即弱引用。本文一开始就提到,ThreadLocalMap中的key是弱引用,其内部实现就体现在这里。
ThreadLocalMap的set方法
private  void  set(ThreadLocal<?> key, Object value) {
 
// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.
 
  Entry[] tab = table;
int  len = tab.length;
int  i = key.threadLocalHashCode & (len- 1 ); //定位hash桶的位置
 
for  (Entry e = tab[i];
e !=  null ;
e = tab[i = nextIndex(i, len)]) { //从当前桶向下遍历
ThreadLocal<?> k = e.get();
 
if  (k == key) { //如果key存在就替换掉原来的value
e.value = value;
return ;
}
 
if  (k ==  null ) { //如果当前桶位置key为null,特殊处理,替换并清除过期的Entry
replaceStaleEntry(key, value, i);
return ;
}
}
 
tab[i] =  new  Entry(key, value); //找到一个为null的桶,将传入的Entry放入当前桶
int  sz = ++size;
if  (!cleanSomeSlots(i, sz) && sz >= threshold) // 6. 没有清理出可用的桶而且容量超过阈值,重新Hash
 
 
rehash();
}

 

set方法,作用很明显,跟HashMap的put方法一样,就是将键值对放入map。当然,考虑到会有Hash冲突,所以需要特殊处理(处理方式前文已经介绍)。

ThreadLocalMap的getEntry方法

/**
* Get the entry associated with key. This method
* itself handles only the fast path: a direct hit of existing
* key. It otherwise relays to getEntryAfterMiss. This is
* designed to maximize performance for direct hits, in part
* by making this method readily inlinable.
*
* @param key the thread local object
* @return the entry associated with key, or null if no such
*/
private  Entry getEntry(ThreadLocal<?> key) {
// 1. 定位Hash桶位置
int  i = key.threadLocalHashCode & (table.length -  1 );
// 2. 获取当前桶位置的Entry
Entry e = table[i];
// 3. 如果Entry不为null且可以相同,说明Hash命中,返回对应的值即可
if  (e !=  null  && e.get() == key)
return  e;
// 4. 未命中,特殊处理
else
return  getEntryAfterMiss(key, i, e);
}

 

get方法的作用,也无需多说,跟HashMap的get方法一样,根据key去找value。同样,考虑到Hash冲突,会有未命中的情况,需要做特殊处理,即调用getEntryAfterMiss方法:

/**
* Version of getEntry method for use when key is not found in
* its direct hash slot.
*
* @param key the thread local object
* @param i the table index for key's hash code
* @param e the entry at table[i]
* @return the entry associated with key, or null if no such
*/
private  Entry getEntryAfterMiss(ThreadLocal<?> key,  int  i, Entry e) {
Entry[] tab = table;
int  len = tab.length;
// 1. 从当前位置往下找,这个原因在Hash冲突处理章节已经介绍过:
// 插入时,如果当前位置已经有元素,则往下找一个位置,看是否为null,
// 如此反复,直到找到一个为null的位置,把Entry放入该位置
// 所以,查找的时候,也是一样的逻辑,如果根据key算出来的Hash值位置上,不是当前Entry,
// 那么就顺着数组往下找
while  (e !=  null ) {
ThreadLocal<?> k = e.get();
// 2. 如果key相同,说明找到,直接返回
if  (k == key)
return  e;
// 3. 如果当前位置key为null,特殊处理:清除过期的Entry
if  (k ==  null )
expungeStaleEntry(i);
// 4. 继续找数组的下一个位置
else
i = nextIndex(i, len);
e = tab[i];
}
// 最后还是没有找到,返回null
return  null ;
}

 

 ThreadLocalMap的remove方法

/**
* Remove the entry for key.
*/
private  void  remove(ThreadLocal<?> key) {
Entry[] tab = table;
int  len = tab.length;
// 1. 定位Hash桶位置
int  i = key.threadLocalHashCode & (len- 1 );
// 2. 遍历找出key对应的Entry,
for  (Entry e = tab[i]; e !=  null ; e = tab[i = nextIndex(i, len)]) {
if  (e.get() == key) {
// 3. 找到对应的Entry后,清理该Entry的弱引用
e.clear();
// 4. 特殊处理:清除过期的Entry
expungeStaleEntry(i);
return ;
}
}
}

 

ThreadLocal的内存泄漏 且听下回分解


如下是我的微信技术公众号,大家多多关注哈,你们能看我才有动力写下去,毕竟读源码不如约会好玩大哭 


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值