先简单总结一下,等看完代码分析后,可以在回来看一下上面的图。每个线程中都持有一个ThreadLocalMap
对象,ThreadLocalMap
中又保存了ThreadLocal - value
键值对。
代码分析
先看一个demo,后面会以此来分析
private ThreadLocal<String> mThreadLocal = new ThreadLocal<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
......
//主线程
mThreadLocal.set("I'm main");
Log.d("main ThreadLocal", mThreadLocal.get() + "");
//子线程
new Thread(new Runnable() {
@Override
public void run() {
Log.d("child ThreadLocal", mThreadLocal.get() + "");
mThreadLocal.set("I'm child");
Log.d("child ThreadLocal", mThreadLocal.get() + "");
mThreadLocal.set("I'm child2");
Log.d("child ThreadLocal", mThreadLocal.get() + "");
}
}, "child").start();
}
打印信息:
D/main ThreadLocal: I'm main
D/child ThreadLocal: null
D/child ThreadLocal: I'm child
D/child ThreadLocal: I'm child2
ThreadLocal
的核心方法是set
和get
,下面就跟随这俩个方法去作分析。
1、set方法
public void set(T value) {
//获取当前线程
Thread t = Thread.currentThread();
//查询当前线程是否已创建ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null)
//已创建,则更新
map.set(this, value);
else
//未创建,则新建
createMap(t, value);
}
上述代码逻辑很简单,首次map
对象是为null
(非ui线程),所以走createMap
方法。
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
此处就可以看出如上图所描述的关系:
1、
ThreadLocalMap
中包含了ThreadLocal
对象和value
值。
2、通过threadLocals
赋值,将线程和ThreadLocalMap
绑定。
static class ThreadLocalMap {
......
//Entry是一个单key-value的类,用来保存ThreadLocal对象和其对应的value值
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
......
//数组容器的初始容量,必须为2的幂次方
private static final int INITIAL_CAPACITY = 16;
//保存所有 ThreadLocal-value 的数组容器
private Entry[] table;
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
//创建数组
table = new Entry[INITIAL_CAPACITY];
//计算数组下标
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
//保存ThreadLocal对象和firstValue值,并将其保存到数组中
table[i] = new Entry(firstKey, firstValue);
size = 1;
//设置阀值,扩容需要
setThreshold(INITIAL_CAPACITY);
}
......
}
构造方法中,Entry
用来保存参数ThreadLocal
对象和其对应的value值的,table
数组保存了所有Entry
对象。
所以,ThreadLocal - firstValue
键值对最终以Entry
的形式保存到数组table
中了,其保存的位置是根据参数ThreadLocal
计算出的一个下标值。
我的几个疑问
-
每个线程通过
ThreadLocal.set
只能保存一个值,也就是一对ThreadLocal - Value
键值类Entry,为什么会使用table
数组保存?我们知道多个线程可以共用一个
ThreadLocal
对象来存取数据。当然也可以一个线程拥有多个ThreadLocal
对象了。这样线程就包含了该线程所持有的所有ThreadLocal - Value
了。跟踪调试发现确实如此。示例:
new Thread(new Runnable() { @Override public void run() { mThreadLocal.set("I'm child"); // Log.d("child ThreadLocal", mThreadLocal.get() + ""); ThreadLocal<String> thre = new ThreadLocal<>(); thre.set("I'm child2"); } }, "child").start();
-
数组下标是怎么计算的?为什么要这么计算
firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); key.threadLocalHashCode & (len-1); k.threadLocalHashCode & (len - 1); ................ private static AtomicInteger nextHashCode = new AtomicInteger(); private final int threadLocalHashCode = nextHashCode(); private static final int HASH_INCREMENT = 0x61c88647; private static int nextHashCode() { //自增, 但返回的是上一次值。类似 N++ return nextHashCode.getAndAdd(HASH_INCREMENT); }
从代码可看出,下标跟
threadLocalHashCode
和table
的长度有关:
&上len-1
保证了下标的范围为0~最大下标
&上threadLocalHashCode
的自增保证了不同ThreadLocal
对应的threadLocalHashCode,防止碰撞
该疑问可以结合下面的问题一块了解 -
数组容器的初始容量,为什么必须为2的幂次方?貌好多容器也是如此,如HashMap
主要是为了提高效率。这块可以看一下HashMap的原理分析。
如果不了解,我这块举个例子,简单说明一下,比如容量分别为16和10:- 16-1=15,15的二进制为0000 1111,这样某个值与其按位&的话,其取值范围为0~15
- 10-1=9, 9的二进制为0000 1001,这样某个值与其按位&的话,其取值为0、1、8、9
很明显,容量不是2的幂次方的可用的值比较少,这样一是很容易碰撞(即有些数据算出来的下标非常容易相同)。二是造成资源浪费,因为本来有10个位置(桶),而只是使用了0、1、8、9四个位置,其他6个位置浪费掉了。
那为什么不用求余来计算位置呢?
两者的关联: 某值(如20:0001 0100) & 0000 1111 = 100(十进制正好为4 == 20 % 16)
求余也是可以了,记得早期的HashMap中好像就是用的该方式(记得不是很清楚了),但是这样会有几个问题,一是负数处理比较麻烦。二是位算法比求余算法能快些,毕竟求余最终还是通过二进制去处理的。
2、get方法
159 public T get() {
//获取当前线程
160 Thread t = Thread.currentThread();
//获取当前线程的ThreadLocalMap对象
161 ThreadLocalMap map = getMap(t);
162 if (map != null) {
//根据ThreadLocal得到值value
163 ThreadLocalMap.Entry e = map.getEntry(this);
164 if (e != null) {
165 @SuppressWarnings("unchecked")
166 T result = (T)e.value;
167 return result;
168 }
169 }
//map为null(如线程中未执行过set方法),则去初始化
170 return setInitialValue();
171 }
......
232 ThreadLocalMap getMap(Thread t) {
233 return t.threadLocals;
234 }
分析完set
后,这块就很简单了,我们主要看一下setInitialValue
的内容。
179 private T setInitialValue() {
180 T value = initialValue();
181 Thread t = Thread.currentThread();
182 ThreadLocalMap map = getMap(t);
183 if (map != null)
184 map.set(this, value);
185 else
186 createMap(t, value);
187 return value;
188 }
......
126 protected T initialValue() {
127 return null;
128 }
setInitialValue
跟set
方法很像,只是value
值为初始化的null
,而不是传参进来的。