在我们日常 Java Web 开发中难免遇到需要把一个参数层层的传递到最内层,然后中间层根本不需要使用这个参数,或者是仅仅在特定的工具类中使用,这样我们完全没有必要在每一个方法里面都传递这样一个通用的参数。
而在Java中每一个线程都独享一个ThreadLocal,在接收请求的时候set特定内容,在需要的时候get这个值,恰好可以实现这样的需求。
ThreadLocal主要用来提供线程局部变量,就是为每一个使用该变量的线程都提供一个变量值的副本,这个变量只对当前线程可见,只在线程的生命周期内起作用,每一个线程都可以独立地改变自己的副本,而不会和其它线程的副本冲突。通过ThreadLocal存取的数据,总是与当前线程相关,也就是说,JVM 为每个运行的线程,绑定了私有的本地实例存取空间,从而为多线程环境常出现的并发访问问题提供了一种隔离机制。
概括起来说,对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。
维持线程封闭性的一种更规范的方法就是使用ThreadLocal,这个类能使线程中的某个值与保存的值的对象关联起来。ThreadLocal提供get和set等接口或方法,这些方法为每一个使用这个变量的线程都存有一份独立的副本,因此get总是返回由当前线程在调用set时设置的最新值。
首先我们来看个例子:
123456789101112131415161718192021222324252627 | public final class ConnectionUtil { private ConnectionUtil() {} private static final ThreadLocal<Connection> conn = new ThreadLocal<>(); public static Connection getConn() { Connection con = conn.get(); if (con == null) { try { Class.forName("com.mysql.jdbc.Driver"); con = DriverManager.getConnection("url", "userName", "password"); conn.set(con); } catch (ClassNotFoundException | SQLException e) { // ... } } return con; } public static void closeConnection() { Connection con = conn.get(); if(con!=null){ con.close(); } }} |
每个线程调用getConn()都使用的是同一个Connection,但是每个线程获取到的都是同一个连接的副本,这样就能很好的解决线程安全问题。
ThreadLocal原理
在Java中,每个线程内部都会维护一个自有的ThreadLocalMap ,用来存储本线程私有的数据。
123 | class Thread implements Runnable { ThreadLocal.ThreadLocalMap threadLocals = null;} |
ThreadLocalMap类的定义是在ThreadLocal类中,真正的引用却是在Thread类中,同时ThreadLocalMap中定义了用于存储数据的Entry结构:
123456789 | static class ThreadLocalMap { static class Entry extends WeakReference<ThreadLocal<?>> { Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }} |
可以看到这个Map的key是ThreadLocal类的实例对象,value为用户的值。每次创建一个ThreadLocalMap对象并且设置值的时候,就会往线程内部私有的ThreadLocalMap中保存这个值,key是创建的ThreadLocal对象,value为用户的值。
一个线程可以有多个ThreadLocal对象,每一个ThreadLocal对象是如何区分的呢?
123456 | private final int threadLocalHashCode = nextHashCode();private static AtomicInteger nextHashCode = new AtomicInteger();private static final int HASH_INCREMENT = 0x61c88647;private static int nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT);} |
对于每一个ThreadLocal对象,都有一个final修饰的int型的threadLocalHashCode
属性,对于基本数据类型,可以认为它在初始化后就不可以进行修改,所以可以唯一确定一个ThreadLocal对象。
在ThreadLocal类中,还包含了一个static修饰的AtomicInteger成员变量(即类变量)和一个static final修饰的常量(作为两个相邻nextHashCode的差值)。由于nextHashCode是类变量,所以每一次创建ThreadLocal对象都可以保证nextHashCode被更新到新的值,并且下一次创建ThreadLocal对象时这个被更新的值仍然可用,同时AtomicInteger保证了nextHashCode自增的原子性。
set()方法
1234567891011121314151617181920212223242526272829303132333435363738394041424344 | ThreadLocal: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:private void set(ThreadLocal<?> key, Object value) { Entry[] tab = table; int len = tab.length; /*根据 ThreadLocal 的散列值,查找对应元素在数组中的位置,这里对 len-1 进行了取余操作。 * 之所以能这样取余是因为 len 的值比较特殊,是 2 的 n 次方 */ int i = key.threadLocalHashCode & (len-1); // 使用线性探测法查找元素 for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { ThreadLocal<?> k = e.get(); // ThreadLocal 对应的 key 存在,直接覆盖之前的值 if (k == key) { e.value = value; return; } // key为 null,但是值不为 null,说明之前的 ThreadLocal 对象已经被回收了, // 当前数组中的 Entry 是一个陈旧(stale)的元素 if (k == null) { // 用新元素替换陈旧的元素,这个方法进行了不少的垃圾清理动作,防止内存泄漏 replaceStaleEntry(key, value, i); return; } } // ThreadLocal 对应的 key 不存在并且没有找到陈旧的元素,则在空元素的位置创建一个新的 Entry。 tab[i] = new Entry(key, value); int sz = ++size; // cleanSomeSlot 清理陈旧的 Entry(key == null),如果没有清理陈旧的Entry 并且数组中的元素大于了阈值,则进行 rehash。 if (!cleanSomeSlots(i, sz) && sz >= threshold){ rehash(); }} |
get()方法
1234567891011121314151617181920212223242526272829303132333435363738394041 | ThreadLocal:public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { "unchecked") ( T result = (T)e.value; return result; } } return setInitialValue();}ThreadLocalMap getMap(Thread t) { return t.threadLocals;}ThreadLocalMap: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);}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;} |
因为 ThreadLocalMap 中采用开放定址法,所以当前 key 的散列值和元素在数组中的索引并不一定完全对应。所以在 get 的时候,首先会看 key 的散列值对应的数组元素是否为要查找的元素,如果不是,再调用 getEntryAfterMiss 方法查找后面的元素。
ThreadLocal的内存泄露问题
根据Entry的源码,我们知道ThreadLocalMap是使用ThreadLocal的弱引用作为Key的(关于对象引用方式可以查看我的另一篇文章Java的四种引用方式)。如果一个ThreadLocal没有外部强引用引用他,那么系统gc的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,如果当前线程再迟迟不结束的话,这些key为null的Entry将无法回收,造成内存泄露。
所以JDK建议将ThreadLocal变量定义成private static的,这样的话ThreadLocal的生命周期就更长,由于一直存在ThreadLocal的强引用,所以ThreadLocal也就不会被回收,也就能保证任何时候都能根据ThreadLocal的弱引用访问到Entry的value值,然后remove它,防止内存泄露。
应用场景
ThreadLocal 最常见的使用场景有解决数据库连接、Session管理等。
数据库连接
JDBC连接保存在ThreadLocal对象中,每个对象都有属于自己的连接,代码如下:
12345678 | private static ThreadLocal < Connection > connectionHolder = new ThreadLocal < Connection > () { public Connection initialValue() { return DriverManager.getConnection(DB_URL); }};public static Connection getConnection() { return connectionHolder.get();} |
Session管理
12345678910111213 | 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;} |