在学习ThreadLocal源码的时候,发现这两篇文章分析的非常透彻,故转载记录。
转载出处(如有不当,请及时告知,立即删除):
【huachao1001的专栏:http://blog.csdn.net/huachao1001】
【huachao1001的专栏:http://blog.csdn.net/huachao1001】
文章目录
ThreadLocal数据存取
ThreadLocal
的使用相信大家都比较熟悉,但是ThreadLocal
内部是如何做到为不同线程保存不同的副本的呢?能看到这篇文章,说明你也跟我一样好奇。接下来我们一层一层解开ThreadLocal
的面纱吧~
1. 涉及到的几个重要类
=============
ThreadLocal
里面的实现,主要涉及到以下几个重要类:
- Thread:大家很熟悉的线程类,一个
Thread
类自然代表一个线程。- ThreadLocal:既然本文是要解析
ThreadLocal
类,自然就离不开这个类啦~。- ThreadLocalMap:可以看成一个
HashMap
,但是它本身具体的实现并没有实现继承HashMap
甚至跟java.util.Map
都沾不上一点关系。只是内部的实现跟HashMap
类似(通过哈希表的方式存储)。- ThreadLocalMap.Entry:把它看成是保存键值对的对象,其本质上是一个
WeakReference<ThreadLocal>
对象。
主要涉及到的类暂时只有这些,其中ThreadLocalMap
和ThreadLocalMap.Entry
的源码解析留到下一节讲,我们暂时不去牵扯它们的实现细节,我们只需在心中默默地为ThreadLocalMap
打上HashMap
的标签,把它暂时先当成HashMap
来看待,ThreadLocalMap.Entry
看成是保存<键,值>
的对象。
2. ThreadLocal数据存取
===================
2.1 set函数
我们知道,在使用ThreadLocal
时,首先创建ThreadLocal
对象,然后再调用其set(T)
、T get()
方法。我们从这些点切入,首先是构造函数如下:
public ThreadLocal() {
}
可以看到,构造函数没有任何实现。接下来我们再从set
函数切入:
/**
* Sets the current thread's copy of this thread-local variable
* to the specified value. Most subclasses will have no need to
* override this method, relying solely on the {@link #initialValue}
* method to set the values of thread-locals.
*
* @param value the value to be stored in the current thread's copy of
* this thread-local.
*/
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
代码整体流程很简单:
先拿到保存键值对的
ThreadLocalMap
对象实例map
,如果map
为空(第一次调用的时候map
值为null
),则去创建一个ThreadLocalMap
对象并赋值给map
,并把键值对保存到map
中。
当然了,虽然整体流程看起来简单,其内部实现需要我们理清!
我们看到,首先是拿到当前线程实例t
,任何将t
作为参数获取ThreadLocalMap
对象。为什么需要通过Thread
类来获取ThreadLocalMap
对象呢?Thread
类和ThreadLocalMap
有什么联系?这些需要我们去看看getMap(Thread t)
函数的实现:
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
我们看到,getMap
的实现非常简单!!!仅仅返回Thread
实例的threadLocals
属性。Thread
中的ThreadLocalMap
属性声明如下:
ThreadLocal.ThreadLocalMap threadLocals = null;
我们先理一理,ThreadLocal
的set(T)
函数中,首先是拿到当前线程Thread
对象中的ThreadLocalMap
对象实例threadLocals
,然后再将需要保存的值保存到threadLocals
里面。
换句话说,每个线程引用的ThreadLocal
副本值都是保存在当前线程Thread
对象里面的。存储结构为ThreadLocalMap
类型,ThreadLocalMap
保存的键类型为ThreadLocal
,值为副本值
。
2.2 get函数
有了set
函数中的解析,我们对get
函数就更容易理解了!先看看get
函数源码:
/**
* Returns the value in the current thread's copy of this
* thread-local variable. If the variable has no value for the
* current thread, it is first initialized to the value returned
* by an invocation of the {@link #initialValue} method.
*
* @return the current thread's value of this thread-local
*/
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null)
return (T)e.value;
}
return setInitialValue();
}
同样的道理,拿到当前线程Thread
对象实例中保存的ThreadLocalMap
对象map
,然后从map
中读取键为this
(即ThreadLocal
类实例)对应的值。
如果map
不是null
,直接从map
里面读取就好,如果map==null
,那么我们需要对当前线程Thread
对象实例中保存的ThreadLocalMap
对象new
一下。即通过setInitialValue
函数来创建,setInitialValue
函数具体实现如下:
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
代码很简单,通过createMap
来创建ThreadLocalMap
对象,前面set
函数里面创建ThreadLocalMap
也是通过createMap
来的,我们看看createMap
具体实现:
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
这下对ThreadLocal
的存取机制彻底清楚了吧!接下来一节我们以图形的形式做个总结。
3. 各个类之间关系
上图中绿色部分就是读取数据过程,橙色就是设置数据过程。
=======
ThreadLocalMap源码解析
===================
1. 源码解析
1.1 存储结构
上一节中,我们说道,ThreadLocalMap
中存储的是ThreadLocalMap.Entry
(为了书写简单,后面直接写成Entry
对象)对象。因此,在ThreadLocalMap
中管理的也就是Entry
对象。也就是说,ThreadLocalMap
里面的大部分函数都是针对Entry
的。
首先ThreadLocalMap
需要一个“容器”来存储这些Entry
对象,ThreadLocalMap
中定义了Entry
数组实例table
,用于存储Entry
。
private Entry[] table;
也就是说,ThreadLocalMap
维护一张哈希表(一个数组),表里面存储Entry
。既然是哈希表,那肯定就会涉及到加载因子,即当表里面存储的对象达到容量的多少百分比的时候需要扩容。ThreadLocalMap中定义了threshold属性,当表里存储的对象数量超过threshold就会扩容。如下所示:
/**
* The next size value at which to resize.
*/
private int threshold; // Default to 0
/**
* Set the resize threshold to maintain at worst a 2/3 load factor.
*/
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
从上面代码看出,加载因子设置为2/3。即每次容量超过设定的len的2/3时,需要扩容。
1.2 存储Entry对象
首先看看数据是如何被放入到哈希表里面:
/**
* Set the value associated with key.
*
* @param key the thread local object
* @param value the value to be 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);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
从上面代码中看出,通过key
(ThreadLocal
类型)的hashcode
来计算存储的索引位置i
。如果i位置已经存储了对象,那么就往后挪一个位置依次类推,直到找到空的位置,再将对象存放。另外,在最后还需要判断一下当前的存储的对象个数是否已经超出了阈值(threshold的值)大小,如果超出了,需要重新扩充并将所有的对象重新计算位置(rehash函数来实现)。那么我们看看rehash函数如何实现的:
private void rehash() {
expungeStaleEntries();
// Use lower threshold for doubling to avoid hysteresis
if (size >= threshold - threshold / 4)
resize();
}
看到,rehash函数里面先调用了expungeStaleEntries
函数,然后再判断当前存储对象的大小是否超出了阈值的3/4
。如果超出了,再扩容。看的有点混乱。为什么不直接扩容并重新摆放对象?为啥要搞成这么复杂?
其实,上一篇文章我们提到,ThreadLocalMap
里面存储的Entry
对象本质上是一个WeakReference<ThreadLocal>
。也就是说,ThreadLocalMap
里面存储的对象本质是一个对ThreadLocal
对象的弱引用,该ThreadLocal
随时可能会被回收!即导致ThreadLocalMap
里面对应的Value
的Key
是null
。我们需要把这样的Entry
给清除掉,不要让它们占坑。
expungeStaleEntries
函数就是做这样的清理工作,清理完后,实际存储的对象数量自然会减少,这也不难理解后面的判断的约束条件为阈值的3/4
,而不是阈值的大小。
那么如何判断哪些Entry
是需要清理的呢?其实很简单,只需把ThreadLocalMap
里面的key
值遍历一遍,为null
的直接删了即可。可是,前面我们说过,ThreadLocalMap
并没有实现java.util.Map
接口,即无法得到keySet
。其实,不难发现,如果Key
值为null
,此时调用ThreadLocalMap
的get(ThreadLocal)
相当于get(null)
,get(null)
返回的是null
,这也就很好的解决了判断问题。也就是说,无需判断,直接根据get
函数的返回值是不是null
来判定需不需要将该Entry
删除掉。注意,get
返回null
也有可能是key
的值不为null
,但是对于get
返回为null
的Entry
,也没有占坑的必要,同样需要删掉,这么一来,就一举两得了。
注意,本文中所说的get
函数是指getEntry
.
1.3 获取Entry对象getEntry
我们看看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) {
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);
}
getEntry
函数很简单,直接通过哈希码计算位置i
,然后把哈希表中对应i
位置的Entry
对象拿出来。如果对应位置的值为null
,这就存在如下几种可能:
key
对应的值确实为null
- 由于位置冲突,
key
对应的值存储的位置并不在i
位置上,即i
位置上的null
并不属于key
的值。
因此,需要一个函数再次去确认key对应的value的值,即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;
while (e != null) {
ThreadLocal k = e.get();
if (k == key)
return e;
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
2 ThreadLocalMap.Entry对象
========================
前面很多地方都在收ThreadLocalMap
里面存储的是ThreadLocalMap.Entry
对象,那么ThreadLocalMap.Entry
对象到底是如何存储键值对的?同时又是如何做到对ThreadLocal对象进行弱引用?
先看看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<ThreadLocal>
。即Entry
本质上就是WeakReference<ThreadLocal>
,换言之,Entry
就是一个弱引用,具体讲,Entry
实例就是对ThreadLocal
某个实例的弱引用。只不过,Entry
同时还保存了value
。
好啦,到现在为止,相信你对Java
中的ThreadLocalMap
在心中多多少的有个新的认识了吧!