最近看面经经常能看到面试官对ThreadLocal
方面的提问,于是就去翻了翻ThreadLocal
的源码,发现源码并不长,大概看了一通,能看出其中的七七八八,打算借此来梳理一下。PS:HashMap的源码能看明白,看这个源码也不是问题。
一、基本认识 点进源码,看见ThreadLocal
是java.lang
包下的类;在网上也看了看其他人对它的概括,加上自己对看完源码后对ThreadLocal
的理解:
用于线程之间数据的隔离。简单说就是通过ThreadLocal
来开辟一块区间存放数据,这个区间作为线程的本地线程存储,只有当前线程才能获取到这个数据,这个数据对其他线程是不可见的。
可以看到ThreadLocal
的公有构造方法:
1 2 public ThreadLocal () {}
就是用来创建ThreadLocal
对象,没有在其中做其他的工作。
平常用ThreadLocal
最多的两个方法就是set
、get
两个方法;那我们来看一看这两个方法的源码。
二、set方法探析 1 2 3 4 5 6 7 8 public void set (T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null ) map.set(this , value); else createMap(t, value); }
首先在使用set
方法时,能够看到它先通过Thread
的静态方法,来获取到当前线程。这个很关键,因为后面的操作,都是基于当前线程进行的操作,这也是为什么ThreadLocal能够做到线程之间数据隔离的。具体如何做到的,还要继续往下面看。
获取到当前线程后,通过getMap(t)
方法来获取到一个ThreadLocalMap
类型的map
对象;来,点进getMap
方法看看里面是啥:
1 2 3 ThreadLocalMap getMap (Thread t) { return t.threadLocals; }
返回的是当前这个线程的属性——threadLocals
,继续点进:
1 ThreadLocal.ThreadLocalMap threadLocals = null ;
可以看到初始时,这个线程的该属性为null
;现在先不管ThreadLocalMap
具体是什么,只要明白该类可以由实例化的一个线程内部的属性指向ThreadLocalMap的实例即可。
下面的逻辑就比较简单了,如果map
不为null
,就能将value
设置到map
中,可以看见key
是由当前的ThreadLocal
对象作为的key
;而值是由我们传递的value
;如果为null
,则进行createMap
操作,从方法名也能看出,是先创建出这个map
,然后后续再进行赋值的操作。
好了,可以看出,搞明白这个ThreadLocalMap
是个啥,对我们很关键。那么下面我们继续看看ThreadLocalMap
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 static class ThreadLocalMap { private Entry[] table; private static final int INITIAL_CAPACITY = 16 ; static class Entry extends WeakReference <ThreadLocal <?>> { Object value; Entry(ThreadLocal<?> k, Object v) { super (k); value = v; } } 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); } }
这里只列出了该类的部分方法和属性,如果谈到其他的方法和属性,到时再列出来。
首先该类是ThreadLocal
中的静态类,诶,那为什么Thread
也能访问到呢??别忘了,我们开头就提到ThreadLocal
是java.lang
包下的类,当然ThreadLocalMap
同样是,而Thread
也是,所以该类对Thread
具有包可见性。
能够看到它其中一个属性为Entry
继承了WeakReference
,弱引用。也就是说正常情况下,Entry
对象的生命周期只能存活到下一次垃圾收集为止。这里Entry
是作为ThreadLocalMap
中的table
数组,也就是map
用来散列的table
;
通过其构造方法,能够看出去。将table
设置为容量为16,然后通过key
的hash
来计算散列到的位置i
;然后创建Entry
节点设置到table[i]
处,最后设定table
此时的容量为1并设定阈值;设定阈值的方法就不继续看了,就是为了后续方便扩容的判断。到这里大致能看明白ThreadLocalMap
的数据结构大致长什么样了;
再回到上面createMap
方法处:
1 2 3 void createMap (Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this , firstValue); }
创建ThreadLocalMap
,并以当前线程引用这个Map
;同时,当前的ThreadLocal对象作为key,我们传入的value作为Entry节点的value。此时结构如下图所示:
这个时候,我们再看一看当map
已经存在时,调用map.set(this,value)
时的操作:
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 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(); }
其实就是hash
的插入方式,通过hash
计算到索引的位置,也就是table中的位置,只是这里解决哈希冲突的方式是通过线性探查法进行冲突的解决(HashMap中是通过拉链法进行的解决)。
其中整个for
循环的逻辑是,根据hash
函数定位到关键字的位置后,如果当前table[i]
上面存在了元素,那么表明可能发生了冲突或者 就是同一个key
,进入for
循环中,一个一个判断是否插入的key
和冲突的key
相等,如果相等那就直接替换该entry
上的value
;
如果在探查过程中发现table
中的key
为null时:这块逻辑比较复杂,目前读源码理解的是,将过时的entry进行删除,用没有过时的key-value进行覆盖,目的是为了防止在后续线程探查解决冲突的时候,冲突的key在数组中是离散的并不是紧凑的;
最后当探查到一个不存在entry时,表明查找到了应该插入的位置,那么就新建一个entry
节点,并将该entry
节点设置到该位置上。下面是清除过期entry的逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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; }
三、get方法探析 大致明白了set
方法后,再看get
方法会比较轻松。
1 2 3 4 5 6 7 8 9 10 11 12 13 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(); }
同样的获取到当前线程,并且从当前线程中获取到ThreadLocalMap;我们之前看到ThreadLocalMap中set
值时,是将ThreadLocal作为key,所以我们通过将ThreadLocal
传入获取到value
;其中具体的逻辑就不看了,和set
差不多,就是通过关键字定位进行比较,线性探查法那一套逻辑。
四、一些细节 4.1 ThreadLocalMap中的相关参数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 private int size = 0 ; private int threshold; private void setThreshold (int len) { threshold = len * 2 / 3 ; }
4.2 ThreadLocal中的hash问题 我们知道这里的ThreadLocalMap
是通过计算关键字key
的hash
进行散列的,而key
就是不同的ThreadLocal
对象,所以看看系统是如何分配这些散列值的。
1 2 3 4 5 6 7 8 9 10 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静态变量HASH_INCREMENT
;每次实例化ThreadLocal
时,都带调用nextHashCode()
方法,继而调用原子类的getAndAdd()
方法,即:
1 2 3 public final int getAndAdd (int delta) { return unsafe.getAndAddInt(this , valueOffset, delta); }
所以看到是通过cas
操作,给每个ThreadLocal
对象加上固定的值——HASH_INCREMENT -> 0x61c88647
;所以每个ThreadLocal
对象的hash
值是0x61c88647
倍数;
4.3 ThreadLocalMap的扩容操作 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 resize () { Entry[] oldTab = table; int oldLen = oldTab.length; int newLen = oldLen * 2 ; Entry[] newTab = new Entry[newLen]; int count = 0 ; for (int j = 0 ; j < oldLen; ++j) { Entry e = oldTab[j]; if (e != null ) { ThreadLocal<?> k = e.get(); if (k == null ) { e.value = null ; } else { int h = k.threadLocalHashCode & (newLen - 1 ); while (newTab[h] != null ) h = nextIndex(h, newLen); newTab[h] = e; count++; } } } setThreshold(newLen); size = count; table = newTab; }
通过注释也能看出,每次进行两倍的扩容。
五、Thread/ThreadLocal/ThreadLocalMap三者之间的关系 通过一张图进行说明:
上面表明两个线程,Thread-1,Thread-2;创建了三个ThreadLocal对象;每个Thread
都有自己的ThreadLocalMap
;而一个ThreadLocal
对于一个Thread
只能存放一个变量;所以每个线程的ThreadLocalMap
中的key
是不同的ThreadLocal
;所以,看到ThreadLocal
是每个线程可以共用的,但是由于相同的ThreadLocal
是作为每个线程中独有的ThreadLocalMap
中的key
;所以,在不同的线程中,通过ThreadLocal
获取到值不一样;是通过给线程设定独有的ThreadLocalMap
来进行的数据隔离 。
六、简单的例子 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 private static ThreadPoolExecutor pool = (ThreadPoolExecutor) Executors.newFixedThreadPool(16 );@Test public void f1 () throws InterruptedException { ThreadLocal<String> threadLocal1 = new ThreadLocal<>(); ThreadLocal<String> threadLocal2 = new ThreadLocal<>(); new Thread(() -> { threadLocal1.set("张四" ); threadLocal2.set("李五" ); System.out.println(Thread.currentThread().getName()+":" + threadLocal1.get()); System.out.println(Thread.currentThread().getName()+":" + threadLocal2.get()); },"Thread-1" ).start(); Thread.sleep(10 ); new Thread(() -> { System.out.println(Thread.currentThread().getName()+":" + threadLocal1.get()); System.out.println(Thread.currentThread().getName()+":" + threadLocal2.get()); System.out.println("==========" ); threadLocal1.set("张四" ); threadLocal2.set("李五" ); System.out.println(Thread.currentThread().getName()+":" + threadLocal1.get()); System.out.println(Thread.currentThread().getName()+":" + threadLocal2.get()); },"Thread-2" ).start(); } 结果: Thread-1 :张三 Thread-1 :李四 Thread-2 :null Thread-2 :null ========== Thread-2 :张四 Thread-2 :李五
看到在Thread-1
中设置的值,在Thread-2
中并获取不到。
七、ThreadLocalMap中的弱引用 可以看到ThreadLocalMap
中的Entry
节点继承了弱引用引用关系的ThreadLocal
;也就是说ThreadLocalMap
中的key
是弱引用,当他不再关联强引用关系时,在下一次垃圾回收时,便会被回收;但是,其中的value
是具有强引用关系的,因此会出现key
为null
,但是value
不为null
的情况,也就是说entry
节点不为null,而强引用关系的value
,它的生命周期同线程的生命周期,因此如果不手动设置为null,会造成内存泄露,继而会引发内存溢出。
在前面的set
、get
等方法中,也能看到,他们在遍历的时候,如果发现key
为Null
的情况,便会进行清除工作,即将value
设置为null
;
因此[1 ]:
由于Thread中包含变量ThreadLocalMap,因此ThreadLocalMap与Thread的生命周期是一样长,如果都没有手动删除对应key,都会导致内存泄漏。
但是使用弱引用 可以多一层保障:弱引用ThreadLocal不会内存泄漏,对应的value在下一次ThreadLocalMap调用set(),get(),remove()的时候会被清除。
因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。
正确使用方式:
每次使用完ThreadLocal都调用它的remove()方法清除数据;
将ThreadLocal变量定义成private static,这样就一直存在ThreadLocal的强引用,也就能保证任何时候都能通过ThreadLocal的弱引用访问到Entry的value值,进而清除掉 。
八、使用示例 需求:在SpringBoot中自定义一个日志类,用于记录一个接口执行花费的时间。
思路:首先采用注解类,对于标志了注解的接口,统计该接口的一个请求所花费的时间;而对该接口的解析,通过设定过滤器,拦截每一个请求,检查请求的接口是否注解了该接口,如果采用了该注解那么就对该请求进行处理。
注解类:
1 2 3 4 5 6 7 8 9 10 11 import java.lang.annotation.ElementType;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;import java.lang.annotation.Target;@Target (ElementType.METHOD) @Retention (RetentionPolicy.RUNTIME) public @interface MyLog { String desc () default "" ; }
Interceptor:
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 public class MyLogIntercepter extends HandlerInterceptorAdapter { private static final ThreadLocal<Long> startTimeThreadLocal = new ThreadLocal<>(); @Override public boolean preHandle (HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HandlerMethod handlerMethod = (HandlerMethod) handler; Method method = handlerMethod.getMethod(); MyLog myLog = method.getAnnotation(MyLog.class ) ; if (myLog != null ) { long startTime = System.currentTimeMillis(); startTimeThreadLocal.set(startTime); } return true ; } @Override public void postHandle (HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { HandlerMethod handlerMethod = (HandlerMethod) handler; Method method = handlerMethod.getMethod(); MyLog myLog = method.getAnnotation(MyLog.class ) ; if (myLog != null ) { long endTime = System.currentTimeMillis(); Long startTime = startTimeThreadLocal.get(); long optTime = endTime - startTime; String requestURI = request.getRequestURI(); String methodName = method.getDeclaringClass().getName() + "." + method.getName(); String desc = myLog.desc(); System.out.println("请求uri: " + requestURI); System.out.println("请求方法名:" + methodName); System.out.println("方法描述:" + desc); System.out.println("方法执行时间:" + optTime + "ms" ); } } }
添加拦截器到配置中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Configuration public class MyLogAutoConfiguration implements WebMvcConfigurer { @Override public void addInterceptors (InterceptorRegistry registry) { registry.addInterceptor(new MyLogIntercepter()).addPathPatterns("/**" ).excludePathPatterns("/images/**" , "/js/**" , "/css/**" , "/toLogin" , "/login" , "/bootstrap-3.3.7-dist/**" , "/favicon.ico" , "/bizhi.jpg" ); } }
将自定义的过滤器进行注册,并设定拦截的路径;