ThreadLocal学习总结
1. 概念
ThreadLocal用于提供线程局部变量,在多线程环境可以保证各个线程里的变量独立于其它线程里的变量。也就是说 ThreadLocal 可以为每个线程创建一个【单独的变量副本】,相当于线程的 private static 类型变量。
ThreadLocal的作用和同步机制有些相反:同步机制是为了保证多线程环境下数据的一致性;而 ThreadLocal 是保证了多线程环境下数据的独立性。
2. 使用示例
publicclassThreadLocalTest{
privatestatic String strLabel;
privatestatic ThreadLocal<String> threadLabel =
new ThreadLocal<>();
publicstaticvoidmain(String... args) {
strLabel ="main";
threadLabel.set("main");
Thread thread =
new Thread() {
@Override
publicvoidrun() {
super.run();
strLabel ="child";
threadLabel.set("child");
}
};
thread.start();
try {
//保证线程执行完毕
thread.join();
}
catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("strLabel = "+ strLabel);
System.out.println("threadLabel = "+ threadLabel.get());
}
}
运行结果:
strLabel = child
threadLabel = main
从运行结果可以看出,对于 ThreadLocal 类型的变量,在一个线程中设置值,不影响其在其它线程中的值。也就是说 ThreadLocal 类型的变量的值在每个线程中是独立的。
3. ThreadLocal 实现
ThreadLocal是怎样保证其值在各个线程中是独立的呢?下面分析下 ThreadLocal 的实现。
ThreadLocal是构造函数只是一个简单的无参构造函数,并且没有任何实现。
3.1 set(T value) 方法
publicvoidset(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map !=
null)
map.set(
this, value);
else
createMap(t, value);
}
set(T value)方法中,首先获取当前线程,然后在获取到当前线程的ThreadLocalMap,如果ThreadLocalMap不为null,则将value保存到ThreadLocalMap中,并用当前ThreadLocal作为key;否则创建一个ThreadLocalMap并给到当前线程,然后保存value。
ThreadLocalMap相当于一个HashMap,是真正保存值的地方。
3.2 get() 方法
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();
}
同样的,在get()方法中也会获取到当前线程的ThreadLocalMap,如果ThreadLocalMap不为null,则把获取key为当前ThreadLocal的值;否则调用setInitialValue()方法返回初始值,并保存到新创建的ThreadLocalMap中。
3.3 initialValue() 方法:
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;
}
...
initialValue()是ThreadLocal的初始值,默认返回null,子类可以重写改方法,用于设置ThreadLocal的初始值。
3.4 remove() 方法
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
ThreadLocal还有一个remove()方法,用来移除当前ThreadLocal对应的值。同样也是同过当前线程的ThreadLocalMap来移除相应的值。
3.5 当前线程的 ThreadLocalMap
在set,get,initialValue和remove方法中都会获取到当前线程,然后通过当前线程获取到ThreadLocalMap,如果ThreadLocalMap为null,则会创建一个ThreadLocalMap,并给到当前线程。
...
ThreadLocalMap
getMap(Thread t) {
return t.threadLocals;
}
voidcreateMap(Thread t, T firstValue) {
t.threadLocals =
new ThreadLocalMap(
this, firstValue);
}
...
可以看到,每一个线程都会持有有一个ThreadLocalMap,用来维护线程本地的值:
publicclassThreadimplementsRunnable {
...
ThreadLocal.ThreadLocalMap threadLocals =
null;
...
}
在使用ThreadLocal类型变量进行相关操作时,都会通过当前线程获取到ThreadLocalMap来完成操作。
每个线程的ThreadLocalMap是属于线程自己的,ThreadLocalMap中维护的值也是属于线程自己的。这就保证了ThreadLocal类型的变量在每个线程中是独立的,在多线程环境下不会相互影响。
4. ThreadLocalMap
4.1 构造方法
ThreadLocal中当前线程的ThreadLocalMap为null时会使用ThreadLocalMap的构造方法新建一个ThreadLocalMap:
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);
}
构造方法中会新建一个数组,并将将第一次需要保存的键值存储到一个数组中,完成一些初始化工作。
4.2 存储结构
ThreadLocalMap内部维护了一个哈希表(数组)来存储数据,并且定义了加载因子:
//初始容量,必须是2的幂
privatestaticfinalint INITIAL_CAPACITY = 16;
//存储数据的哈希表
privateEntry[] table;
// table中已存储的条目数
privateint size = 0;
//表示一个阈值,当table中存储的对象达到该值时就会扩容
privateint threshold;
//设置threshold的值
privatevoidsetThreshold(
int len) {
threshold = len * 2 / 3;
}
table是一个Entry类型的数组,Entry是ThreadLocalMap的一个内部类。
4.3 存储对象 Entry
Entry用于保存一个键值对,其中key以弱引用的方式保存:
staticclassEntryextendsWeakReference<
ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
4.4 保存键值对
调用set(ThreadLocal key, Object value)方法将数据保存到哈希表中:
privatevoidset(ThreadLocal key, Object value) {
Entry[] tab = table;
int len = tab.length;
//计算要存储的索引位置
int i = key.threadLocalHashCode & (len-1);
//循环判断要存放的索引位置是否已经存在Entry,若存在,进入循环体
for (Entry e = tab[i];
e !=
null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal k = e.get();
//若索引位置的Entry的key和要保存的key相等,则更新该Entry的值
if (k == key) {
e.value = value;
return;
}
//若索引位置的Entry的key为null(key已经被回收了),表示该位置的Entry已经无效,用要保存的键值替换该位置上的Entry
if (k ==
null) {
replaceStaleEntry(key, value, i);
return;
}
}
//要存放的索引位置没有Entry,将当前键值作为一个Entry保存在该位置
tab[i] =
new Entry(key, value);
//增加table存储的条目数
int sz = ++size;
//清除一些无效的条目并判断table中的条目数是否已经超出阈值
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash(); //调整table的容量,并重新摆放table中的Entry
}
首先使用key(当前ThreadLocal)的threadLocalHashCode来计算要存储的索引位置i。threadLocalHashCode的值由ThreadLocal类管理,每创建一个ThreadLocal对象都会自动生成一个相应的threadLocalHashCode值,其实现如下:
// ThreadLocal对象的HashCode
privatefinalint threadLocalHashCode = nextHashCode();
//使用AtomicInteger保证多线程环境下的同步
privatestatic AtomicInteger nextHashCode =
new AtomicInteger();
//每次创建ThreadLocal对象是HashCode的增量
privatestaticfinalint HASH_INCREMENT = 0x61c88647;
//计算ThreadLocal对象的HashCode
privatestaticintnextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
在保存数据时,如果索引位置有Entry,且该Entry的key为null,那么就会执行清除无效Entry的操作,因为Entry的key使用的是弱引用的方式,key如果被回收(即key为null),这时就无法再访问到key对应的value,需要把这样的无效Entry清除掉来腾出空间。
在调整table容量时,也会先清除无效对象,然后再根据需要扩容。
privatevoidrehash() {
//先清除无效Entry
expungeStaleEntries();
//判断当前table中的条目数是否超出了阈值的3/4
if (size >= threshold - threshold / 4)
resize();
}
清除无用对象和扩容的方法这里就不再展开说明了。
4.5 获取 Entry 对象
取值是直接获取到Entry对象,使用getEntry(ThreadLocal key)方法:
privateEntry
getEntry(ThreadLocal key) {
//使用指定的key的HashCode计算索引位置
int i = key.threadLocalHashCode & (table.length - 1);
//获取当前位置的Entry
Entry e = table[i];
//如果Entry不为null且Entry的key和指定的key相等,则返回该Entry
//否则调用getEntryAfterMiss(ThreadLocal key, int i, Entry e)方法
if (e !=
null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
因为可能存在哈希冲突,key对应的Entry的存储位置可能不在通过key计算出的索引位置上,也就是说索引位置上的Entry不一定是key对应的Entry。所以需要调用getEntryAfterMiss(ThreadLocal key, int i, Entry e)方法获取。
privateEntry
getEntryAfterMiss(ThreadLocal key,
int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
//索引位置上的Entry不为null进入循环,为null则返回null
while (e !=
null) {
ThreadLocal k = e.get();
//如果Entry的key和指定的key相等,则返回该Entry
if (k == key)
return e;
//如果Entry的key为null(key已经被回收了),清除无效的Entry
//否则获取下一个位置的Entry,循环判断
if (k ==
null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
returnnull;
}
4.6 移除指定的 Entry
privatevoidremove(ThreadLocal key) {
Entry[] tab = table;
int len = tab.length;
//使用指定的key的HashCode计算索引位置
int i = key.threadLocalHashCode & (len-1);
//循环判断索引位置的Entry是否为null
for (Entry e = tab[i];
e !=
null;
e = tab[i = nextIndex(i, len)]) {
//若Entry的key和指定的key相等,执行删除操作
if (e.get() == key) {
//清除Entry的key的引用
e.clear();
//清除无效的Entry
expungeStaleEntry(i);
return;
}
}
}
4.7 内存泄漏
在ThreadLocalMap的set(),get()和remove()方法中,都有清除无效Entry的操作,这样做是为了降低内存泄漏发生的可能。
Entry中的key使用了弱引用的方式,这样做是为了降低内存泄漏发生的概率,但不能完全避免内存泄漏。
这句话的意思好象是矛盾的,下面来分析一下。
假设Entry的key没有使用弱引用的方式,而是使用了强引用:由于ThreadLocalMap的生命周期和当前线程一样长,那么当引用ThreadLocal的对象被回收后,由于ThreadLocalMap还持有ThreadLocal和对应value的强引用,ThreadLocal和对应的value是不会被回收的,这就导致了内存泄漏。所以Entry以弱引用的方式避免了ThreadLocal没有被回收而导致的内存泄漏,但是此时value仍然是无法回收的,依然会导致内存泄漏。