JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal为解决多线程程序的并发问题提供了一种新的思路。使用这个工具类可以很简洁地编写出优美的多线程程序。也是面试中出现频率比较高的知识点。ThreadLocal望文生义即表示的是一个线程中的局部变量,对比我们自己编写业务类中的私有变量,在对象实例化后,类的私有属性就是实例对象的局部变量,在实例对象的整个生命周期都可以随时获取这些私有变量。那么ThreadLocal和我们自己编写业务类的私有变量有什么本质区别吗?实际上没有区别,ThreadLocal的中存储的线程局部变量,最终也是Thread类中的一个局部变量(类似于Map的数据结构),生命周期和Thread实例一样。只不过JDK通过ThreadLocal做了一层封装,并对外提供了更简便的API。下面就来看看ThreadLocal的实现原理,它到底是做了什么事情。
ThreadLocal的应用
ThreadLocal是一个本地线程副本变量工具类。主要用于将私有线程和该线程存放的副本对象做一个映射,各个线程之间的变量互不干扰,在高并发场景下,可以实现无状态的调用,特别适用于各个线程依赖不通的变量值完成操作的场景。因此,ThreadLocal 不是用来解决共享对象的多线程访问问题的,一般情况下,通过ThreadLocal.set() 到线程中的对象是该线程自己使用的对象,其他线程是不需要访问的,也是访问不到的。通过ThreadLocal.set()保存到当前线程实例的ThreadLocalMap对象中,ThreadLocal实例是作为map的key来使用的。 下面来看一个hibernate中典型的ThreadLocal的应用:
private static final ThreadLocal threadSession = new ThreadLocal();
public static Session getSession() throws InfrastructureException {
Session s = (Session) threadSession.get();
try {
if (s == null) {
s = getSessionFactory().openSession();
threadSession.set(s);
}
} catch (HibernateException ex) {
throw new InfrastructureException(ex);
}
return s;
}
由于在数据库操作中,很多地方会用到Session会话,而且操作都是在同一个线程中。所以hibernate把Session存入到ThreadLocal中,以便在其他方法获取Session实例时,直接通过ThreadLocal获取。这里如果不用ThreadLocal方式,该怎么做呢?最直接的方式,就是把需要用到Session对象的方法多加一个Session类型的入参。但这个种方式很麻烦,而且还有一个不好的地方。因为框架封装了数据库的缘故,我们编写的dao层代码是通过AOP嵌入到hibernate中执行的,因此我们编写dao层的代码就可能需要加上Session参数而我们又完全用不上这个参数,这会让开发者感到莫名其妙。hibernate利用ThreadLocal得到了一个很完美的设计思路。不仅仅是hibernate,在其他许多框架中都可以看到ThreadLocal的身影
ThreadLocal的原理
话不多说了吧,直接上源码,主要来看一下ThreadLocal的get和set方法,这两个方法是最常用的
/**
* Returns the value in the current thread's copy of this
* thread-local variable. If the variable has no value for the
* current thread, it is first initialized to the value returned
* by an invocation of the {@link #initialValue} method.
*
* @return the current thread's value of this thread-local
*/
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();
}
/**
* Sets the current thread's copy of this thread-local variable
* to the specified value. Most subclasses will have no need to
* override this method, relying solely on the {@link #initialValue}
* method to set the values of thread-locals.
*
* @param value the value to be stored in the current thread's copy of
* this thread-local.
*/
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,我们看一下getMap的实现
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
在getMap方法中,通过Thread对象直接获取了threadLocals属性,再来看一下Thread类中的threadLocals属性
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
注意threadLocals没有关键字修饰,表示它是default的,表示该属性只允许在同一个包中进行访问。到这里就明白了,在每个Thread线程被创建的时候,线程对象本身就包含一个ThreadLocalMap类型的属性,用来存储线程局部变量。我们再来看一看ThreadLocalMap是个什么数据结构,其实看名字也能知道它是个Map。但它和HashMap有一点区别,就是对于哈希冲突的处理方式。ThreadLocalMap实现更简单轻巧,但不适合存储大量的数据。后面会再详细说这一点。下面是运行时ThreadLocal的数据结构图
线程运行时,内部保存着一个Map的数据结构,而Map的Key是固定的ThreadLocal类型,值就是存储我们自己想存储的数据,图中用Object类型表示。也就是说 ThreadLocal本身并不存储值,它只是作为一个 key来让线程从 ThreadLocalMap获取 value。看明白了这个图,就明白了ThreadLocal的工作原理了。
哈希冲突
前面说过ThreadLocalMap实现方式简单,不适合存储大量的数据,那么ThreadLocalMap是怎么实现的呢,它是怎么解决哈希冲突的。这就是需要深入看一下set方法的源码
/**
* 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;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
到这里看着和HashMap类似,也定义了一个Entry数组。首先通过当前key的哈希码和数组长度,计算出当前key应该存储在数组中的下标位置,继续看下面for循环,这个逻辑很简单,从数组中获取刚刚算出的下标位置的元素,如果为空就直接把key和value存储进去,如果不为空就出现哈希碰撞了,它是如何解决的。重点在于nextIndex方法。继续看nextIndex方法的源码。
/**
* Increment i modulo len.
*/
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
是不是很震惊,这个方法竟然如此的简单。就是把下标往后移一位,然后返回,再继续刚刚的循环。由此可见,ThreadLocalMap处理哈希碰撞的方式就是把下标往后移一位。直到能放进去为止。这也是它为什么不适合存储大量数据的原因,数据一多出现哈希碰撞的概率就大,如果出现大量的哈希碰撞,那么数组就会特别长。同样在get方法的时候,会先根据key计算出下标,再比较key是否相等。如果不相等就从计算出的下标开始往后遍历数组。源码如下
/**
* Get the entry associated with key. This method
* itself handles only the fast path: a direct hit of existing
* key. It otherwise relays to getEntryAfterMiss. This is
* designed to maximize performance for direct hits, in part
* by making this method readily inlinable.
*
* @param key the thread local object
* @return the entry associated with key, or null if no such
*/
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
/**
* Version of getEntry method for use when key is not found in
* its direct hash slot.
*
* @param key the thread local object
* @param i the table index for key's hash code
* @param e the entry at table[i]
* @return the entry associated with key, or null if no such
*/
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)
return e;
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
getEntry就是从数组中获取Entry实例,当出现key不相等时会进入else,也就是getEntryAfterMiss方法。从这个方法可以看出,出现哈希碰撞就会遍历数组了。如果看过HashMap源码的朋友,看这个代码应该不在话下,逻辑非常简单明了。
内存泄漏
ThreadLocal会出现内存泄漏,其原因出现在Entry中的Key是一个弱引用。源码如下
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是ThreadLocalMap的静态内部类。从代码中可以看出Entry的Key是WeakReference包装的,是一个弱引用类型。什么是弱引用,在Java中有4中引用:强引用,弱引用, 虚引用, 软引用。这里只说弱引用,我们平时写代码基本使用的都是强引用。
// 强引用
String str = "hello";
// 弱引用
WeakReference<String> weak = new WeakReference<String>(new String("hello"));
// 主动触发GC
System.gc();
弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。因此一个对象如果只有弱引用,那么它能存活到下一次GC之前。在ThreadLocalMap的设计中,Key使用的是弱引用,那么在运行时,引用链如下:
ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,那么系统 GC 的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。其实,ThreadLocalMap的设计中已经考虑到这种情况,也加上了一些防护措施:在ThreadLocal的get(),set(),remove()的时候都会清除线程ThreadLocalMap里所有key为null的value,当然这只是一种防护措施,JDK建议将ThreadLocal变量定义成private static的,这样的话ThreadLocal的生命周期就更长,由于一直存在ThreadLocal的强引用,所以ThreadLocal也就不会被回收,也就能保证任何时候都能根据ThreadLocal的弱引用访问到Entry的value值,然后remove它,防止内存泄露。