为什么要了解ThreadLocal
提起这个东西好多android开发人员可能会感觉比较陌生,因为在平时的工作中好像根本用不到。好像的确是这个样子,但是作为一个android开发人员却依然很有必要去了解一下它,因为它离我们
并不远。我们Android系统中由Handler、MessageQueue和Looper构成的消息循环机制有多重要就不用我多说了吧,而其中Looper和多线程就是通过
ThreadLocal来进行管理的,有兴趣的小伙伴可以查看相关源码,这里就不赘述了。
1.ThreadLocal简介
JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal为解决多线程程序的并发问题提供了一种新的思路。使用这个工具类可以很简洁地编写出优美的多线程程序,ThreadLocal并不是一个Thread,而是Thread的局部变量。ThreadLocal一般称为线程本地变量,它是一种特殊的线程绑定机制,将变量与线程绑定在一起,为每一个线程维护一个独立的变量副本。通过ThreadLocal可以将对象的可见范围限制在同一个线程内。
2.ThreadLocal的使用
上面我们简单介绍了什么是ThreadLocal,那么我们首先来看一下它的用法
public class ThreadLocalTest {
// 新建一个 ThreadLocal 对象
static ThreadLocal<Integer> value = new ThreadLocal<Integer>();
public static void startTest() {
// 新建 5 个子线程,run 方法中调用新建的 ThreadLocal 对象 value 的 get/set 方法来获取/设置对应值
for (int i = 0; i < 5; i++) {
new Thread(new Runnable() {
public void run() {
while (value.get() <= 5) {
System.out.println(Thread.currentThread().getName() + " 的 value 值:" + value.get());
// 当前线程的 value 自增一
value.set(value.get() + 1);
}
}
}, "线程 " + (i+1)).start();
}
}
public static void main(String[] args) {
ThreadLocalTest.startTest();
}
}
怎么样代码是不是很简单,可是你运行上面代码你会发现报错了,还是我们常见的空指针异常。报错发生在10行,不难分析是value.get()为null造成的
。如果你对ThreadLocal的用法比较了解的话,肯定一眼就能看出来,我在创建ThreadLocal对象的时候没有重写它的initialValue()方法。许多文章
只是在介绍用法的时候提醒一定要重写这个方法或者先调用set()进行初始化,并没有详细解释,下面我们根据源码去看看原因。
首先我们先找到ThreadLocal的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();
}
看代码我们可以知道,首先是获取到了当前进线程,然后根据当前线程获取ThreadLocalMap对象,至于这个对象是干嘛的后面在进行分析,
下面是getMap()方法
/**
* Get the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @param t the current thread
* @return the map
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
我们可以看到这个对象是直接返回的当前Thread对象的threadLocals属性,这时还没有对它进行赋值,获取到的是它的初始值null,那么get()方法中的if判断就会跳过,那么我们只需要看一下
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;
}
我们可以看到首先对调用了initialValue()方法对value进行了赋值,而我们在没重写这个方法的时候返回的是null,后面走完判断的代码直接将null返回了
所以到这我们就找到了报空指针的原因了。那么正确的使用方法如下:
public class ThreadLocalTest {
// 新建一个 ThreadLocal 对象
static ThreadLocal<Integer> value = new ThreadLocal<Integer>(){
@Override
protected Integer initialValue() {
//进行初始化 进行初始化 进行初始化
return 0;
}
};
public static void startTest() {
// 新建 5 个子线程,run 方法中调用新建的 ThreadLocal 对象 value 的 get/set 方法来获取/设置对应值
for (int i = 0; i < 5; i++) {
new Thread(new Runnable() {
public void run() {
while (value.get() <= 5) {
System.out.println(Thread.currentThread().getName() + " 的 value 值:" + value.get());
// 当前线程的 value 自增一
value.set(value.get() + 1);
}
}
}, "线程 " + (i+1)).start();
}
}
public static void main(String[] args) {
ThreadLocalTest.startTest();
}
}
下面是运行结果,我们可以看到线程之间是不受影响的
3.进一步分析
到这并没有结束,上面我们还留着一个疑问,ThreadLocalMap是干什么的呢,其实它才是关键,这个类是ThreadLocal的静态内部类,有点类似于HashMap,
通过一个内部类Entry来保存对应的键值对信息,并且键必须是一个ThreadLocal对象。由于这个类比较占篇幅,就不贴代码了,感兴趣的同学可以自己去查看源码,那么它是如何创建的呢,仔细的同学可能会发现上面的代码中createMap()这个方法,
那么我们看一下这个方法中做了什么
/**
* 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);//this就是代表的当前ThreadLocal对象
}
很明显,这个方法是为当前线程创建了一个ThreadLocalMap对象,而它的Entry对象的value其实就相当于一个副本,至此,大家应该已经明白了ThreadLocal是如何为每个线程创建变量的副本的了吧。
总结
到这里我们就把ThreadLocal的用法和原理大致走了一遍,那我们简单总结一下。 ThreadLocal在某些情况可以代替线程之间的同步机制解决线程之间的同步问题,但是也要根据具体情况来使用。因
为 ThreadLocal 采用的机制是每个线程内部都保存了相应的副本,而不同的线程的值是独立的。而比较常见的卖票问题就无法用它来完成,比较稳妥的方法还是通过java的锁机制去解决。如果在内存足够的情况下,
ThreadLocal是比较高效的,因为它毕竟就是用空间换时间的一种做法。