理解和使用ThreadLocal类

一、从数据结构入手

  下图为ThreadLocal的内部结构图

  理解和使用ThreadLocal类

  从上面的机构图,可以窥见ThreadLocal的核心机制:

  • 每个Thread线程内部都有一个Map;
  • Map里面存储线程本地对象(key)和线程的变量副本(value)
  • 但是,Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程的变量值

  所以对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰。

 

二、深入解析ThreadLocal

  ThreadLocal类提供如下几个核心方法:

public T get()
public void set(T value)
public void remove()
protected T initialValue() { }
  • get()方法用于获取当前线程的变量副本
  • set()方法用户保存当前线程的变量副本
  • initialValue()是一个protected方法,一般是用来在使用时进行重写的,它是一个延迟加载方法,用于为当前线程初始变量副本值
  • remove()方法移除当前线程的副本变量

  get()方法

 /**
* 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)
return (T)e.value;
}
return setInitialValue();
}

  第一句取得当前线程,然后通过getMap(t)方法获取到一个map,map的类型为ThreadLocalMap。然后接着下面获取到<key,value>键值对Entry节点,注意这里获取Entry传进去的是this,而不是当前线程t

  如果获取成功,则返回value值。

  如果map为空,则调用setInitialValue方法返回value。

  我们对上面的每一句仔细分析:

  首先看一下getMap方法中做了什么:

    /**
* Get the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @param t the current thread
* @return the map
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}

  可能大家没有想到的是,在getMap中,是调用当前线程t,返回当前线程t中的一个成员变量threadLocals

  那么我们继续取Thread类中去看一下成员变量threadLocals是什么:

 public class Thread implements Runnable {

     /* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
}

  实际上就是一个ThreadLocalMap,这个类型是ThreadLocal类的一个内部类,我们继续去看ThreadLocalMap的实现:

    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;
}
}
}

  可以看到ThreadLocalMap的内部类Entry继承了WeakReference,并且使用ThreadLocal作为键值。

  然后再继续看setInitialValue()方法的具体实现:

     /**
* Variant of set() to establish initialValue. Used instead
* of set() in case user has overridden the set() method.
*
* @return the initial value
*/
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}

  很容易了解,就是如果map不为空,就设置键值对,为空,再创建Map,看一下createMap的实现:

     /**
* Create the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @param t the current thread
* @param firstValue value for the initial entry of the map
* @param map the map to store.
*/
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}

  至此,我们可以明白ThreadLocal是如何为每个线程创建变量的副本的:

  首先,在每个线程Thread内部有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals,这个threadLocals就是用来存储实际的变量副本的,key为当前ThreadLocal变量,value为变量副本(即T类型的变量)。

  初始时,在Thread里面,threadLocals为空,当通过ThreadLocal变量调用get()方法或者set()方法,就会对Thread类中的threadLocals进行初始化,并且以当前ThreadLocal变量为键值,以ThreadLocal要保存的副本变量为value,存到threadLocals。

  然后在当前线程里面,如果要使用副本变量,就可以通过get方法在threadLocals里面查找

  set()方法

 /**
* 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);
}

  获取当前线程的成员变量map,若map非空,则重新将ThreadLocal和新的value副本放入到map中。

  若map空,则对线程的成员变量ThreadLocalMap进行初始化创建,并将ThreadLocal和value副本放入map中。

  remove()方法

 /**
* Removes the current thread's value for this thread-local
* variable. If this thread-local variable is subsequently
* {@linkplain #get read} by the current thread, its value will be
* reinitialized by invoking its {@link #initialValue} method,
* unless its value is {@linkplain #set set} by the current thread
* in the interim. This may result in multiple invocations of the
* <tt>initialValue</tt> method in the current thread.
*
* @since 1.5
*/
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}

  将当前ThreadLocal对应的value副本从map中移除。

  

三、ThreadLocalMap

  ThreadLocalMap是ThreadLocal的内部类,没有实现Map接口,用独立的方式实现了Map的功能,其内部的Entry也独立实现。

理解和使用ThreadLocal类

  在ThreadLocalMap中,也是用Entry来保存K-V结构数据的。但是Entry中key只能是Threadlocal对象,这点被Entry的构造方法已经限定死了

 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(弱引用,生命周期只能存活到下次GC前),但只有Key是弱引用类型的,Value并非弱引用

  ThreadLocalMap的成员变量:

 static class ThreadLocalMap {
/**
* The initial capacity -- MUST be a power of two.
*/
private static final int INITIAL_CAPACITY = 16; /**
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/
private Entry[] table; /**
* The number of entries in the table.
*/
private int size = 0; /**
* The next size value at which to resize.
*/
private int threshold; // Default to 0
}

四、Hash冲突怎么解决

  和HashMap的最大不同在于,ThreadLocalMap结构非常简单,没有next引用,也就是说ThreadLocalMap中解决Hash冲突的方式并非链表的方式,而是采用线性探测的方式,所谓线性探测,就是根据初始Key的hashcode值确定元素在table数组中的位置,如果发现这个位置上已经有其它Key值的元素被占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置

  ThreadLocalMap解决Hash冲突的方式就是简单的步长加1或减1,寻找下一个相邻的位置。

 /**
* Increment i modulo len.
*/
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
} /**
* Decrement i modulo len.
*/
private static int prevIndex(int i, int len) {
return ((i - 1 >= 0) ? i - 1 : len - 1);
}

  显然ThreadLocalMap采用线性探测的方式解决Hash冲突的效率很低,如果有大量不同的ThreadLocal对象放入map中时发生冲突,或者发生二次冲突,则效率很低。

  所以这里引出的良好建议是:每个线程只存放一个变量,这样的话所有的线程存放到map中的Key都是相同的ThreadLocal,如果一个线程要保存多个变量,就需要创建多个ThreadLocal,多个ThreadLocal放入Map中时会极大地增加Hash冲突的可能

五、ThreadLocalMap的问题

  由于ThreadLocalMap的Key是弱引用,而Value是强引用。这就导致一个问题,ThreadLocal在没有外部对象强引用时,发生GC时弱引用Key会被回收,而Value不会回收,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄漏。

  如何避免泄漏

  既然Key是弱引用,那么我们要做的事,就是在调用ThreadLocal的get()、set()方法时,完成后再调用remove()方法,将Entry节点和Map的引用关系移除,这样整个Entry对象在GC Roots分析后就变成不可达了,下次GC的时候就可以被回收。

  如果使用ThreadLocal的get方法之后,没有显式地调用remove()方法,就有可能发生内存泄漏,所以养成良好的变成习惯十分重要,使用完ThreadLocal之后,记得调用remove()方法。如:

ThreadLocal<Session> threadLocal = new ThreadLocal<Session>();
try {
threadLocal.get();
// 其它业务逻辑
} finally {
threadLocal.remove();
}

六、ThreadLocal的应用场景

  最常见的ThreadLocal的使用场景为用来解决数据库连接、Session管理等。

  如:

 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;
}

  可以看到,在getSession()方法中,首先判断当前线程中有没有放进去session,如果还没有,那么通过sessionFactory().openSession()来创建一个session,再将session set到线程中,实际上放到当前线程ThreadLocalMap这个map中,这时,对于这个session的唯一引用就是当前线程中的那个ThreadLocalMap,而threadSession作为这个值的Key,要取得这个session可以通过threadSession.get()来得到,里面执行的操作实际上是先取得当前线程的ThreadLocalMap,然后将threadSession作为key将对应的值取出。这个session相当于线程的私有变量。显然,其它线程中是取不到这个session的,他们也只能取到自己的ThreadLocalMap中的东西。

  总之,ThreadLocal不是用来解决对象共享访问问题的,而主要是提供了保持对象的方法和避免参数传递的方便的对象访问方式

  ThreadLocal的应用场合,我觉得最适合的是对同一个线程调用的多个方法*享了某一个变量(如上面的session),这个变量需要传递到多个方法中(如上的session在service层和dao层的很多方法中都要用到),这样传来传去太麻烦了,这时可以采用ThreadLocal

  这里还有一个在Java Web中使用ThreadLocal的例子可供学习:《优雅的使用ThreadLocal

七、总结

  ThreadLocal并不是为了解决线程安全问题,而是提供了一种将实例绑定到当前线程的机制,类似于隔离的效果,实际上自己的方法中new出来变量也能达到类似的效果。ThreadLocal跟线程安全基本不搭边,绑定上去的实例也不是多线程共用的,而是每个线程new一份,这个实例肯定不是共用的,如果共用了,那就会引发线程安全问题。ThreadLocal最大的用处是用来把实例变量共享成全局变量,在线程的任何方法中都可以访问到该实例变量而已。网上很多人说ThreadLocal是解决了线程安全问题,其实是望文生义,两者不是同类问题。

  

补充:ThreadLocal类型变量为何声明为静态static?

  Java中每个线程都有与之关联的Thread对象,Thread对象中有一个ThreadLocal.ThreadLocalMap类型的成员变量,该变量是一个Hash表,所以每个线程都单独维护这样一个Hash表,当ThreadLocal类型对象调用 set()方法时,这个set()方法会使用当前线程维护的Hash表,把自己(ThreadLocal)作为Key,相应的值作为value插入到Hash表中。由于每个线程维护的Hash表是独立的,因此在不同的Hash表中,key值即使相同也是没问题的

  如果把ThreadLocal对象声明为非静态的,则当包含ThreadLocal类声明的类去产生一个实例时,都会产生一个ThreadLocal的新对象,这是毫无意义的,只是增加了内存消耗

上一篇:Mabits简单应用 2017.8.3


下一篇:Mathtype使用技巧