简单使用
//我们只需要实例化一次,作为key就够了,或者使用静态。
private ThreadLocal myThreadLoca<String>l = new ThreadLocal();
// 写入
myThreadLocal.set("A thread local value");
// 读取
String threadLocalValue = myThreadLocal.get();
ThreadLocal 与Thread 同步机制的比较
同步机制 -- “以时间换空间”
在同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量,实现串行化.这时该变量是多个线程共享的,使用同步机制要求程序缜密地分析什么时候对变量进行读/写、什么时候需要锁定某个对象、什么时候释放对象锁等繁杂的问题,程序设计和编写难度相对较大。
ThreadLocal -- “以空间换时间”
给每个线程建立副本,互不干扰.
总结
对于多线程资源共享的问题.
同步机制采用“以时间换空间”的方式:访问串行化,对象共享化 ; ThreadLocaI采用了“以空间换时间”的方式:访问并行化,对象独享化。
前者仅提供一份变量,让不同的线程排队访问;而后者为每个线程都提供了一份变量,因此可以同时访问而互不影响。
ThreadLocal 源码分析
ThreadLocal的Hash值的计算
// hash code
private final int threadLocalHashCode = nextHashCode();
// AtomicInteger类型,从0开始
private static AtomicInteger nextHashCode =
new AtomicInteger();
// hash code每次增加1640531527
private static final int HASH_INCREMENT = 0x61c88647;
// 下一个hash code
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
ThreadLocal的hashcode(threadLocalHashCode)是从0开始,每新建一个ThreadLocal,对应的hashcode就加0x61c88647.
ThreadLocal的set方法
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t); // 获取当前线程的ThreadLocalMap
// 当前线程的ThreadLocalMap不为空则调用set方法, this为调用该方法的ThreadLocal对象
if (map != null)
map.set(this, value);
// map为空则调用createMap方法创建一个新的ThreadLocalMap, 并新建一个Entry放入该
// ThreadLocalMap, 调用set方法的ThreadLocal和传入的value作为该Entry的key和value
else
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals; // 返回线程t的threadLocals属性,类型是ThreadLocalMap
}
- 先拿到当前线程,再使用getMap方法拿到当前线程的threadLocals变量(类型ThreadLocalMap)
- 如果threadLocals不为空,则将当前ThreadLocal作为key,传入的值作为value,调用set方法(见下文ThreadLocalMap的set方法)插入threadLocals。
- 如果threadLocals为空则调用创建一个ThreadLocalMap,并新建一个Entry放入该ThreadLocalMap, 调用set方法的ThreadLocal自身和传入的value作为该Entry的key和value
注意此处的threadLocals变量是一个ThreadLocalMap,是Thread的一个局部变量,因此它只与当前线程绑定。
ThreadLocal的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();
}
也是先获得当前线程的ThreadLocalMap变量.其实ThreadLocal类就像是一个工具类一样,核心是其内部类ThreadLocalMap.
ThreadLocalMap 源码分析
示意图
ThreadLocalMap是一个自定义哈希映射,仅用于维护线程本地变量值。
ThreadLocalMap是ThreadLocal的内部类,主要有一个Entry数组,Entry的key为ThreadLocal,value为ThreadLocal对应的值。
每个线程都有一个ThreadLocalMap类型的threadLocals变量。
成员变量和相关方法
// 初始容量16
private static final int INITIAL_CAPACITY = 16;
// entry数组
private Entry[] table;
// entry 元素个数
private int size = 0;
// 扩容阀值
private int threshold; // Default to 0
// 一次set是2/3 len
private void setThreshold(int len) { threshold = len * 2 / 3; }
//下面两个方法可以向前/后获取坐标.
private static int nextIndex(int i, int len) { return ((i + 1 < len) ? i + 1 : 0);}
private static int prevIndex(int i, int len) {return ((i - 1 >= 0) ? i - 1 : len - 1); }
Entry实体
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
结合示意看,entry中的key是一个弱引用.它的存在不会使得ThreadLocal变量豁免垃圾回收.当定义ThreadLocal变量的线程终止,没有强引用关联ThreadLocal变量,它就会被垃圾回收.
说到底ThreadLocal变量只是一个key而已,可以用它去各个线程自己的ThreadLocalMap中查询,它本身并不存储信息.
当然这会造成各个线程的ThreadLocalMap中出现key为null的entry.这个在后边内存泄漏会讲到.
扩容
private void rehash() {
expungeStaleEntries(); // 调用expungeStaleEntries方法清理key为null的Entry
// 如果清理后size超过阈值的3/4, 则进行扩容
if (size >= threshold - threshold / 4)
// 大小*2
// h = k.threadLocalHashCode & (newLen - 1);
resize();
}
清理key为null的entry
cleanSomeSlots方法
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
do {
i = nextIndex(i, len); // 下一个索引位置
Entry e = tab[i];
if (e != null && e.get() == null) { // 遍历到key为null的元素
n = len; // 重置n的值
removed = true; // 标志有移除元素
i = expungeStaleEntry(i); // 移除i位置及之后的key为null的元素
}
} while ( (n >>>= 1) != 0);
return removed;
}
从 i 开始,清除key为空的Entry,扫描次数是log2n
,当遍历到一个key为null的元素时,调用expungeStaleEntry清除,并将遍历次数重置。
n的值可能是当前元素个数size(从增加元素操作调过来).或者整个entry长度len(从replaceStaleEntry方法调过来).
官方给出的解释是这个方法简单、快速,并且效果不错。
expungeStaleEntry 方法
// 从staleSlot开始, 清除key为空的Entry, 并将不为空的元素放到合适的位置,最后返回Entry为空的位置
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
tab[staleSlot].value = null; // 将tab上staleSlot位置的对象清空
tab[staleSlot] = null;
size--;
// Rehash until we encounter null
Entry e;
int i;
for (i = nextIndex(staleSlot, len); // 遍历下一个元素, 即(i+1)%len位置的元素
(e = tab[i]) != null; // 遍历到Entry为空时, 跳出循环并返回索引位置
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) { // 当前遍历Entry的key为空, 则将该位置的对象清空
e.value = null;
tab[i] = null;
size--;
} else { // 当前遍历Entry的key不为空
int h = k.threadLocalHashCode & (len - 1); // 重新计算该Entry的索引位置
if (h != i) { // 如果索引位置不为当前索引位置i
tab[i] = null; // 则将i位置对象清空, 替当前Entry寻找正确的位置
// 如果h位置不为null,则向后寻找当前Entry的位置
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
ThreadLocalMap的set方法
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1); // 计算出索引的位置
// 从索引位置开始遍历,由于不是链表结构,因此通过nextIndex方法来寻找下一个索引位置
for (Entry e = tab[i];
e != null; // 当遍历到的Entry为空时结束遍历
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get(); // 拿到Entry的key,也就是ThreadLocal
// 该Entry的key和传入的key相等, 则用传入的value替换掉原来的value
if (k == key) {
e.value = value;
return;
}
// 该Entry的key为空, 则代表该Entry需要被清空,
// 调用replaceStaleEntry方法
if (k == null) {
// 该方法会继续寻找传入key的安放位置, 并清理掉key为空的Entry
replaceStaleEntry(key, value, i);
return;
}
}
// 寻找到一个空位置, 则放置在该位置上
tab[i] = new Entry(key, value);
int sz = ++size;
// cleanSomeSlots是用来清理掉key为空的Entry,如果此方法返回true,则代表至少清理
// 了1个元素, 则此次set必然不需要扩容, 如果此方法返回false则判断sz是否大于阈值
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash(); // 扩容
}
- 通过传入的key的hashCode计算出索引的位置
- 从索引位置开始遍历,由于不是链表结构,因此通过nextIndex方法来寻找下一个索引位置
- 如果找到某个Entry的key和传入的key相同,则用传入的value替换掉该Entry的value。
- 如果遍历到某个Entry的key为空,则调用replaceStaleEntry方法
- 如果通过nextIndex寻找到一个空位置(代表没有找到key相同的),则将元素放在该位置上
- 调用cleanSomeSlots方法清理key为null的Entry,并判断是否需要扩容,如果需要则调用rehash方法进行扩容。
replaceStaleEntry 方法
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
int slotToExpunge = staleSlot; // 清除元素的开始位置(记录索引位置最前面的)
// 向前遍历,直到遇到Entry为空
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i; // 记录最后一个key为null的索引位置
// Find either the key or trailing null slot of run, whichever
// occurs first
for (int i = nextIndex(staleSlot, len); // 向后遍历,直到遇到Entry为空
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
// 该Entry的key和传入的key相等, 则将传入的value替换掉该Entry的value
if (k == key) {
e.value = value;
// 将i位置和staleSlot位置的元素对换(staleSlot位置较前,是要清除的元素)
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
// 如果相等, 则代表上面的向前寻找key为null的遍历没有找到,
// 即staleSlot位置前面的元素没有需要清除的,此时将slotToExpunge设置为i,
// 因为原staleSlot的元素已经被放到i位置了,这时位置i前面的元素都不需要清除
if (slotToExpunge == staleSlot)
slotToExpunge = i;
// 从slotToExpunge位置开始清除key为空的Entry
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
// 如果第一次遍历到key为null的元素,并且上面的向前寻找key为null的遍历没有找到,
// 则将slotToExpunge设置为当前的位置
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
// 如果key没有找到,则新建一个Entry,放在staleSlot位置
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
// 如果slotToExpunge!=staleSlot,代表除了staleSlot位置还有其他位置的元素需要清除
// 需要清除的定义:key为null的Entry,调用cleanSomeSlots方法清除key为null的Entry
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
- slotToExpunge始终记录着需要清除的元素的最前面的位置(即slotToExpunge前面的元素是不需要清除的)
- 从位置staleSlot向前遍历,直到遇到Entry为空,用staleSlot记录最后一个key为null的索引位置(也就是遍历过位置最前的key为null的位置)
- 从位置staleSlot向后遍历,直到遇到Entry为空,如果遍历到key和入参key相同的,则将入参的value替换掉该Entry的value,并将i位置和staleSlot位置的元素对换(staleSlot位置较前,是要清除的元素),遍历的时候判断slotToExpunge的值是否需要调整,最后调用expungeStaleEntry方法和cleanSomeSlots方法清除key为null的元素。
- 如果key没有找到,则使用入参的key和value新建一个Entry,放在staleSlot位置
- 判断是否还有其他位置的元素key为null,如果有则调用expungeStaleEntry方法和cleanSomeSlots方法清除key为null的元素
源码总结
- 每个线程都有一个ThreadLocalMap 类型的 threadLocals 属性。
- ThreadLocalMap 类相当于一个Map,key 是 ThreadLocal 本身,value 就是我们的值。
- 当我们通过 threadLocal.set(new Integer(123)); ,我们就会在这个线程中的 threadLocals 属性中放入一个键值对,key 是 这个 threadLocal.set(new Integer(123))的threadlocal,value 就是值new Integer(123)。
- 当我们通过 threadlocal.get() 方法的时候,首先会根据这个线程得到这个线程的 threadLocals 属性,然后由于这个属性放的是键值对,我们就可以根据键 threadlocal 拿到值。 注意,这时候这个键 threadlocal 和 我们 set 方法的时候的那个键 threadlocal 是一样的,所以我们能够拿到相同的值。
- ThreadLocalMap 的get/set/remove方法跟HashMap的内部实现都基本一样,通过 "key.threadLocalHashCode & (table.length - 1)" 运算式计算得到我们想要找的索引位置,如果该索引位置的键值对不是我们要找的,则通过nextIndex方法计算下一个索引位置,直到找到目标键值对或者为空。
- hash冲突:在HashMap中相同索引位置的元素以链表形式保存在同一个索引位置;而在ThreadLocalMap中,没有使用链表的数据结构,而是将(当前的索引位置+1)对length取模的结果作为相同索引元素的位置: 源码中的nextIndex方法,可以表达成如下公式:如果i为当前索引位置,则下一个索引位置 = (i + 1 < len) ? i + 1 : 0
问题辨析
为什么ThreadLocal通常被static修饰?
ThreadLocal 是一个key,去各个线程自己的map里取值,没必要多个.
Java 中每个线程都有与之关联的Thread对象,Thread对象中有一个ThreadLocal.ThreadLocalMap类型的成员变量,该变量是一个Hash表.所以每个线程都单独维护这样一个Hash表. 当ThreadLocal类型对象调用set方法时,这个set方法会使用当前线程维护的Hash表,把自己作为key, set方法的参数作为value插入到Hash表中.由于每个线程维护的Hash表是独立的,因此在不同的Hash表中,key值即使相同也是没问题的.
如果不使用static的ThreadLocal变量,那么当定义ThreadLocal的类创建新的实例时候,会出现多个ThreadLocal.这在大多数时候是没有意义的.
内存泄漏问题
ThreadLocal变量被正常回收
ThreadLocalMap使用ThreadLocal的弱引用作为Entry的key,如果一个ThreadLocal没有外部强引用来引用它,下一次系统GC时,这个ThreadLocal必然会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value。
我们上面介绍的get、set、remove等方法中,都会对key为null的Entry进行清除(expungeStaleEntry方法,将Entry的value清空,等下一次垃圾回收时,这些Entry将会被彻底回收)。
但是如果当前线程一直在运行,并且一直不执行get、set、remove方法,这些key为null的Entry的value就会一直存在一条强引用练:Thread Ref -> Thread -> ThreadLocalMap -> Entry -> value,导致这些key为null的Entry的value永远无法回收,造成内存泄漏。
为了避免这种情况,我们可以在使用完ThreadLocal后,手动调用remove方法,以避免出现内存泄漏。
ThreadLocal变量没被正常回收
由于我们一般把ThreadLocal变量声明成static类型,延长了它的生命周期,如果由于一些原因,持有ThreadLocal变量声明的类没有被回收. 那么确实所有使用这个ThreadLocal变量的线程中的ThreadLocalMap中的entry中的value都不会被回收(key != null). 这确实会造成问题,尤其当你的工作线程是复用的,本身永远不会终止,比如线程池/tomat的工作线程. 更糟糕的是,无法回收的value对应的类本身,和它的类加载器也无法被回收.这个类加载器所有加载的所有的类的数据会存留在永久区.造成永久区内存泄漏(permgen leak)
使用后remove()是一个好习惯.
InheritableThreadLocal
InheritableThreadLocal类是ThreadLocal类的子类. 子线程会copy父线程的值,形成自己的副本(浅拷贝).
1.对于可变对象:
- 父线程初始化, 因为Thread Construct浅拷贝, 共用索引, 子线程修改父线程跟着变;
- 父线程不初始化, 子线程初始化, 无Thread Construct浅拷贝, 子线程和父线程都是单独引用, 不同对象, 子线程修改父线程不跟着变。
2.对于不可变对象: 不可变对象由于每次都是新对象, 所以无论父线程初始化与否,子线程和父线程都互不影响。
从上面两条结论可知,子线程只能通过修改可变性(Mutable)对象对主线程才是可见的,即才能将修改传递给主线程,但这不是一种好的实践,不建议使用,为了保护线程的安全性,一般建议只传递不可变(Immuable)对象,即没有状态的对象。