最近有小伙伴问我ThreadLocal
的实现是怎样的,作为一个Java
小能手,我就大(装)致(逼)的说了下。他听后甚为满意,并给了我一个鸡腿。然而第二天,小伙伴就找到了我说你还我鸡腿!!!你说的和源码根本不一样。我一脸懵逼,然后我打开一看,果然有点小不一样,原来现在的和之前版本的ThreadLocal
实现稍微有点不一样。于是,我就高(被)高(硬)兴(拖)兴(着)来码文章了。
打开ThreadLocal
一看。ThreadLocal
的构造方法和函数方法一共就下面几个:
ThreadLocal<String> threadLocal=new ThreadLocal<>();
threadLocal.set("鸡腿费");
threadLocal.get();
threadLocal.remove();
如此简洁,仿佛我的鸡腿在向我招手。下面我们先来看构造函数:
ThreadLocal<String> threadLocal=new ThreadLocal<>();
完美!!构造函数解析完毕!!下面请看set()
方法。
86行代码获取当前线程,通过values
方法创建一个Values
对象,可以看到values()
方法其实就是获取当前线程的localValues
变量。
然后88行代码判断values
是否为空,如果为空的话,则调用initializeValues()
创建并初始化一个values
,不为空的话,则调用value.put()
方法。
initializeValues()
方法的实现也比较简单,就是直接new
一个Value
对象,然后对value
对象进行初始化。设置size
(当前线程存储的ThreadLoacl
个数,也可以理解为值的个数)、tombstones
(已经标志为无效等待移除的个数)、table
(定义一个默认为32长度的数组,用于存储ThreadLocal
的对应的值,每一个ThreadLocal
占两位长度)、mask
(用于进行与运算,等会会详细讲到)、clean
(准备清除数据的起始位置)、maxmumLoad
(用于判断table
数组是否应该扩容)。
下面我来就看最重要的一个方法value.put()
方法
开始的cleanUp()
方法我们先不管,直接看下面的代码。首先在385行定义了一个变量firstTombstone
,用于记录第一个遇到的无效位置(ThreadLocal
已经被回收了)。然后在387行执行for
循环,其索引位置是ThreadLocal
的hash&mask
(这是ThreadLocal
的精华所在,等会再讲,现在先认为每一个ThreadLocal
都有一个特定的hash
),然后每次循环数组索引加2。然后后面的就比较简单了,先取出当前位置的ThreadLocal.referenece
判断是不是同一个ThreadLocal
,是的话表示之前已经存储过值,现在是更新值,直接index+2
设置值就可以了。如果不是的话则判断当前位置的ThreadLocal.referenece
是不是为空,如果不为空的话,则判断当前位置的ThreadLocal
是否已经是失效,失效的话则用变量firstTombstone
设置失效位置,然后一直index+2
,直到下一个table
索引的ThreadLocal.referenece
为空,如果有可回收的索引,则把值设置在回收索引上。如果没有,则设置当前为空的table
索引上。
这样ThreadLocal
的set()
方法我们就知道是怎样实现的,但是这里也留下了两个问题:
-
ThreadLocal
的hash
是怎么设置的,为什么要这样设置?
-
-
16*2
的数组用完了怎么办,set()
里面的for
循环找不到空的位置或者可以回收的位置怎么退出循环。
-
现在我们就来一一解决!
可以看到ThreadLocal
的hash
是通过AtomicInteger
(CAS
,可以简单理解为锁)来实现的,然后通过hashCounter.getAndAdd(0x61c88647 * 2)
方法为每一个ThreadLocal
分配一个hash
值,这里面有一个魔数0x61c88647
,通过这个方法,可以使产生的hash
值都是2的倍数,而且出现的很均匀,这样就刚好和table
的每一个ThreadLocal
占数组两位长度符合,从而提高效率。
第一个问题解决之后,我们来看第二个问题,之前我们跳过了cleanUp()
方法。现在我们进去看看。
进去之后我们可以看到会先调用rehash()
方法判断是否需要扩容,如果不需要扩容或者长度为0,则不清除数据。否则的话,则对table
数组进行一定位置和数量的循环,判断对应的ThreadLocal
是否回收(通过弱引用weakReference
来判断),如果已经回收的话,这把table[index]
的值设置为TOMBSTONE
,并将当前循环结束位置赋值给clean
。
现在我们来看rehash()
方法
首先判断回收过期和现有的数据是否超过了阀值,如果没有超过的话,表示还有索引可用,则不扩容。否则的话,判断size
的个数,是否超过了table
所能容纳ThreadLocal
值总个数的一半,如果是,则对容量扩大一倍。然后重新初始化table
数组和相关信息,并把旧值copy
过去(ThreadLocal
回收的值除外)。
到这里,我们的set()
方法就结束了,基本套路大家也懂了,后面的也就很简单了,现在来看get()
方法。
52到65行一看就明白,和set()
的方法类似,根据索引去values
中取值。我们主要来看values.getAfterMiss(this)
方法(当取不到值的时候)。
这个方法里面也是比较容易理解的,首先根据当前ThreadLocal
的hash
值判断该位置下是否为空,如果为空的话则直接设置到table
中,并设置值为默认值(这么做的主要原因是为了提高下次的查找设置速度),然后调cleanUp()
检查是否需要扩容并标记回收的数据。如果不为空的话,则表示该ThreadLocal
对应的hash值mask的索引被占用,往后找下一个被回收或者为空的索引(和set()
方法类似),设值后调用clean()
方法。
最后我们再来看看remove()
方法
remove()
方法就很简单了,首先找到values
,然后调用values.remove()
方法。remove
方法先调用了cleanUp()
方法,然后循环查找是否有当前ThreadLocal的索引,如有则把要remove
的ThreadLocal
对应的索引值设置为TOMBSTONE
已回收,并修改tombstones
和size
的值就可以了。
到这里,我们就把整个ThreadLocal
的源码分析完毕了。相信能看到这里的小伙伴也对ThreadLocal
的实现了然于心,希望小伙伴们能有所收获。