一、ThreadLocal的简介和使用
ThreadLocal是一个线程内部的数据存储类。顾名思义,就是用来存储线程的私有数据用的,或者说存放线程自己的变量。举个例子,有三个线程下载文件,每个线程都要记录自己的下载进度的百分比。那我们将如何来实现这个功能呢?
可能有些朋友想了,用一个map来存储三个线程的进度值就可以了嘛,用thread的线程名作为key。实现代码如下:
private Map<String, Integer> progressMap = new HashMap<>();
private void test() {
new Thread(mDlFileRunnable, "thread-1").start();
new Thread(mDlFileRunnable, "thread-2").start();
new Thread(mDlFileRunnable, "thread-3").start();
}
private Runnable mDlFileRunnable = new Runnable() {
@Override
public void run() {
// do something download file
int progress = 30;// 模拟计算进度值
synchronized(progressMap) {
progressMap.put(Thread.currentThread().getName(), progress); // 设置下载进度
}
Log.d(TAG, Thread.currentThread().getName()
+ " progress : " + progressMap.get(Thread.currentThread().getName()));
}
};
这个想法很直观,其他人也很容易理解,但是有一些问题:
1、增加了一个全局Map变量,需要手动维护,增加了维护的成本。
2、每次记录下载进度,都需要加锁,这种操作比较耗时,影响性能。
下面我们来看一下,怎么使用ThreadLocal实现上述的功能,废话不多说,直接上代码:
ThreadLocal<Integer> mProgressThreadLocal = new ThreadLocal<>();
private void testThreadLocal() {
new Thread(mDlFileRunnableNew, "thread-1").start();
new Thread(mDlFileRunnableNew, "thread-2").start();
new Thread(mDlFileRunnableNew, "thread-3").start();
}
private Runnable mDlFileRunnableNew = new Runnable() {
@Override
public void run() {
// do something download file
int progress = 30;// 模拟计算进度值
mProgressThreadLocal.set(progress);// 设置下载进度
Log.d(TAG, Thread.currentThread().getName() + " progress : " + mProgressThreadLocal.get());
}
};
可以看到这里ThreadLocal 可以很方便的保存每个线程自己的进度,并且不需要维护一个map,也不用考虑这方面的效率问题。
二、ThreadLocal的源码分析
ThreadLocal在很多方面可以实现一些很复杂的功能,例如Android的Looper、ActivityThread、AMS等等。关于Looper的功能,我们后面会专门有文章分析。
现在我们就简单的分析一下ThreadLocal的源码,看看它的实现原理。
ThreadLocal主要使用就是set和get方法。
下面我们就以这两个方法为入口,跟踪源码来分析一下ThreadLocal的原理。
(注:JDK和Android SDK中的源码有所区别,本文以Android SDK为基准分析的,JDK的实现思路是一致的)
我们先来看ThreadLocal的set方法:
/**
* Sets the value of this variable for the current thread. If set to
* {@code null}, the value will be set to null and the underlying entry will
* still be present.
*
* @param value the new value of the variable for the caller thread.
*/
public void set(T value) {
Thread currentThread = Thread.currentThread();
Values values = values(currentThread);
if (values == null) {
values = initializeValues(currentThread);
}
values.put(this, value);
}
/**
* Gets Values instance for this thread and variable type.
*/
Values values(Thread current) {
return current.localValues;
}
/**
* Creates Values instance for this thread and variable type.
*/
Values initializeValues(Thread current) {
return current.localValues = new Values();
}
/**
* Sets entry for given ThreadLocal to given value, creating an
* entry if necessary.
*/
void put(ThreadLocal<?> key, Object value) {
cleanUp();
// Keep track of first tombstone. That's where we want to go back
// and add an entry if necessary.
int firstTombstone = -1;
for (int index = key.hash & mask;; index = next(index)) {
Object k = table[index];
if (k == key.reference) {
// Replace existing entry.
table[index + 1] = value;
return;
}
if (k == null) {
if (firstTombstone == -1) {
// Fill in null slot.
table[index] = key.reference;
table[index + 1] = value;
size++;
return;
}
// Go back and replace first tombstone.
table[firstTombstone] = key.reference;
table[firstTombstone + 1] = value;
tombstones--;
size++;
return;
}
// Remember first tombstone.
if (firstTombstone == -1 && k == TOMBSTONE) {
firstTombstone = index;
}
}
}
流程解读:
- 1、获取当前线程的Thread对象
- 2、获取当前线程Thread的数据存储区域,即ThreadLocal.Values localValues;
ThreadLocal.Values是ThreadLocal的一个内部类,功能就是存放数据的键值对。在JDK的源码中,localValues是一个Map,但是在Android SDK中,却是自己用数组实现的HashMap。为什么Google要用数组,自己实现这个hash算法,我们不得而知,可能是出于对自己技术的自信吧。(.) - 3、如果localValues 为空,则初始化之
- 4、调用ThreadLocal.Values的put方法,以ThreadLocal的reference为键,插入数据到数组类型的hash Map中去。
接下来,我们看ThreadLocal的get方法:
/**
* Returns the value of this variable for the current thread. If an entry
* doesn't yet exist for this variable on this thread, this method will
* create an entry, populating the value with the result of
* {@link #initialValue()}.
*
* @return the current value of the variable for the calling thread.
*/
@SuppressWarnings("unchecked")
public T get() {
// Optimized for the fast path.
Thread currentThread = Thread.currentThread();
Values values = values(currentThread);
if (values != null) {
Object[] table = values.table;
// 直接通过hash算法,获取到可能保存的位置
int index = hash & values.mask;
// 如果保存位置的reference和ThreadLocal的reference相同,那就是之前保存的
if (this.reference == table[index]) {
return (T) table[index + 1];
}
} else {
values = initializeValues(currentThread);
}
// 否则进行哈希冲突的处理,即通过哈希算出位置后,依次向后遍历
return (T) values.getAfterMiss(this);
}
具体的关系见下图:
ThreadLocal和Thread的关系图.png
通过源码,我们可以得出以下结论:
- 1、一个ThreadLocal对象,只能为一个Thread保存一个值,因为在此设置会覆盖
- 2、ThreadLocal保存的线程私有数据,实际上,是保存到了Thread对象自身里面,通过一个哈希map的形式保存
- 3、通过在Thread自身保存哈希Map的方式,而不是在ThreadLocal中以Manager的方式统一管理,应该主要是从两方面考虑,
其一降低了代码之间的耦合度,比如将ThreadLocal.Values 拆出来,提供查找和插入操作,逻辑上独立,降低代码耦合
其二,不同Threa各自管理自己的私有数据Map,存储和查找都很方便,因为Map都相对较小,提高了效率,同时节约了内存。