深入理解ThreadLocal
JDK 1.2的版本中就提供了java.lang.ThreadLocal,ThreadLocal为解决多线程程序的并发问题提供了一种新的思路。使用这个工具类可以很简洁地编写出优美的多线程程序,ThreadLocal并不是一个Thread,而是专属于某个Thread的局部变量集。
上面提到“ThreadLocal为解决多线程程序的并发问题提供了一种新的思路”,那么,我们就先来看下多线程并发时带来的线程安全问题的解决思路。
synchronized同步机制是解决多线程并发问题的一个重要思路。synchronized利用锁的机制,使临界区【指的是一个访问共用资源(如共用设备或是共用存储器)的程序片段;临界区维护的共用资源被称为临界资源】在某一时刻只能被一个线程访问到,从而保障临界区的线程安全。因此,synchronized用于线程间的数据共享,通过锁机制确保某一时刻只能有一个获得锁的线程进入临界区操作临界资源。
可以说,ThreadLocal在处理并发问题上采取了与synchronized截然不同的解决方案。按照Java官方解释,ThreadLocal provides thread-local variables,也就是说,ThreadLocal提供了线程的本地变量,这个变量里面的值(通过get方法获取)是和其他线程分割开来的,变量的值只有当前线程能访问到。实际上,ThreadLocal为每个线程提供了都需要访问的变量的副本,这样的话,每个线程访问的并非同一个对象,从而隔离了多个线程对数据的共享访问,实现了线程间的数据隔离,既然线程间各自访问各自专属的变量,自然也就不会涉及多线程访问共享数据时带来的线程安全问题了。
通过上面的阐述,总结:synchronized在多线程访问临界区时,采用锁机制保障临界资源的线程安全;ThreadLocal则是为每个线程提供一个变量副本(相当于每个线程的局部变量),线程们各自操作自己的“局部变量”即可,互不干扰,也就是说,每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
那么,ThreadLocal都有哪些使用场景呢?大名鼎鼎的Spring框架里就用到了ThreadLocal。
Spring的事务管理器通过AOP切入业务代码,在进入业务代码前,会依据相应的事务管理器提取出相应的事务对象,假如事务管理器是DataSourceTransactionManager,就会从DataSource中获取一个连接对象,通过一定的包装后将其保存在ThreadLocal中。这样的话,Spring就能让线程内多次获取到的Connection对象是同一个。Spring为什么要把一个Coonection连接对象放在ThreadLocal里面呢?这是因为,Spring通过ThreadLocal保证连接对象始终在线程内部,不论什么时候都能拿到。因此,Spring就可以实现对这个连接对象的控制。
那么,ThreadLocal是如何做到为每一个线程维护变量的副本的呢?在ThreadLocal类中有一个内部静态Map,用于存储每一个线程的变量副本,Map中元素的键为线程对象,而值对应线程的变量副本。
static class ThreadLocalMap { ... }
ThreadLocal最常见的操作就是set、get、remove三个操作:
public class ThreadLocal<T> { ... public T get() {... } public void set(T value) {... } public void remove() {... } protected T initialValue() {... } ... static class ThreadLocalMap { ... static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } ... } }
get()方法是用来获取ThreadLocal在当前线程中保存的变量副本,set()用来设置当前线程中变量的副本,remove()用来移除当前线程中变量的副本,initialValue()是一个protected方法,一般是用来在使用时进行重写的,它是一个延迟加载的方法。
我们来看ThreadLocal是如何为每个线程创建变量的副本的:
首先,在每个线程Thread内部有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals,这个threadLocals就是用来存储实际的变量副本的,key为当前ThreadLocal变量,value为变量副本(即T类型的变量)。
public class Thread implements Runnable {
... ThreadLocal.ThreadLocalMap threadLocals = null;
... }
初始时,在Thread里面,threadLocals为空,当通过ThreadLocal变量调用get()方法或者set()方法,就会对Thread类中的threadLocals进行初始化,并且以当前ThreadLocal变量为键值,以ThreadLocal要保存的副本变量为value,存到threadLocals。
public class ThreadLocal<T> { ... public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); } ... 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(); } ... }
然后在当前线程里面,如果要使用副本变量,就可以通过get方法在threadLocals里面查找。
一般来说,ThreadLocal最常见的使用场景为:用来解决数据库连接、Session管理等。案例:
package com.itszt.test8; /** * ThreadLocal */ public class ThreadLocalTest { /** * 声明两个ThreadLocal实例 * 一个用来存储long型值,即所依附线程的id * 一个用来存储String型对象,即所依附线程的name */ ThreadLocal<Long> longLocal = new ThreadLocal<Long>(); ThreadLocal<String> stringLocal = new ThreadLocal<String>(); /** * ThreadLocal实例位于哪个线程t下, * 就把该线程的成员属性t.threadLocals赋值一个ThreadLocalMap实例, * 接着把当前的ThreadLocal对象作为该Map的key键, * 把longLocal.set(Object val)传入的val作为该Map的key键对应的value值 * 这样的话,当前线程下就拥有了一个ThreadLocal.ThreadLocalMap作为局部变量 */ public void set() { longLocal.set(Thread.currentThread().getId()); stringLocal.set(Thread.currentThread().getName()); } /** * 下述两个方法,可以获取当前线程下相应的数据 * @return */ public long getLong() { return longLocal.get(); } public String getString() { return stringLocal.get(); } /** * main是主线程 * @param args * @throws InterruptedException */ public static void main(String[] args) throws InterruptedException { final ThreadLocalTest test = new ThreadLocalTest(); test.set();//给主线程下的两个ThreadLocal中存储数据 System.out.println(Thread.currentThread().getName()+"线程编号--"+test.getLong()); System.out.println(Thread.currentThread().getName()+"名称--"+test.getString()); Thread thread1 = new Thread("haha"){ public void run() { test.set();//给当前子线程下的两个ThreadLocal中存储数据 System.out.println(Thread.currentThread().getName()+"线程编号--"+test.getLong()); System.out.println(Thread.currentThread().getName()+"名称--"+test.getString()); }; }; thread1.start();//启动子线程 thread1.join();//让父线程(此处为主线程)等待子线程结束之后,父线程才能继续运行 System.out.println(Thread.currentThread().getName()+"线程编号--"+test.getLong()); System.out.println(Thread.currentThread().getName()+"名称--"+test.getString()); } }
控制台打印结果:
main线程编号--1 main名称--main haha线程编号--9 haha名称--haha main线程编号--1 main名称--main
可见,在多线程环境下运用ThreadLocal后,每个线程下都有了自己“局部变量”,从而使得线程间互不干扰,实现了线程安全。