1.ThreadLocal 是什么.
ThreadLocal 是一个线程内部的数据存储类, 通过它可以在指定的线程中存储数据, 数据存储以后, 只有在指定线程中可以获取到存储的数据, 对于其他线程来说则无法获取到数据.
2.什么情况下使用 ThreadLocal
- 当某些数据是以线程为作用域并且不同线程具有不同的数据副本的时候, 就可以考虑使用 ThreadLocal, 比如对于 Handler 来说, 它需要获取当前线程的 Looper, 很显然 Looper 的作用域就是线程并且不同线程具有不同的 Looper, 这个时候通过 ThreadLocal 就可以轻松实现 Looper 在线程中的获取。
- 另一个场景是在复杂逻辑下的对象传递, 比如监听器的传递, 有时候一个线程中的任务过于繁杂, 这可能表现为函数调用栈比较深以及代码入口的多样性. 在这种情况下, 又需要监听器能够贯穿整个线程的执行过程, 这个时候就可以采用 ThreadLocal. 采用 ThreadLocal 可以让监听器作为线程内的全局对象而存在, 在线程内只要通过 get 方法就可以获取到监听器。
下面使用一个例子来简单使用以下ThreadLocal :
public class MainActivity extends AppCompatActivity {
final String TAG = "MainActivity";
private ThreadLocal<Boolean> mBooleanThreadLocal = new ThreadLocal<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mBooleanThreadLocal.set(true);
Log.d(TAG,"[ Thread#main ] mBooleanThreadLocal =" + mBooleanThreadLocal.get());
new Thread("Thread#1"){
@Override
public void run() {
mBooleanThreadLocal.set(false);
Log.d(TAG,"[ Thread#1 ] mBooleanThreadLocal =" + mBooleanThreadLocal.get());
}
}.start();
new Thread("Thread#2"){
@Override
public void run() {
// mBooleanThreadLocal.set(false);
Log.d(TAG,"[ Thread#2 ] mBooleanThreadLocal =" + mBooleanThreadLocal.get());
}
}.start();
}
}
输出结果:
D/MainActivity: [ Thread#main ] mBooleanThreadLocal =true
D/MainActivity: [ Thread#1 ] mBooleanThreadLocal =false
D/MainActivity: [ Thread#2 ] mBooleanThreadLocal =null
- 在主线程中设置 mBooleanThreadLocal 为 true, 在子线程 Thread#1 中设置 mBooleanThreadLocal 为 false. 在子线程 Thread#2 不设置. 然后在三个线程中分别通过 get 方法获取 mBooleanThreadLocal 的值, 根据上面对 ThreadLocal 的描述, 这个时候, 主线程中应该是 true,子1中应该是 false, 子2中没有设置应该是 null。
- 虽然他们在不同的线程访问的是同一个 ThreadLocal 对象, 但是获取到的值却是不同的. 不同线程访问 ThreadLocal 的 get 方法, ThreadLocal 内部会从各自线程中取出一个数组, 然后再从数组中根据当前线程 ThreadLocal 的索引去查找对应的 value 值, 显然, 不同线程中的数据是不同的, 在哪个线程中设置的值, 那么这个值就存储在哪个线程中. 这就是为什么通过 ThreadLocal 可以在不同线程中维护一套数据副本, 并且彼此互不干扰。
3. ThreadLocal 内部实现
关键代码如下:
class ThreadLocal<T>{
private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode = new AtomicInteger();
private static int nextHashCode() {
//自增
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
//步骤 1
public void set(T value) {
Thread t = Thread.currentThread();
//ThreadLocalMap 是 ThreadLocal 的一个静态内部类
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
//步骤 2
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();
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
}
步骤1:
根据当前所在线程 getMap(t) 获取 ThreadLocalMap 对象, 如果 ThreadLocalMap != null 就将需要保存的值以 <key, value> 的形式保存, key 是 ThreadLocal 实例, value 是传入的参数 value. 如果 ThreadLocalMap == null 就创建一个 ThreadLocalMap 对象, 并将这个对象赋值给当前线程的 threadLocals 属性, 并将值保存。
步骤2:
也是根据当前所在线程获取到 ThreadLocalMap 对象, 然后进行判断. 再根据当前 ThreadLocal 作为 key 进行取值并返回, 如果 ThreadLocalMap == null 那么会调用 setInitialValue() 方法. 后面会说到这个方法。
4. ThreadLocalMap 的内部实现
class ThreadLocal{
...
static class ThreadLocalMap{
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
private static final int INITIAL_CAPACITY = 16;
private Entry[] table;
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
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);
}
private void set(ThreadLocal<?> key, Object value) {
//一起贴出来代码会过长, 不方便阅读, 所以下面会单独拿出来看.
}
private Entry getEntry(ThreadLocal<?> key) {
//一起贴出来代码会过长, 不方便阅读, 所以下面会单独拿出来看.
}
}
}
- 通过 int i = key.threadLocalHashCode & (len-1)方法获得的,这里采用的斐波那契散列方法。可以参考从 ThreadLocal 的实现看散列算法
hreadLocalHashCode方法最终调用的是nextHashCode()方法。而nextHashCode()方法如下面代码所示调用的是getAndAdd,这个方法的作用是让当前线程的nextHashCode这个值与魔法值HASH_INCREMENT相加。每调用一次加一次魔法值。也就是线程中每添加一个threadlocal,AtomicInteger 类型的nextHashCode值就会增加一个HASH_INCREMENT=0x61c88647。 - Entry 以键值对存储, Key 就为 一个 ThreadLocal 对象, value 是一个 Object 对象。
ThreadLocalMap 是 ThreadLocal 中的一个静态内部类, 构造方法中定了一个初始大小为 16 的 Entry 数组实例 table, 用于存储 Entry 对象. 那么不难理解 key, value 就是被封装到了 Entry 对象里. 也就是说 ThreadLocalMap 中维护着一张哈希表, 也就是 table 数组, 并设定了一个临界值. setThreshold, 但是当哈希列存储的对象达到容量的 2/3 的时候, 就会扩容。
ThreadLocal 中调用的 get(), set() 其实就是调用 ThreadLocalMap 中的 set(), getEntry() 这两个方法。
4.1 现在开始看 ThreadLocalMap.set()
class ThreadLocal{
...
static class ThreadLocalMap{
...
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;//从INITIAL_CAPACITY =16可以看出len =16
//分析 1
int index = key.threadLocalHashCode & (len-1);
for (Entry e = tab[index]; e != null; e = tab[index = nextIndex(index, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, index);
return;
}
}
//分析 2
tab[index] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(index, sz) && sz >= threshold){
rehash();
}
}
//分析 3
private void rehash() {
expungeStaleEntries();
if (size >= threshold - threshold / 4){
resize();
}
}
}
}
分析 1:
在 4 中得知 table 是一个 Entry 数组, 根据数组的长度与当前线程 ThreadLocal 对象的哈希值, 先计算要存储的位置, 然后判断指定的位置是否有数据, 有数据就开始遍历 Entry, 在循环中获取指定位置 Entry 中的 ThreadLocal 是否与传入的 ThreadLocal 相同. 如果相同, 就替换指定位置 Entry 的 value 为传入的 value 值. 并返回。
当向table不断添加threadlocal,因为threadlocal是弱引用,所以可能被回收,因为没有达到扩容的标准,所以当计算出最后的位置为索引9后,再添加的时候就要向索引0位置添加,但是有可能索引0位置的threadlocal没有被回收,所以就出现了哈希冲突。这个时候就需要向下循环,看索引为1的位置是否有值,如果没有添加,如果有继续循环知道找到空的位置为止。这个方法叫开放地址法
分析 2:
如果 table 数组中不存在指定位置的 Entry 对象, 那么就创建一个 Entry 对象存储. 并且判断 table 数组中存储对象的个数是否超过了临界值, 如果超过了, 就调用 rehash() 方法进行扩容和重新计算所有对象的位置.
分析 3:
先是调用 expungeStaleEntries() 方法删除过期的 Entry 对象 (怎么判断过期呢, 就是 Entry 不为空, 但是 Entry.get() 获取的 key 为空.). 如果清理完后, size >= threshold - threshold / 4 成立 则扩容两倍.
5. ThreadLocal 总结
- 每个线程都持有一个 ThreadLocalMap 的引用 (代码在 Thread.java 190行), ThreadLocalMap 中又有一个 Entry 类型叫 table 数组, 而 Entry 又是以键值对的形式来存储数据, key 为 ThreadLocal 类型. 所以, 同一线程可以有多个 ThreadLocal, 但是对于同一线程不同的 ThreadLocal 来说, 它们共享的同一个 table 数组, 只是在 table 中的索引不同。
- Entry 的 key 是弱引用, 当空间不足的时候, 会清理未被引用的 Entry 对象. 所以会有过期的 Entry, 也就是 Entry 不为空。
- 对于某一 ThreadLocal 来说, 它的索引值是确定的, 在不同线程之间访问时, 访问的是不同的 table 数组的同一位置, 只不过这个不同线程之间的 table 是独立的.