序言
ThreadLocal,很多人没搞清楚它的原理和用法,包括我以前也理解错了它的原理和使用场景,今天我就来彻底地看一看它的使用场景和原理。
使用场景
ThreadLocal,顾名思义是存储线程本地变量的意思,可以让多线程的情况下每个线程都可以线程安全地访问且只能访问属于自己线程的变量。但是!但是!但是!它并不能解决多线程访问共享变量的问题,每个线程只能访问属于本线程的变量,不能访问其他线程的变量。
那么有一个问题,想一想,为什么它会被发明出来?
1、如果不用ThreadLocal,也能实现每个线程都有自己的本地变量,比如:
public class T extends Thread{
public Object boj;
public Object getObj (){
return this.obj;
}
public void setObj(Object obj){
this.obj = obj;
}
}
每个线程都塞进去一个局部变量即可,但是这样做会很麻烦,每个线程都需要参数传递。
所以使用ThreadLocal来存放线程都要用到的变量会简洁方便很多,它为每个线程都提供了存放局部变量的地方。
2、它究竟和线程同步有啥区别?
首先它俩要解决的问题本质不同!
线程同步
是要解决多线程间通信的问题ThreadLocal
让每个线程都有自己的本地变量,也就是隔离多线程之间的变量共享。
所以“认为 ThreadLocal 相对于 线程同步机制来说 是空间换时间”的说法其实是不准确的,因为两个方案要解决的问题本质就不同。
3、 有什么工程场景使用到了ThreadLocal?
我印象比较深的是spring的事务管理,事务管理一定会涉及到数据库连接,web应用来说,开启一个事务必须要获取一个数据库连接,那么对于一个事务过程中的所有方法如何获取一个数据库连接?
spring便使用了基于ThreadLocal的资源与事务线程绑定成功地解决了这个问题:
一个web线程过来以后,请求数据库时把数据库连接的资源绑定在当前处理线程的ThreadLocal上,这样这个线程的所有方法都能够访问到这个数据库连接。
但是这里要注意,事务与ThreadLocal的生命周期:
如果从字面上看,很容易误认为事务资源绑定在线程上,事务的生命周期和线程的生命周期相同。
但这是错误的认识,事务的生命周期是和数据库连接的生命周期相同的,和线程的生命周期并没有关系。
怎么实现?
终于到了具体怎么实现了
好吧,很多人心目中的想法可能和我以前一样,不就是ThreadLocal吗,我弄成这样一个数据结构
ThreadLocal<Thread,Map<k,v>>
就可以啦(相信很多人开始都是这么以为的),以Thread为key,然后用一个Map作为Object,后来细看代码后发觉不是这样的,下面先看一下UML图:
和之前想象的不同,每个Thread中包含一个ThreadLocalMap,这个ThreadLocalMap和一个hashMap类似,其中的Entry继承自弱引用WeakReference:
static class Entry extends WeakReference<ThreadLocal> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal k, Object v) {
super(k);
value = v;
}
}
弱引用:我的理解是一旦对象为null,则会被标记为垃圾,如果被垃圾,然后会被垃圾回收器给回收掉。
为啥使用弱引用?
主要还是从内存管理角度出发,弱引用可以让ThreadLocalMap知道ThreadLocal是否失效,一旦失效就会抹去对应的k-v键值对。来保证Map的内存占用。
为啥把threadLocalMap放到单独的线程里面自己存储?
据说在早期版本里就是放到一个Map中的,但是测试后发现由于Map太大了,才分到了每个Thread中。(不保证正确性)
当然如前面所说,ThreadLocalMap存储在每一个对应的Thread中,下面详细看一下set()方法 和 get()方法 就知道什么意思了。
public void set(T value) {
//拿到当前线程
Thread t = Thread.currentThread();
//map就是t.threadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null)
//调用实际的set()方法
map.set(this, value);
else
//如果为空则直接创建一个map
createMap(t, value);
}
/**
* Set the value associated with key.
*
* @param key the thread local object
* @param value the value to be set
*/
private void set(ThreadLocal<?> key, Object value) {
// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.
Entry[] tab = table;
int len = tab.length;
//计算hash槽
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];//从当前的槽开始
e != null;//槽不为空
e = tab[i = nextIndex(i, len)]/*往后移一个*/) {
//拿到当前Entry的key---ThreadLocal
ThreadLocal<?> k = e.get();
//如果相等则设置value
if (k == key) {
e.value = value;
return;
}
//如果k为空则替换当前的key和value到这个entry
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
//如果槽(Entry)为空,new一个Entry
tab[i] = new Entry(key, value);
int sz = ++size;
//如果清理过期槽失败或者数量超过警戒值则rehash
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
public T get() {
//获取当前线程
Thread t = Thread.currentThread();
//拿到当前线程的ThreadLocalMap
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();
}
private Entry getEntry(ThreadLocal<?> key) {
//计算索引
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
//如果拿得到key
if (e != null && e.get() == key)
return e;
else
//如果拿不到key
return getEntryAfterMiss(key, i, e);
}
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
//当Entry不为空
while (e != null) {
//拿到ThreadLocal---key
ThreadLocal<?> k = e.get();
//如果key相等则返回
if (k == key)
return e;
//如果key为空则清楚掉没有被引用的Entry
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);//从一下个开始找
e = tab[i];
}
return null;
}
上面的代码解析有很多细节,大家不需要死记硬背,平时用不到细节很容易忘记。
总结
下面总结一下ThreadLocal的重点知识:
- 1、ThreadLocal的理念和线程同步机制不同,前者希望做到线程间的资源隔离,后者希望做到多线程通信的问题。
- 2、ThreadLocal存储的ThreadLocalMap是放在每个Thread当中,并不是放在一个大Map里面。
- 3、ThreadLocal所存储资源的生命周期并不是和Thread绑定在一起的
- 4、什么情况下被废弃的ThreadLocal引用会被清理?
(1)Thread线程结束
(2)ThreadLocalMap中的数量到达最大值
(3)set方法调用时,hash没有命中,new Entry时,会被清理
(4)自己调用remove()
5、ThreadLocal有什么坑?
ThreadLocal “污染”:对于线程池这种可能会导致线程复用的工具,如果在一次使用后忘记调用remove方法,当前使用的ThreadLocal变量会被保留,线程被下一次使用的时候就会造成变量污染,就会导致一些诡异的错误。
比如:tomcat中的ThreadLocal,由于是线程池处理连接,所以不能在线程的ThreadLocal里面存放关于本次request的特殊变量,应该存放一些公共变量,否则就会导致变量被污染。