ThreadLocal源码完全解析

最近有小伙伴问我ThreadLocal的实现是怎样的,作为一个Java小能手,我就大(装)致(逼)的说了下。他听后甚为满意,并给了我一个鸡腿。然而第二天,小伙伴就找到了我说你还我鸡腿!!!你说的和源码根本不一样。我一脸懵逼,然后我打开一看,果然有点小不一样,原来现在的和之前版本的ThreadLocal实现稍微有点不一样。于是,我就高(被)高(硬)兴(拖)兴(着)来码文章了。

打开ThreadLocal一看。ThreadLocal的构造方法和函数方法一共就下面几个:

ThreadLocal<String> threadLocal=new ThreadLocal<>();
threadLocal.set("鸡腿费");
threadLocal.get();
threadLocal.remove();

如此简洁,仿佛我的鸡腿在向我招手。下面我们先来看构造函数:

ThreadLocal<String> threadLocal=new ThreadLocal<>();
image.png

完美!!构造函数解析完毕!!下面请看set()方法。

image.png

86行代码获取当前线程,通过values方法创建一个Values对象,可以看到values()方法其实就是获取当前线程的localValues变量。

image.png

然后88行代码判断values是否为空,如果为空的话,则调用initializeValues()创建并初始化一个values,不为空的话,则调用value.put()方法。

image.png
image.png
image.png

initializeValues()方法的实现也比较简单,就是直接new一个Value对象,然后对value对象进行初始化。设置size(当前线程存储的ThreadLoacl个数,也可以理解为值的个数)、tombstones(已经标志为无效等待移除的个数)、table(定义一个默认为32长度的数组,用于存储ThreadLocal的对应的值,每一个ThreadLocal占两位长度)、mask(用于进行与运算,等会会详细讲到)、clean(准备清除数据的起始位置)、maxmumLoad(用于判断table数组是否应该扩容)。

下面我来就看最重要的一个方法value.put()方法

image.png
image.png

开始的cleanUp()方法我们先不管,直接看下面的代码。首先在385行定义了一个变量firstTombstone,用于记录第一个遇到的无效位置(ThreadLocal已经被回收了)。然后在387行执行for循环,其索引位置是ThreadLocalhash&mask(这是ThreadLocal的精华所在,等会再讲,现在先认为每一个ThreadLocal都有一个特定的hash),然后每次循环数组索引加2。然后后面的就比较简单了,先取出当前位置的ThreadLocal.referenece判断是不是同一个ThreadLocal,是的话表示之前已经存储过值,现在是更新值,直接index+2设置值就可以了。如果不是的话则判断当前位置的ThreadLocal.referenece是不是为空,如果不为空的话,则判断当前位置的ThreadLocal是否已经是失效,失效的话则用变量firstTombstone设置失效位置,然后一直index+2,直到下一个table索引的ThreadLocal.referenece为空,如果有可回收的索引,则把值设置在回收索引上。如果没有,则设置当前为空的table索引上。

这样ThreadLocalset()方法我们就知道是怎样实现的,但是这里也留下了两个问题

    1. ThreadLocalhash是怎么设置的,为什么要这样设置?
    1. 16*2的数组用完了怎么办,set()里面的for循环找不到空的位置或者可以回收的位置怎么退出循环。

现在我们就来一一解决!

image.png

可以看到ThreadLocalhash是通过AtomicInteger(CAS,可以简单理解为锁)来实现的,然后通过hashCounter.getAndAdd(0x61c88647 * 2)方法为每一个ThreadLocal分配一个hash值,这里面有一个魔数0x61c88647,通过这个方法,可以使产生的hash值都是2的倍数,而且出现的很均匀,这样就刚好和table的每一个ThreadLocal占数组两位长度符合,从而提高效率。

第一个问题解决之后,我们来看第二个问题,之前我们跳过了cleanUp()方法。现在我们进去看看。

image.png
image.png

进去之后我们可以看到会先调用rehash()方法判断是否需要扩容,如果不需要扩容或者长度为0,则不清除数据。否则的话,则对table数组进行一定位置和数量的循环,判断对应的ThreadLocal是否回收(通过弱引用weakReference来判断),如果已经回收的话,这把table[index]的值设置为TOMBSTONE,并将当前循环结束位置赋值给clean

现在我们来看rehash()方法

image.png
image.png
image.png

首先判断回收过期和现有的数据是否超过了阀值,如果没有超过的话,表示还有索引可用,则不扩容。否则的话,判断size的个数,是否超过了table所能容纳ThreadLocal值总个数的一半,如果是,则对容量扩大一倍。然后重新初始化table数组和相关信息,并把旧值copy过去ThreadLocal回收的值除外)

到这里,我们的set()方法就结束了,基本套路大家也懂了,后面的也就很简单了,现在来看get()方法。

image.png

52到65行一看就明白,和set()的方法类似,根据索引去values中取值。我们主要来看values.getAfterMiss(this)方法(当取不到值的时候)。

image.png
image.png
image.png

这个方法里面也是比较容易理解的,首先根据当前ThreadLocalhash值判断该位置下是否为空,如果为空的话则直接设置到table中,并设置值为默认值(这么做的主要原因是为了提高下次的查找设置速度),然后调cleanUp()检查是否需要扩容并标记回收的数据。如果不为空的话,则表示该ThreadLocal对应的hash值mask的索引被占用,往后找下一个被回收或者为空的索引(和set()方法类似),设值后调用clean()方法。

最后我们再来看看remove()方法

image.png
image.png

remove()方法就很简单了,首先找到values,然后调用values.remove()方法。remove方法先调用了cleanUp()方法,然后循环查找是否有当前ThreadLocal的索引,如有则把要removeThreadLocal对应的索引值设置为TOMBSTONE已回收,并修改tombstonessize的值就可以了。

到这里,我们就把整个ThreadLocal的源码分析完毕了。相信能看到这里的小伙伴也对ThreadLocal的实现了然于心,希望小伙伴们能有所收获。

推荐阅读更多精彩内容

  • Android Handler机制系列文章整体内容如下: Android Handler机制1之ThreadAnd...
    隔壁老李头阅读 5,315评论 3 29
  • 一、基本数据类型 注释 单行注释:// 区域注释:/* */ 文档注释:/** */ 数值 对于byte类型而言...
    龙猫小爷阅读 3,282评论 0 16
  • "那你,那你对我说你现实的朋友有多少,那种接触十年的你有几个?" 麦芽糖的提问,让我瞬间陷入懵b状态,十年。我才1...
    陈贰九阅读 327评论 14 11
  • 经济学是我一直以为不用学习的专业,而且比较高深,认为他没有多大用处,所以在很多时候都会忽略它的存在,且一直不...
    寻梦未央阅读 1,568评论 0 4
  • 尊重他人生活,不说三道四,不指手画脚,对自己不了解的东西不随便评论,更不能下定论,这应该是一个人的基本操守,但是能...
    张发荣阅读 59评论 0 1