并发的学习与使用系列 第七篇

在Android的消息机制中,Handler是非常重要的一部分,而完全要理解Handler的机制,首先应该理解ThreadLocal,关于ThreadLocal,见到很多地方叫做线程本地变量,也有些地方叫做线程本地存储,其实意思差不多。可能很多人都知道ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量,这样的词容易让人产生误解或者迷惑。

首先,从最新的ThreadLocal源码看,ThreadLocal并未创建任何本地变量,也没有copy副本的存在,是直接用的Thread对象的成员变量,因此叫做“线程变量帮助类”其实更合适,它的作用就是拿到当前线程对象的Object[] value数组,然后进行存储和取值,因为这属于每个线程的内部变量数组,因此也不存在共享,所以也就没有线程安全的问题。
先看一个例子:

demo.jpg

例子可以看出不同的线程得到的值是不同的,说明ThreadLocal可以使同一个变量在不同的线程里有不同的值,为什么同一个变量在不同的线程的会表现出不同的值呢,源码说明一切:

先看set方法:

ThreadLocal.set():

1
2
3
4
5
6
7
8
public void set(T value) {
Thread currentThread = Thread.currentThread();
Values values = values(currentThread);
if (values == null) {
values = initializeValues(currentThread);
}
values.put(this, value);
}

ThreadLocal.values():

1
2
3
Values values(Thread current) {
return current.localValues;
}

每个线程里有一个Values对象,对象有个Object[]的数组table,这个Object[]数组就是用来存储属于当前线程的内容,这也是为什么叫ThreadLocal的原因,可以看出ThreadLocal的Values引用直接指向Thread的localValues值。set值时实际上是将T value直接存入每个线程的数组里。

同时为了避免内存泄露问题,ThreadLocal通过弱引用所持有。

1
private final Reference<ThreadLocal<T>> reference = new WeakReference<ThreadLocal<T>>(this);

看下put()方法的实现。

Values.put():

简化后的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void put(ThreadLocal<?> key, Object value) {
int firstTombstone = -1;
for (int index = key.hash & mask;; index = next(index)) {
Object k = table[index];
//非第一次put时
if (k == key.reference) {
// Replace existing entry.
table[index + 1] = value;
return;
}
//第一次put的时,table[]里还未存储key.reference引用
if (k == null) {
if (firstTombstone == -1) {
// Fill in null slot.
table[index] = key.reference;
table[index + 1] = value;
size++;
return;
}
}
}

结合注释很好理解,存储过程可以简单看做用单个数组来实现的简易HashMap过程,HashMap的key是当前ThreadLocal对象的hash值与当前数组长度的求模运算,存入在数组的index位置,value就是当前的存入值,这个值总是放在index+1的位置,可以理解为index和index+1这两个位置就是HashMap的Entry。在jdk1.7之前就是用HashMap来实现的,原理都是一样的。这样是Thread类更加的轻量化。

ThreadLocal.get():

1
2
3
4
5
6
7
8
9
10
11
12
13
public T get() {
// Optimized for the fast path.
Thread currentThread = Thread.currentThread();
Values values = values(currentThread);
if (values != null) {
Object[] table = values.table;
int index = hash & values.mask;
if (this.reference == table[index]) {
return (T) table[index + 1];
}
}
....
}

通过上面的分析get函数也很好理解了。先得到当前线程对象的Values对象,然后得到Values中的Object[] table数组,从数组中取出值。

ThreadLocal 实例通常建议是用 private static 字段,至于原因想不太清楚。但这不是绝对的,在Android的事件机制Looper中

1
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();

这就不是一个private变量,至于静态

1
2
3
public static @Nullable Looper myLooper() {
return sThreadLocal.get();
}

因为需要静态方法获取Looper对象,所以就必须是静态的的吧。看到一种说法是设置static 是因为ThreadLocal支持线程范围生命周期的变量,所以不属于类的属性。不知是否有些牵强。

关于内存泄露的问题

因为是Values数组持有的是ThreadLocal的若引用,所以不会存在内存泄露的问题。但确定不需要使用的时候最好调用remove()方法来释放内存。

对比总结

判断是否需要对资源进行同步的判断准则是,当前获取(get)资源是否会有其他线程进行修改(set)或者当前进行修改的资源是否会有其他线程可以获取。几种并发工具类的关键点:

  • synchronized——主要是进行同步的控制,同时保证可见性。
  • volatile——阻止指令重排序,保证可见性。
  • ThreadLocal——线程空间内的全局变量,不存在同步问题。