一、引言 ThreadLocal 是一个 Java 提供的线程局部变量类。它的主要作用是为每个线程提供独立的变量副本,使得每个线程都可以独立地修改自己所拥有的变量副本,而不会影响其他线程的副本
二、源码分析 1.ThreadLocalMap ThreadLocalMap是实现ThreadLocal的根本所在,因此分析ThreadLocalMap的代码即可了解ThreadLocal的实现原理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 static class ThreadLocalMap { static class Entry extends WeakReference <ThreadLocal<?>> { Object value; Entry(ThreadLocal<?> k, Object v) { super (k); value = v; } } private static final int INITIAL_CAPACITY = 16 ; private Entry[] table; private int size = 0 ; private int threshold; private void setThreshold (int len) { threshold = len * 2 / 3 ; } ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) { table = new Entry [INITIAL_CAPACITY]; int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1 ); table[i] = new Entry (firstKey, firstValue); size = 1 ; setThreshold(INITIAL_CAPACITY); } private ThreadLocalMap (ThreadLocalMap parentMap) { Entry[] parentTable = parentMap.table; int len = parentTable.length; setThreshold(len); table = new Entry [len]; for (int j = 0 ; j < len; j++) { Entry e = parentTable[j]; if (e != null ) { @SuppressWarnings("unchecked") ThreadLocal<Object> key = (ThreadLocal<Object>) e.get(); if (key != null ) { Object value = key.childValue(e.value); Entry c = new Entry (key, value); int h = key.threadLocalHashCode & (len - 1 ); while (table[h] != null ) h = nextIndex(h, len); table[h] = c; size++; } } } } }
2.set方法 1 2 3 4 5 6 7 8 9 public void set (T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null ) { map.set(this , value); } else { createMap(t, value); } }
这个方法十分简单,直接传进来要存储的value,然后获得ThreadLocalMap放进去即可
但是要注意,这里调用了两个其他方法,分析下这两个方法
1 2 3 ThreadLocalMap getMap (Thread t) { return t.threadLocals; }
这里调用了线程的threadLocals参数,这个参数如何定义的呢?在Thread类里面,如下
1 ThreadLocal.ThreadLocalMap threadLocals = null ;
即每个线程都有一个ThreadLocalMap类型的threadLocals变量,用于存储threadLocal值
所以set操作,最后其实都是放在了线程内,即拿到当前操作的线程,获取线程内部的threadlocals对象,将threadlocal作为key,存储进threadlocals里面去
假如线程的threadlocals对象尚未初始化,则会调用下述方法,来进行ThreadLocalMap对象的创建
1 2 3 void createMap (Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap (this , firstValue); }
最终是使用ThreadLocalMap的构造器创建了对象,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) { table = new Entry [INITIAL_CAPACITY]; int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1 ); table[i] = new Entry (firstKey, firstValue); size = 1 ; setThreshold(INITIAL_CAPACITY); }
下面就是真正的set方法实现的地方,上面调用了map .set(this,value),最终调用的是ThreadLocalmap内的set方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 private void set (ThreadLocal<?> key, Object value) { Entry[] tab = table; int len = tab.length; int i = key.threadLocalHashCode & (len-1 ); for (Entry e = tab[i]; e != null ; e = tab[i = nextIndex(i, len)]) { ThreadLocal<?> k = e.get(); if (k == key) { e.value = value; return ; } if (k == null ) { replaceStaleEntry(key, value, i); return ; } } tab[i] = new Entry (key, value); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }
这里介绍下replaceStaleEntry方法,它内部调用了cleanSomeSlots方法,而在cleanSomeSlots内部又调用了expungeStaleEntry方法,那么这三个方法有什么用呢?
首先说回threadLocalMap中数据的存储方法,是采用Entry数组进行的存储,面对冲突,采用的开放地址法解决冲突,这种方法实现简单,就是不断往后遍历,直到找到空位置,插入即可;但是也存在很大的问题,那就是当一些数据失效时,本来连续的数据就会存在空缺,这时候再进行查询,查到空位就会停止了,因此需要提供一个策略,保证失效数据被清除后能够重整数据位置,而上面的这三个方法,就是实现这个清理过程的
首先看expungeStaleEntry方法,传入的staleSlot对应位置的key为null
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 private int expungeStaleEntry (int staleSlot) { Entry[] tab = table; int len = tab.length; tab[staleSlot].value = null ; tab[staleSlot] = null ; size--; Entry e; int i; for (i = nextIndex(staleSlot, len); (e = tab[i]) != null ; i = nextIndex(i, len)) { ThreadLocal<?> k = e.get(); if (k == null ) { e.value = null ; tab[i] = null ; size--; } else { int h = k.threadLocalHashCode & (len - 1 ); if (h != i) { tab[i] = null ; while (tab[h] != null ) h = nextIndex(h, len); tab[h] = e; } } } return i; }
经过上面的源码分析,可以知道expungeStaleEntry方法主要是什么作用了,它主要是重新调整key为null后面那些key的位置,保证能够不出现空缺
由于在table表中,staleSlot位置的key没了,那么之前通过开放地址法经过这个key,往后排的那些key都不能再被查到了,因此需要调整它后面的数据位置,走到第一个key为null的位置即可,具体参考下图
下面再看cleanSomeSlots方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 private boolean cleanSomeSlots (int i, int n) { boolean removed = false ; Entry[] tab = table; int len = tab.length; do { i = nextIndex(i, len); Entry e = tab[i]; if (e != null && e.get() == null ) { n = len; removed = true ; i = expungeStaleEntry(i); } } while ( (n >>>= 1 ) != 0 ); return removed; }
所以它本质上就是遍历了
一遍数组,清理并调整了数组中的对象,将无效value清除,重新调整了其他有效数据的位置
再看replaceStaleEntry方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 private void replaceStaleEntry (ThreadLocal<?> key, Object value, int staleSlot) { Entry[] tab = table; int len = tab.length; Entry e; int slotToExpunge = staleSlot; for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null ; i = prevIndex(i, len)) if (e.get() == null ) slotToExpunge = i; for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null ; i = nextIndex(i, len)) { ThreadLocal<?> k = e.get(); if (k == key) { e.value = value; tab[i] = tab[staleSlot]; tab[staleSlot] = e; if (slotToExpunge == staleSlot) slotToExpunge = i; cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); return ; } if (k == null && slotToExpunge == staleSlot) slotToExpunge = i; } tab[staleSlot].value = null ; tab[staleSlot] = new Entry (key, value); if (slotToExpunge != staleSlot) cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); }
所以replaceStaleEntry方法就是一个把value放入table,并且清理无效key调整元素位置的方法,但是这个方法只在插入时,走到了一个key为null但是有value的地方时才会调用
3.get方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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(); }
如何查询的呢?如下
1 2 3 4 5 6 7 8 9 10 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); }
向后查找
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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 ; }
三、总结 3.1 应用 ThreadLocal有哪些应用呢?
它的应用环境主要是线程私有一些数据的情况,如建立连接后保持连接信息—前后端,后端与数据库的连接都算,具体如下示例
日期格式化工具:在多线程环境下,SimpleDateFormat 是非线程安全的,可以使用 ThreadLocal 存储每个线程的 SimpleDateFormat 实例
数据库连接管理:通过 ThreadLocal 可以为每个线程分配独立的数据库连接,确保线程之间的数据库连接独立性
Spring 中的事务管理:Spring 使用 ThreadLocal 来管理事务,在同一个线程中的方法调用之间共享事务状态
同一个线程中的业务处理需要共享数据:比如用户登录信息等
3.2 内存泄漏 首先要明白,ThreadLocal为什么把key设置成弱引用类型?
因为实际上,key-value是存储在ThreadLocalMap中的,而每个线程都有一个ThreadLocalMap类型的参数threadLocals,如果key是强引用,那么基本上除非手动指定remove,不然这个key对应的位置永远会被占用,这就造成了所说的内存泄漏问题
那么key设置成弱引用,就一定能避免内存泄漏吗?
也不一定,因为key虽然会在gc时被回收掉,但是value还存在呀,如果只是gc了,那value没被清除掉,依然会造成内存泄漏
为什么value不会被gc回收掉?
因为它是强引用类型的,实际上是存储在Entry数组上的,数组有指向它的强引用
那么如何回收,保证不会内存泄漏呢?
为了防止内存泄漏,ThreadLocal在get和set方法都能够清除无效的key对应的无效value,但是如果一直没调用这些方法,也会导致value存储在table中无法清除,造成内存泄漏
因此使用threadLocal之后,要保留调用一下remove方法的习惯
3.3 小结 由上面的分析可知,threadLocal主要依赖ThreadLocalMap来实现各种功能,数据也是包装成Entry类型存储在数组之中
而ThreadLocalMap采用的是开放地址法解决哈希冲突,若是某位置key失效了,清除后可能会对其他Entry对象的查找造成问题,所以ThreadLocalMap提供了cleanSomeSlots和expungeStaleEntry方法配合使用,可以清除key失效的无效槽,并且能够调整剩余的Entry到正确的位置