java并发编程(二十六)----ThreadLocal的使用
其实ThreadLocal很多接触过多线程的同学都可能会很陌生,他不像current包里面那些耳熟能详的api一样在我们面前经常出现,更多的他作为一个本地类出现在系统设计里面。我们可以说一下Spring,Spring的事务管理器通过AOP切入业务代码,在进入业务代码前,会根据对应的事务管理器提取出相应的事务对象,假如事务管理器是DataSourceTransactionManager,就会从DataSource中获取一个连接对象,通过一定的包装后将其保存在ThreadLocal中。并且Spring也将DataSource进行了包装,重写了其中的getConnection()方法,或者说该方法的返回将由Spring来控制,这样Spring就能让线程内多次获取到的Connection对象是同一个。
为什么要放在ThreadLocal里面呢?因为Spring在AOP后并不能向应用程序传递参数,应用程序的每个业务代码是事先定义好的,Spring并不会要求在业务代码的入口参数中必须编写Connection的入口参数。此时Spring选择了ThreadLocal,通过它保证连接对象始终在线程内部,任何时候都能拿到,此时Spring非常清楚什么时候回收这个连接,也就是非常清楚什么时候从ThreadLocal中删除这个元素
从名字上看我们很容易误解,ThreadLocal,本地线程。local有当地的,本地的,局部的意思,这里说的是局部线程,意思是线程的局部变量。我们知道synchronized是独占锁,同一时间只能有一个线程操作被锁住的代码大家排队等待,典型的以时间换空间的策略。那如果我们空间很足时间不够该怎么办呢,ThreadLocal就该派上用场了。ThreadLocal作为线程的局部变量,会为这个线程创建独立的变量副本,在线程的内部,他所创建的对象相当于全局对象。
说到这里,大家是不是还是没有分清楚ThreadLocal和synchronized有什么区别,下面我们来讲。
- ThreadLocal 不是用来解决共享对象的多线程访问问题的,上面说了ThreadLocal是线程的局部变量。一般情况下,通过ThreadLocal.set() 到线程中的对象是该线程自己使用的对象,其他线程是不需要访问的,也访问不到的。各个线程中访问的是不同的对象。
- ThreadLocal使得各线程能够保持各自独立的一个对象,并不是通过ThreadLocal.set()来实现的,而是通过每个线程中的new 对象 的操作来创建的对象,每个线程创建一个,不是什么对象的拷贝或副本。通过ThreadLocal.set()将这个新创建的对象的引用保存到各线程的自己的一个map中,每个线程都有这样一个map,执行ThreadLocal.get()时,各线程从自己的map中取出放进去的对象,因此取出来的是各自自己线程中的对象,ThreadLocal实例是作为map的key来使用的。
如果ThreadLocal.set()进去的东西本来就是多个线程共享的同一个对象,那么多个线程的ThreadLocal.get()取得的还是这个共享对象本身,还是有并发访问问题。
我们来看一个例子:
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将对应的值取出。上面我们也讲过每个线程进来创建threadSession 的时候,这个threadSession 只属于他一个人所有,别的线程无法共享到他自己创建的ThreadLocal。这就避免了所有线程共享同一个对象的问题。并且该session创建完成之后,我们不必走到哪里都携带着session这个参数,走到哪里传递到哪里。需要使用的时候只需要从threadlocal中取出即可。这也是极其省事的。
我们可以举一个 例子来说明ThreadLocal不是用来解决对象共享访问问题的,而是为了处理在多线程环境中,某个方法处理一个业务,需要递归依赖其他方法时,而要在这些方法中共享参数的问题。例如有方法a(),在该方法中调用了方法b(),而在b方法中又调用了方法c(),即a–>b—>c,如果a,b,c都需要使用用户对象,那么我们常用做法就是a(User user)–>b(User user)—c(User user)。但是如果使用ThreadLocal我们就可以用另外一种方式解决:
- 在某个接口中定义一个静态的ThreadLocal 对象,例如 public static ThreadLocal threadLocal=new ThreadLocal ();
- 然后让a,b,c方法所在的类假设是类A,类B,类C都实现1中的接口
- 在调用a时,使用A.threadLocal.set(user) 把user对象放入ThreadLocal环境
- 这样我们在方法a,方法b,方法c可以在不用传参数的前提下,在方法体中使用threadLocal.get()方法就可以得到user对象。
上面我们说到ThreadLocal的使用,也说了ThreadLocal里面有一个ThreadLocalMap 用于存储当前线程的对象,下面我们简单的看一下源码来理解一下这个过程。先上类图:
ThreadLocal里面有一个内部类ThreadLocalMap,在ThreadLocal内部又装了一个Entry,他继承了WeakReference,我们来看一下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对象其实还是ThreadLocal类型的,这里我们看到ThreadLocal用了一个WeakReference包装是为了保证该ThreadLocal对象在没有被引用的时候能够及时的被gc掉。
下面再看一下ThreadLocal的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);
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
private void set(ThreadLocal key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
...
...
...
}
在set方法中t.threadLocals只要不为空,便创建map对象,我们看到set方法中的key是ThreadLocal,即thread调用ThreadLocal.get()方法既可得到当前thread的threadLocal对象里面的ThreadLocalMap的值!是不是有点绕,是不是不知道为什么当前线程能调用ThreadLocal,我们看一下上面的getMap()方法,返回值是:t.threadLocals,这个t即当前线程,在Thread类里面有一个threadLocals对象,我们可以跟过去看一下,在这里限于篇幅,就只上相关的源码:
public class Thread implements Runnable {
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
Thread parent = currentThread();
if (parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
}
下面方法是ThreadLocal中的:
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
return new ThreadLocalMap(parentMap);
}
我们在源码中看到threadLocals并未进行赋值,他一直都是一个空对象,为什么这么做呢,我们接着看下面的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();
}
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;
}
在get方法中,如果一个线程当前并未使用ThreadLocal对象那么getMap(t)必然是空,那我们就得想了,难道在Thread类中创建一个空对象threadLocals就这么空着?哈哈,当然不是啦,我也着急了。所以就进入了下面的setInitialValue()方法啦,这里的getMap(t)当然还是空的,那进入createMap(t, value)呗:
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
终于在这里拨开云雾见月明!妈妈再也不用担心threadLocals没有人要了!上面分析的比较乱,大家就将就看,用一句话总结那就是:
在Thread类中有一个对象是threadLocals,如果在该线程运行中有ThreadLocal创建threadLocals会去找到他的!获得你在ThreadLocal中存储的值!
上面我们已经详细分析了ThreadLocal的使用和实现,那么在真实的环境中使用它有什么弊端没呢。其实使用中还真的是有很多问题的。
我们知道ThreadLocal是和当前线程绑定的,即他的生命周期是和当前线程共存,当线程结束,ThreadLocal内部的Entity对象才会被gc回收。
下面我说一个场景大家看会带来什么样的后果:如果现在是线程池对象使用了ThreadLocal来保存变量会发生什么?大家知道线程池的主要目的是为了线程复用,那么线程池中的线程基本不会结束,与jvm的生命周期是一致的。那这个时候谁知道一个携带了ThreadLocal的线程会什么时候结束呢。长久以往必然造成内存泄露。
另外我们再说一个关于忘记释放的问题。如果你在线程刚开始进来的时候就载入了ThreadLocal用来保存变量,假设你的程序设计的不是很健壮,你忘记了写remove()。这个时候事情就来了。再假设你在ThreadLocal中存放了map对象,真实的业务中Map对象也许包含了很多数据,随着时间流逝,内存中的无用对象越来越多,内存泄露是必然的。
关于ThreadLocal的内容我们就讲到这里,其实里面还有很多值得我们深究的东西,慢慢一点点的去看吧!