一,前言
ThreadLocal对很多人来说会比较陌生,因为我们在平时开发中使用的并不是很多,但是在特定的情况下使用可以帮我们舍去很多的麻烦。这篇博客就是从简单使用到源码分析来介绍一下ThreadLocal。本篇博客的源码是基于Jdk1.8
二,ThreadLocal的简单介绍
ThreadLocal是线程内部的数据存储类,我们可以使用ThreadLocal在指定的线程中存储数据,而存储后的数据也只有在这个指定的线程中才能够获取到,其它线程则无法获取。
三,ThreadLocal的简单使用
1,创建一个泛型为String的ThreadLocal
val threadLocal = ThreadLocal<String>()
2,在指定线程中存储值并取出存储的值
threadLocal.set("MainThreadValue")
Log.e(TAG, "${Thread.currentThread().name} : threadLocal = " + threadLocal.get())
Thread(Runnable {
threadLocal.set("Thread1Value")
Log.e(TAG,"${Thread.currentThread().name} : threadLocal = " + threadLocal.get())
},"Thread1").start()
Thread(Runnable {
Log.e(TAG,"${Thread.currentThread().name} : threadLocal = " + threadLocal.get())
},"Thread2").start()
3,输出指定线程中存储的值
从上面日志可以看出,虽然在不同线程中访问的是同一个ThreadLocal对象,但是在不同的线程中获取的值却是不一样的。下面就是对ThreadLocal从源码层进行分析。
四,ThreadLocal的源码分析
public class ThreadLocal<T>
ThreadLocal是一个泛型类,所以我们可以在ThreadLocal中存储任意类型的值,它的核心方法有3个:get(),set(T value),remove()。
1,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);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
/**
* Create the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @param t the current thread
* @param firstValue value for the initial entry of the map
*/
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
set()方法里的逻辑比较简单:
根据当前所在线程getMap(t)获取ThreadLocalMap对象,如果ThreadLocalMap!=null就将需要保存的值以<key,value>的形式保存,key是ThreadLocal实例,value是传入的参数value,ThreadLocalMap==null就创建ThreadLocalMap对象,将对象赋值给Thread实例的threadLocals属性,并将值保存。我们发现数据的存储与ThreadLocalMap有很大的关系,那很自然的数据的获取也与ThreadLocalMap有关。我们一起对ThreadLocalMap一探究竟。
ThreadLocalMap
static class ThreadLocalMap
/**
* 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;
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);}
/**
* Set the resize threshold to maintain at worst a 2/3 load factor.
*/
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
ThreadLocalMap是Thread中的一个静态内部类,构造方法里先是定义一个初始大小为16的Entry数组实例table,用于储存Entry对象。不难理解key,value被封装进了Entry对象里。也就是说。ThreadLocalMap维护着一张哈希表也就是table数组,并且设定了一个临界值setThreshold(),当哈希表里存储的对象达到容量的2/3的时候就需要扩容。
注意:用于存放Entry对象的table数组是每一个线程都持有的,内部使用ThreadLocal<?> key来区分数据来自哪个ThreadLocal.这也是为什么在指定的线程中可以获取到相应的值
Entry
/**
* 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;
}
}
entry是ThreadLocalMap中的静态内部类。需要注意的是entry继承自WeakReference,是一个弱引用很有可能会被系统回收掉,这个放在后面再说。
我们发现调用ThreadLocal的set(),就是调用了ThreadLocalMap的set()方法,看一下ThreadLocalMap的set();
/**
* 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;
//计算存储的位置i
int i = key.threadLocalHashCode & (len-1);
//如果存储的位置i已经存储了对象,就向后寻找下一个位置(nextIndex(i,len)),以此类推直到找到空的位置
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
//如果Entry对象的key值和需要存储的key值是同一个,就对这个value进行替换
if (k == key) {
e.value = value;
return;
}
//key为空对这个位置的Entry对象重新赋值
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
//table数组中没有这样的含有这个key值的Entry对象,创建Entry对象存储
//判断table数组中存储对象的个数是否超过了临界值(threshold),如果超出了需要扩充并重新计算所有对象的位置(rehash())
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
大致分析上面都已经标注出来了,需要注意的是Entry对象是继承是WeakReference也就是一个弱引用是会被回收的,所以对应 的key值可能是为null的。存放对象之后是需要判断数组中存储对象的个数是否超过了设定的临界值threshold的大小,如果超过了需要扩容,并且还要重新计算扩容后所有对象的位置。扩容的方法是rehash()
/**
* Re-pack and/or re-size the table. First scan the entire
* table removing stale entries. If this doesn't sufficiently
* shrink the size of the table, double the table size.
*/
private void rehash() {
expungeStaleEntries();
// Use lower threshold for doubling to avoid hysteresis
if (size >= threshold - threshold / 4)
resize();
}
rehash()中主要做了两步操作:
1,先是删除过时的Entry对象:expungeStaleEntries(),
2,如果存储对象个数大于临界值的3/4,扩容
来看一下expungeStaleEntries()方法
/**
* Expunge all stale entries in the table.
*/
private void expungeStaleEntries() {
Entry[] tab = table;
int len = tab.length;
for (int j = 0; j < len; j++) {
Entry e = tab[j];
if (e != null && e.get() == null)
expungeStaleEntry(j);
}
}
源码中的注释说的也很明白了这个方法的作用:删除数组中过时的Entry对象。有些小伙伴可能会有些疑问什么是过时的Entry?为什么会过时?其实这个在前面说过,Entry是弱引用会被回收。这个方法中判断的删除条件是,Entry对象不为空并且key值为空。可见expungStaleEntry(j) 方法就是删除指定索引的Entry对象。
再来看一下resize();方法
/**
* Double the capacity of the table.
*/
private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
int newLen = oldLen * 2;
Entry[] newTab = new Entry[newLen];
int count = 0;
for (int j = 0; j < oldLen; ++j) {
Entry e = oldTab[j];
if (e != null) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null; // Help the GC
} else {
int h = k.threadLocalHashCode & (newLen - 1);
while (newTab[h] != null)
h = nextIndex(h, newLen);
newTab[h] = e;
count++;
}
}
}
setThreshold(newLen);
size = count;
table = newTab;
}
看到注释就知道了新的Entry[] 数组扩容至原来的两倍。具体来分析一下:
先是创建一个是原来容量两倍的Entry[]数组,在遍历原来的数组,将key值为空的Entry对象的value置为空方便GC回收,key不为空的Entry对象先根据key的hashcode计算需要存放的位置存入新的数组中,存储结束后别忘了更新临界值。
set();方法分析完了我们再来看一下get();
2,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) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
/**
* Variant of set() to establish initialValue. Used instead
* of set() in case user has overridden the set() method.
*
* @return the initial value
*/
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;
}
protected T initialValue() {
return null;
}
调用getMap(t);方法获取当前线程的ThreadLocalMap对象,ThreadLocalMap != null的时候调用ThreadLocalMap的getEntry();方法获取ThreadLocalMap.Entry对象,再获取Entry对象中的value值并返回。如果ThreadLocalMap==null的时候(可能未调用过set();方法进行初始化),调用setInitialValue();初始化value(null),保存,并返回。
3,remove()
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
调用了ThreadLocalMap的remove();方法
/**
* Remove the entry for key.
*/
private void remove(ThreadLocal<?> key) {
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)]) {
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}
最终调用了Entry的clear();方法,也就是Reference的clear();方法,删除数组中根据key的hashcode计算出来的索引i位置上的Entry对象。
总结:从ThreadLocal的set();,get()和remove();方法可以看出,它们所操作的对象都是当前线程的ThreadLocalMap对象的table数组,因此在不同线程中访问同一个ThreadLocal的set();,get();和remove();方法,它们对ThreadLocal所做的读/写操作仅限于各自线程的内部,这就是为什么ThreadLocal可以在多个线程中互不干扰地存储和修改数据。