一、ThreadLocal介绍
这是一个线程的局部变量。也就是说,只有当前线程可以访问。既然是只有当前线程可以访问的数据,自然是线程安全的。
为每一个线程分配不同的对象,需要在应用层面保证。ThreadLocal只是起到了简单的容器作用。
二、实现原理
1. 我们需要关注的是ThreadLocal的set()方法和get()方法。
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);
}
在set时,首先获得当前线程对象,然后通过getMap()拿到线程的ThreadLocalMap,并将值设入ThreadLocalMap中。而ThreadLocalMap可以理解为一个Map(虽然不是,但是你可以把它简单地理解成HashMap),但是它是定义在Thread内部的成员。注意下面的定义是从Thread类中摘出来的:
ThreadLocal.ThreadLocalMap threadLocals = null;
而设置到ThreadLocal中的数据,也正是写入了threadLocals这个Map。其中,key为Thread-Local当前对象,value就是我们需要的值。而threadLocals本身就保存了当前自己所在线程的所有“局部变量”,也就是一个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();
}
首先,get()方法也是先取得当前线程的ThreadLocalMap对象。然后,通过将自己作为key取得内部的实际数据。
2.Thread类会进行一些清理工作,其中就包括清理ThreadLocalMap
这些变量是维护在Thread类内部的(ThreadLocalMap定义所在类),这也意味着只要线程不退出,对象的引用将一直存在。
下面是Thread中的方法:
/**
* This method is called by the system to give a Thread
* a chance to clean up before it actually exits.
*/
private void exit() {
if (group != null) {
group.threadTerminated(this);
group = null;
}
/* Aggressively null out all reference fields: see bug 4006245 */
target = null;
/* Speed the release of some of these resources */
threadLocals = null;
inheritableThreadLocals = null;
inheritedAccessControlContext = null;
blocker = null;
uncaughtExceptionHandler = null;
}
所以当我们使用线程池的时候,那就意味着线程未必会退出(比如固定大小的线程池,线程总是存在)
如果这样,将一些大大的对象设置到ThreadLocal中(它实际保存在线程持有的thread-Locals Map内),可能会使系统出现内存泄露的可能(这里我的意思是:你设置了对象到ThreadLo-cal中,但是不清理它,在你使用几次后,这个对象也不再有用了,但是它却无法被回收)。
此时,如果你希望及时回收对象,最好使用ThreadLocal.remove()方法将这个变量移除。就像我们习惯性地关闭数据库连接一样。如果你确实不需要这个对象了,那么就应该告诉虚拟机,请把它回收掉,防止内存泄露。
另外一种有趣的情况是JDK也可能允许你像释放普通变量一样释放ThreadLocal。比如,我们有时候为了加速垃圾回收,会特意写出类似obj=null之类的代码。如果这么做,obj所指向的对象就会更容易地被垃圾回收器发现,从而加速回收。同理,如果对于ThreadLocal的变量,我们也手动将其设置为null,比如tl=null。那么这个ThreadLocal对应的所有线程的局部变量都有可能被回收。
要了解这里的回收机制,我们需要更进一步了解Thread.ThreadLocalMap的实现。之前我们说过,ThreadLocalMap是一个类似HashMap的东西。更精确地说,它更加类似WeakHashMap。ThreadLocalMap的实现使用了弱引用。弱引用是比强引用弱得多的引用。Java虚拟机在垃圾回收时,如果发现弱引用,就会立即回收。Thread-LocalMap内部由一系列Entry构成,每一个Entry都是WeakReference<ThreadLocal>:
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
这里的参数k就是Map的key,v就是Map的value。其中k也就是ThreadLocal实例,作为弱引用使用(super(k)就是调用了WeakReference的构造函数)。因此,虽然这里使用ThreadLocal作为Map的key,但是实际上,它并不真的持有ThreadLocal的引用。而当ThreadLocal的外部强引用被回收时,ThreadLocalMap中的key就会变成null。当系统进行ThreadLocalMap清理时(比如将新的变量加入表中,就会自动进行一次清理,虽然JDK不一定会进行一次彻底的扫描,但显然在我们这个案例中,它奏效了),就会自然将这些垃圾数据回收。
三、对性能的影响
为每一个线程分配一个独立的对象对系统性能也许是有帮助的。当然了,这也不一定,这完全取决于共享对象的内部逻辑。如果共享对象对于竞争的处理容易引起性能损失,我们还是应该考虑使用ThreadLocal为每个线程分配单独的对象。
一个典型的案例就是在多线程下产生随机数。在多线程共享一个Random实例的情况下,总耗时达13秒之多(这里是指4个线程的耗时总和,不是程序执行的经历时间)。而在ThreadLocal模式下,仅耗时1.7秒左右。