在Java中,我们知道,如果需要进行线程间共享数据,可能会使用加锁或者volatile的方式;如果想要在单个线程里共享数据,可能就会使用ThreadLocal变量。本文将浅析我理解的和我看到的ThreadLocal变量,包括它的机制、使用方式和常见问题。
机制
我们知道ThreadLocal的使用方式如下:(这段代码是在ThreadLocal变量的doc中抄过来的)
public class ThreadId {
// Atomic integer containing the next thread ID to be assigned
private static final AtomicInteger nextId = new AtomicInteger(0);
private static final ThreadLocal<Integer> threadId =
new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
return nextId.getAndIncrement();
}
};
// Returns the current thread's unique ID, assigning it if necessary
public static int get() {
return threadId.get();
}
}
在调用ThreadLocal.get()方法时,首先得到当前线程,然后从当前线程中取得ThreadLocalMap变量。如果存在这个map,那么以ThreadLocal为key就可以获得当前线程的value了。这种方式相当于让每个线程都拥有一个变量的拷贝。
//ThreadLocal中的get方法
public T get() {
Thread t = Thread.currentThread();
//每个Thread中都有ThreadLocalMap,这个map以ThreadLocal为key,保存的值为value
//比如ThreadLocalMap<ThreadLocal,Integer>。 所以可以通过当前线程获得这个map。
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null)
return (T)e.value;
}
return setInitialValue();
}
在更深层次的读代码后发现,这个ThreadLocalMap后面并不是HashMap,而是它自己实现了一套map。另外,map中的entry对象很有意思,如下:
static class ThreadLocalMap {
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal k, Object v) {
super(k);
value = v;
}
}
可以发现entry是一个WeakReference对象,我理解的原因是如果这个ThreadLocal变量不再使用了,那么entry会被垃圾回收掉。
常见场景
SimpleDataFormat是一个线程不安全的类,并且它的性能不是很好。在多线程情况下,一般能想到的使用方式是,
每次调用的时候都是new出来一个SimpleDataFormat对象
使用加锁的方式来使用SimpleDataFormat
加锁不是很好,每次new呢,性能也不是不好,所以在这里,就可以发挥ThreadLocal的强大功能了。
public class Foo{
private static final ThreadLocal<SimpleDateFormat> formatter = new ThreadLocal<SimpleDateFormat>(){
@Override
protected SimpleDateFormat initialValue()
{
return new SimpleDateFormat("yyyyMMdd HHmm");
}
};
public String formatIt(Date date)
{
return formatter.get().format(date);
}}
这种方式非常高效、优雅。
常见问题
在使用ThreadLocal变量的时候,在使用完毕之后,需要显示的主动的释放资源,这是一个很好的习惯
threadLocal.remove();
在有线程池的情况下,这种显示remove的好处在于,
1. 可以避免因为线程池的重用而导致的threadlocal变量中get的数据是老线程的数据,而导致不正确
2. 在一些Web容器中,比如tomcat、jboss的中,如果不主动释放资源,那么可能会导致out of memory的异常出现。
那么为什么会出现OOM呢?
因为在tomcat或者其他web容器中,通常有线程池。线程池中的线程是复用的,Thread不会被销毁,而ThreadLocal变量是保存在Thread中的ThreadLocalMap中的,所以如果不主动清理这些变量,那么这些ThreadLocal变量一直会存在线程池中,不被清理。就算tomcat中的应用销毁了,这些threadlocal变量还在。这就造成了memory leak,但还不至于出现OOM。那么什么情况下出现OOM呢?
当应用不停的reload的情况下会出现OOM。为什么呢?当tomcat中的应用卸载的时候,classloader会被回收,那么GC会清理在perm heap中不被使用的class(注:PermGen中可以被回收的条件是:classloader可以被回收,其下的所有加载过的没有对应实例的类信息(保存在permgen)可以被回收)。发现它无法清理ThreadLocal变量中引用的那个class,因为这个class正在被线程池中的线程使用。如果那个class又引用了其他class,那么所有被引用的class将都不能被清理。这样就导致这些类信息在PermGen中发生memory leak了。然后当应用重新load之后,相应的类信息又被新的classloader重新在permGen中加载一遍。在最坏的情况下,应用不停的reload,那么permGen终将被class信息撑爆,造成OOM。
初次发文,不足之处,多多指正。
如有转载,请指明出处。