1、TheadLocal类不继承Thread类,也不实现Runable接口,ThreadLocal类为每一个线程都维护了自己独有的变量拷贝。每个线程都拥有自己独立的变量。
ThreadLocal采用了“以空间换时间”的方式:访问并行化,对象独享化。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。
ThreadLocal的实现是这样的:每个Thread维护了一个ThreadLocalMap映射表,这个映射表的key是ThreadLocal实例本身。Value是真正需要存储的变量。也就是说,ThreadLocal本身并不存储值,它只是作为一个key来让线程从ThreadLocalMap获取value。注意,ThreadLocalMap是使用ThreadLocal的弱引用作为key的,弱引用的对象在GC时会被
2、ThreadLocal的底层实现原理
(1)
public class Thread {
ThreadLocal.ThreadLocalMap threadLocals = null;
}
ThreadLocal的set方法:
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
在上面的set方法中,首先会通过getMap方法来获取当前线程中的ThreadLocalMap对象。
在Thread类内部,有一个专门的成员用于存储线程的ThreadLocalMap对象:Thread.threadLocals,这个引用是在ThreadLocal类中createmap函数内初始化的。
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)
return (T)e.value;
}
return setInitialValue();
}
如果Thread的ThreadLocalMap对象不为空,则调用其getEntry方法获取ThreadLocal对应的值。
注:ThreadLocal采用数据存储的方式,以ThreadLocal的hashcode&数组长度作为下标,存储的是变量的值。
自定义Entry类用于存储
(2)
在ThreadLocalMap的底层是一个初始大小为16的Entry数组,Entry对象用来保存每一个key-value键值对,只不过这里的key永远是ThreadLocal对象。
没有链表结构,那发生hash冲突了怎么办?
利用开放地址法,每个ThreadLocal对象都有一个hash值 threadLocalHashCode
,每初始化一个ThreadLocal对象,hash值就增加一个固定的大小 0x61c88647
。
在插入过程中,根据ThreadLocal对象的hash值,定位到table中的位置i,过程如下: 1、如果当前位置是空的,那么正好,就初始化一个Entry对象放在位置i上; 2、不巧,位置i已经有Entry对象了,如果这个Entry对象的key正好是即将设置的key,那么重新设置Entry中的value; 3、很不巧,位置i的Entry对象,和即将设置的key没关系,那么只能找下一个空位置(注:即使不同的hash值,也可能映射到数组的相同位置);
这样的话,在get的时候,也会根据ThreadLocal对象的hash值,定位到table中的位置,然后判断该位置Entry对象中的key是否和get的key一致,如果不一致,就判断下一个位置
可以发现,set和get如果冲突严重的话,效率很低,因为ThreadLoalMap是Thread的一个属性,所以即使在自己的代码中控制了设置的元素个数,但还是不能控制其它代码的行为。
可以从ThreadLocal的get函数中看出来,其中getmap函数是用t作为参数,这里t就是当前执行的线程。
从而得知,get函数就是从当前线程的threadlocalmap中取出当前线程对应的变量的副本【注意,变量是保存在线程中的,而不是保存在ThreadLocal变量中】。当前线程中,有一个变量引用名字是threadLocals,这个引用是在ThreadLocal类中createmap函数内初始化的。每个线程都有一个这样的threadLocals引用的ThreadLocalMap,以ThreadLocal和ThreadLocal对象声明的变量类型作为参数。这样,我们所使用的ThreadLocal变量的实际数据,通过get函数取值的时候,就是通过取出Thread中threadLocals引用的map,然后从这个map中根据当前threadLocal作为参数,取出数据。
(3)ThreadLocal内存泄漏
注:各种引用
I:强引用,强引用就是指在程序代码中普遍存在的,类似“Object obj = new Object()”这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
II:软引用,是用来描述一些还有用但并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列入回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。
III:弱引用,弱引用也是描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾回收之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。
IV:虚引用,也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象自是否有虚引用存在,完全不会对其生存空间构成影响,也无法通过虚引用来获得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被回收器回收时收到一个系统通知。
ThreadLocal为什么会内存泄漏?
ThreadLocalMap使用ThreadLocal的弱引用作为key,若果一个ThreadLocal没有外部强引用用来引用它,那么系统GC的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref->Thread->ThreadLocalMap->Entry-value永远无法回收,造成内存泄漏。其实,ThreadLocalMap的设计中已经考虑到了这种情况,也加上了一些防护措施:在ThreadLocal的get(),set(),remove()的时候都会清除线程ThreadLocalMap里所有key为null的value。
p使用static的ThreadLocal,延长了ThreadLocal的生命周期。分配使用了ThreadLocal又不再调用get(),set(),remove()方法,那么就会导致内存泄漏。
从表面上看内存泄漏的根源在于使用了弱引用。为什么使用弱引用而不是强引用?
下面我们分两种情况讨论:
key 使用强引用:引用的ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。
key 使用弱引用:引用的ThreadLocal的对象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。
比较两种情况,我们可以发现:由于ThreadLocalMap的生命周期跟Thread一样长,如果都没有手动删除对应key,都会导致内存泄漏,但是使用弱引用可以多一层保障:弱引用ThreadLocal不会内存泄漏,对应的value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。
因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。
(4)自定义ThreadLocal实现
如何让大家去实现一个ThreadLocal,我相信很多同学第一时间会写出如下代码:
public class ThreadLocal {
private Map values = new java.util.WeakHashMap();
public synchronized void set(T value) {
values.put(Thread.currentThread(), value);
}
public synchronized T get() {
return values.get(Thread.currentThread());
}
}
上述ThreadLocal其实是可以完成ThreadLocal功能的,但是在性能上却不是最优的。毕竟多线程访问ThreadLocal的map对象会导致并发冲突,用synchronized加锁会导致性能上的损失。
因此,JDK7里是将values这个Map对象保存在线程中,这样每个线程去取自己的数据,就不需要加锁保护的。