ThreadLocal解析
ThreadLocal含义
首先明确ThreadLocal并不是解决多线程下共享对象的并发访问而产生的,他也做不到这一点,因为他内部保存的仍然是对象的引用,而不是真的存储的是对象本身,通常使用ThreadLocal保存的对象,是当前线程单独创建(new)的对象,然后通过ThreadLocal.set()放入到当前当前线程中,这样对该对象的操作肯定不会影响到其他线程,其他线程也不需要对该对象进行操作(其他线程想要应该要自己去创建属于该线程自己的对象)
下面代码证明:ThreadLocal存储的是对象引用,无法保证共享对象的线程安全
public class ThreadLocalTest {
public static void main(String[] args) {
final Student aa = new Student();
aa.setName("初始名称");
new Thread(new Runnable() {
public void run() {
ThreadLocal<Student> threadLocal1 = new ThreadLocal<Student>();
threadLocal1.set(aa);
System.out.println("线程一:"+threadLocal1.get().getName());
threadLocal1.get().setName("被线程一修改后的名称");
}
}).start();
new Thread(new Runnable() {
public void run() {
ThreadLocal<Student> threadLocal2 = new ThreadLocal<Student>();
threadLocal2.set(aa);
try {
Thread.sleep(2000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程二:"+threadLocal2.get().getName());
}
}).start();
}
}
class Student{
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
运行上面程序获取的结果是:
线程一:初始名称
线程二:被线程一修改后的名称
上面的程序含义是创建一个共享的Student对象,创建两个线程分别在线程内部使用ThreadLocal保存该对象,线程一保存完毕后修改该对象的属性,根据程序运行结果可以看出线程二中保存的Student对象名称也被修改,因此ThreadLocal不能保存或者说不应保存共享对象,他应该只保存当前线程私有(本地)变量
ThreadLocal的实现(参考的JDK1.7的源码)
set()方法
ThreadLocal保证在当前线程中创建的对象,通过ThreadLocal.set()放入到当前当前线程中,通过ThreadLocal.get()获取存入的值,其他线程无法通过ThreadLocal.get()获取当前线程存入的值,这里的实现可以通过查看ThreadLocal的set/get方法源码查看
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);
}
- 每一个线程都有一个ThreadLocal.ThreadLocalMap类型的成员变量,这里获取当前线程对应的ThreadLocalMap,他存储的也是key-value类型的数据结构,这里如果不理解ThreadLocalMap的结构可以先将ThreadLocalMap简单看成HashMap,后面具体分析ThreadLocalMap的结构
- 如果当前线程有ThreadLocalMap,将ThreadLocal对象本身作为键,要存的值作为值存放到ThreadLocalMap中,没有就创建一个ThreadLocalMap然后存放键及值
ThreadLocalMap结构及深入set()/get()方法
ThreadLocalMap的结构和HashMap类似,如下图所示:
ThreadLocalMap内部同样有一个初始大小为16的Entry数组,他和HashMap中的Entry区别是
- Entry类继承WeakReference
,他对键的引用是弱引用 - 他的数据结构里面没有链表,发生哈希冲突后会根据key判断更新当前位置值还是寻找下一个位置
关于第一点区别可以参考Entry类的源码
static class Entry extends WeakReference<ThreadLocal> {
Object value;
Entry(ThreadLocal k, Object v) {
super(k);
value = v;
}
}
从源码上可以看出Entry本质上是WeakReference
关于第二点区别可以参考源码详细分析
private void set(ThreadLocal key, Object value) {
Entry[] tab = table;
int len = tab.length;
//确定本次存储在Entry数组的索引位置
int i = key.threadLocalHashCode & (len-1);
/*
*对于本次要存储的键值对应位置已有值,则说明发生了hash冲突
*发生hash冲突的解决方法是:遍历整个数组,直到找到key或者是找到空位
*/
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal k = e.get();
//找到了该Entry并且键值相等,新值换旧值
if (k == key) {
e.value = value;
return;
}
//该位置key已经被gc回收,
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
//未发生hash碰撞直接赋值即可
tab[i] = new Entry(key, value);
int sz = ++size;
/*
*添加数据后做一下是否扩容的判断
*1.没有要清理的被gc回收key的Entry
*2.满足容量扩容条件
*/
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
从源码上set()步骤就是
- 确定本次存储的键值在Entry数组中的位置
- 看是否发生Hash冲突,如果冲突了(该位置已经有Entry对象了),看该位置的Entry的的key是否为null,为null说明该键已被gc回收,将现有的键值插入即可,不为null查看已经存在的key和本次要插入的key是否相同,如果相同,更新值即可
- 未发生冲突直接构造新的键值Entry存到数组中即可
- 判断是否需要扩容
ThreadLocal的应用场景
- hibernate数据库链接池,spring的事务实现
- 多层间的参数传递
- 某些情况提升安全和性能(DateFormat工具类)
ThreadLocal的问题-内存泄漏
所谓ThreadLocal的内存泄漏主要体现在线程池的环境下,Entry中的value无法被垃圾回收器回收,因为有一条强引用链Thread->ThreadLocalMap->Entry->某个Entry中的value
,正常情况下当线程执行完销毁后这条引用链就不会存在了,但线程池环境下线程不会被销毁,因此也就造成所谓的内存泄漏,ThreadLocal的源码中也做了一定的措施防范内存泄漏,就是对应线程之后调用ThreadLocal的get和set方法都有很高的概率会顺便清理掉无效对象,断开value强引用,从而大对象被收集器回收。但是这是有概率的,因此线程池环境下,我们编码要注意用完调用remove()方法
参考链接
ThreadLocal源码解读
ThreadLocal 内部实现、应用场景和内存泄漏
ThreadLocal 内存泄露的实例分析
清理ThreadLocal