一.ThreandLocal是什么
ThreadLocal叫做线程变量,意思是ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的。早在JDK 1.2的版本中就提供java.lang.ThreadLocal。
ThreadLocal是一个关于创建线程局部变量的类。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。
必须要区分ThreadLocal和Syncronized这种同步机制,两者面向的问题领域是不一样的。sysnchronized是一种互斥同步机制,是为了保证在多线程环境下对于共享资源的正确访问。
而ThreadLocal是提供了一个“线程级”的变量作用域,它是一种线程封闭(每个线程独享变量)技术,
更直白点讲,ThreadLocal可以理解为将对象的作用范围限制在一个线程上下文中,使得变量的作用域为“线程级”。
没有ThreadLocal的时候,一个线程在其声明周期内,可能穿过多个层级,多个方法,如果有个对象需要在此线程周期内多次调用,且是跨层级的(线程内共享),通常的做法是通过参数进行传递;
而ThreadLocal将变量绑定在线程上,在一个线程周期内,无论“你身处何地”,只需通过其提供的get方法就可轻松获取到对象。极大地提高了对于“线程级变量”的访问便利性。
二.ThreandLocal能做什么
1、避免一些参数传递
进行对象跨层传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的约束。
每当我访问一个页面的时候,浏览器都会从硬盘中找到对应的Cookie发送过去。
浏览器是十分聪明的,不会发送别的网站的Cookie过去,只带当前网站发布过来的Cookie过去。
浏览器就相当于我们的ThreadLocal,它仅仅会发送我们当前浏览器存在的Cookie(ThreadLocal的局部变量),
不同的浏览器对Cookie是隔离的(Chrome,Opera,IE的Cookie是隔离的【在Chrome登陆了,在IE你也得重新登陆】),同样地:线程之间ThreadLocal变量也是隔离的….
那上面避免了参数的传递了吗??其实是避免了。Cookie并不是我们手动传递过去的,并不需要写<input name= cookie/>
来进行传递参数。
2、线程间数据隔离
3、进行事务操作,用于存储线程事务信息。
4、数据库连接,Session会话管理。
在学JDBC的时候,为了方便操作写了一个简单数据库连接池,需要数据库连接池的理由也很简单,频繁创建和关闭Connection是一件非常耗费资源的操作,因此需要创建数据库连接池~
那么,数据库连接池的连接怎么管理呢??由ThreadLocal来进行管理。为什么交给它来管理呢??ThreadLocal能够实现当前线程的操作都是用同一个Connection,保证了事务!
三.ThreandLocal原理
ThreadLocal类中有一个Map,用于存储每一个线程的变量副本,Map中元素的键为线程对象,而值对应线程的变量副本,由于Key值不可重复,每一个“线程对象”对应线程的“变量副本”,而到达了线程安全。
我们知道Spring通过各种DAO模板类降低了开发者使用各种数据持久技术的难度。这些模板类都是线程安全的,也就是说,多个DAO可以复用同一个模板实例而不会发生冲突。
我们使用模板类访问底层数据,根据持久化技术的不同,模板类需要绑定数据连接或会话的资源。但这些资源本身是非线程安全的,也就是说它们不能在同一时刻被多个线程共享。
虽然模板类通过资源池获取数据连接或会话,但资源池本身解决的是数据连接或会话的缓存问题,并非数据连接或会话的线程安全问题。
按照传统经验,如果某个对象是非线程安全的,在多线程环境下,对对象的访问必须采用synchronized进行线程同步。
但Spring的DAO模板类并未采用线程同步机制,因为线程同步限制了并发访问,会带来很大的性能损失。
此外,通过代码同步解决性能安全问题挑战性很大,可能会增强好几倍的实现难度。
那模板类究竟仰丈何种魔法神功,可以在无需同步的情况下就化解线程安全的难题呢?答案就是ThreadLocal!
ThreadLocal在Spring中发挥着重要的作用,在管理request作用域的Bean、事务管理、任务调度、AOP等模块都出现了它们的身影,起着举足轻重的作用。
要想了解Spring事务管理的底层技术,ThreadLocal是必须攻克的山头堡垒。
1、源代码分析
/** * 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); //获取当前线程对应的ThreadLocalMap if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); //获取对应ThreadLocal的变量值 if (e != null) return (T)e.value; } return setInitialValue(); //若当前线程还未创建ThreadLocalMap,则返回调用此方法并在其中调用createMap方法进行创建并返回初始值。 } // 是调用当期线程t,返回当前线程t中的一个成员变量threadLocals。 ThreadLocalMap getMap(Thread t) { return t.threadLocals; } // java.lang.Thread类下, 实际上就是一个ThreadLocalMap ThreadLocal.ThreadLocalMap threadLocals = null; /** * 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; } /** * 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); } /** * 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); } /** * 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类中的几个主要的方法,他们的核心都是对其内部类ThreadLocalMap进行操作,下面看一下该类的源代码:
static class ThreadLocalMap { //map中的每个节点Entry,其键key是ThreadLocal并且还是弱引用,这也导致了后续会产生内存泄漏问题的原因。 static class Entry extends WeakReference<ThreadLocal<?>> { Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } /** * 初始化容量为16,以为对其扩充也必须是2的指数 */ private static final int INITIAL_CAPACITY = 16; /** * 真正用于存储线程的每个ThreadLocal的数组,将ThreadLocal和其对应的值包装为一个Entry。 */ private Entry[] table; ///....其他的方法和操作都和map的类似 }
在JDK1.5以后,ThreadLocal已经支持泛型,该类的类名已经变为ThreadLocal<T>。
ThreadLocal<T>类提供了4个可用的方法(基于JDK1.7版本):
(1)void set(T value)设置当前线程的线程本地变量的值。
(2)public T get()该方法返回当前线程所对应的线程局部变量。
(3)public void remove()将当前线程局部变量的值删除。
该方法是JDK 5.0新增的方法,目的是为了减少内存的占用。
需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。
(4)protected T initialValue()返回该线程局部变量的初始值。
该方法是一个protected的方法,显然是为了让子类覆盖而设计的。
这个方法是一个延迟调用方法,在线程第1次调用get()或set(T value)时才执行,并且仅执行1次,ThreadLocal中的缺省实现是直接返回一个null。
可以通过上述的几个方法实现ThreadLocal中变量的访问,数据设置,初始化以及删除局部变量。
总结:
1、每个Thread维护着一个ThreadLocalMap的引用;
2、ThreadLocalMap是ThreadLocal的内部类,用Entry来进行存储;
3、调用ThreadLocal的set()方法时,实际上就是往ThreadLocalMap设置值,key是ThreadLocal对象,值是传递进来的对象;
4、调用ThreadLocal的get()方法时,实际上就是往ThreadLocalMap获取值,key是ThreadLocal对象ThreadLocal本身并不存储值,它只是作为一个key来让线程从ThreadLocalMap获取value。
5、ThreadLocal设计的目的就是为了能够在当前线程中有属于自己的变量,并不是为了解决并发或者共享变量的问题。
四.ThreandLocal使用
启动两个线程,第一个线程中存储的userid为1,第二个线程中存储的userid为2。
测试类:
package com.lzumetal.multithread.threadlocal; import java.math.BigDecimal; public class ThreadLocalTest { public static void main(String[] args) throws InterruptedException { Order order01 = new Order(1, 1, new BigDecimal(10), 1); new Thread(new OrderHandler(1, order01)).start(); Order order02 = new Order(2, 2, new BigDecimal(20), 2); new Thread(new OrderHandler(2, order02)).start(); } }
package com.lzumetal.multithread.threadlocal; public class OrderHandler implements Runnable { private static OrderService orderService = new OrderService(); private Integer userId; private Order order; public OrderHandler(Integer userId, Order order) { this.userId = userId; this.order = order; } @Override public void run() { EnvUtil.getUserIdContext().set(userId); orderService.addOrder(order); orderService.updateStock(order.getGoodId(), order.getGoodCount()); } }
package com.lzumetal.multithread.threadlocal; public class OrderService { /** * 新增订单 * * @param order */ public void addOrder(Order order) { Integer userId = EnvUtil.getUserIdContext().get(); System.out.println(Thread.currentThread().getName() + "新增订单服务中获取用户id-->" + userId); } /** * 更新库存 * * @param goodId * @param goodCount */ public void updateStock(Integer goodId, Integer goodCount) { //虽然更新库存不需要关注userId,但是在这里也一样能够获取到 Integer userId = EnvUtil.getUserIdContext().get(); System.out.println(Thread.currentThread().getName() + "在更新库存中获取用户id-->" + userId); } }
运行结果:
Thread-0新增订单服务中获取用户id-->1 Thread-1新增订单服务中获取用户id-->2 Thread-0在更新库存中获取用户id-->1 Thread-1在更新库存中获取用户id-->2
四.ThreandLocal问题
1、ThreadLocal的内存泄露问题
首先要理解内存泄露(memory leak)和内存溢出(out of memory)的区别。
内存溢出是因为在内存中创建了大量在引用的对象,导致后续再申请内存时没有足够的内存空间供其使用。
内存泄露是指程序申请完内存后,无法释放已申请的内存空间,(不再使用的对象或者变量仍占内存空间)。
根据上面的源码,我们知道ThreadLocalMap是使用ThreadLocal的弱引用作为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
永远无法回收,造成内存泄露。
只有当前thread结束以后, Thread Ref就不会存在栈中,强引用断开, Thread, ThreadLocalMap, Entry将全部被GC回收。
但如果是线程对象不被回收的情况,比如使用线程池,线程结束是不会销毁的,就可能出现真正意义上的内存泄露。
ThreadLocalMap设计时的对上面问题的对策:
当仔细读过ThreadLocalMap的源码,可以推断,如果在使用的ThreadLocal的过程中,显式地进行remove是个很好的编码习惯,这样是不会引起内存泄漏。
那么如果没有显式地进行remove呢?只能说如果对应线程之后调用ThreadLocal的get和set方法都有很高的概率会顺便清理掉无效对象,断开value强引用,从而大对象被收集器回收。
但无论如何,我们应该考虑到何时调用ThreadLocal的remove方法。
一个比较熟悉的场景就是对于一个请求一个线程的server如tomcat,在代码中对web api作一个切面,存放一些如用户名等用户信息,在连接点方法结束后,再显式调用remove。