ThreadLocal源码 ThreadLocal可以实现每个线程的隔离,自定义线程级别的变量,使用如下:
1 private static final ThreadLocal<SimpleDateFormat> formatter = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMdd HHmm" ));
可以使用 get()
和 set()
方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题。
ThreadLocal使用了ThreadLocalMap去存取值,而每个Thread类都有自己的ThreadLocalMap,ThreadLocalMap底层使用的是 Entry[] table,用来保存每个ThreadLocal对应的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 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 static class ThreadLocalMap { static class Entry extends WeakReference <ThreadLocal <?>> { Object value; Entry(ThreadLocal<?> k, Object v) { super (k); value = v; } } 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(); } }
ThreadLocal本身的hashcode作为数组下标,value作为值,存在ThreadLocalMap的Entry[]中,可以保证多线程安全
ThreadLocal内存泄漏 Entry的Key是ThreadLocal,它是弱引用的
如果一个对象只具有弱引用,那就类似于可有可无的生活用品。 弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。 在垃圾回收器线程扫描它 所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。 不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。 弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。
一旦ThreadLocal没有被外部引用,那么垃圾回收就会回收掉Key(ThreadLocal),这样就会导致ThreadLocalMap中key为null, 而value还存在着强引用,只有thead线程退出以后,value的强引用链条才会断掉。 但如果当前线程迟迟不结束的话,这些key为null的Entry的value就会一直存在,造成内存泄漏
Thread Ref -> Thread -> ThreadLocalMap -> Entry -> value
防止内存泄漏的措施
每次使用完ThreadLocal都调用它的remove()方法清除数据
线程池中使用ThreadLocal的问题 ThreadLocal是使用Thread中的ThreadLocalMap进行存储的,如果都是新线程使用起来没有问题,但是如果是线程池,还是会 有一些问题的,下面看一段代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 private static final ThreadLocal currentUser = ThreadLocal.withInitial(() -> null );@GetMapping("wrong") public Map wrong (@RequestParam("userId") Integer userId) { String before = Thread.currentThread().getName() + ":" + currentUser.get(); currentUser.set(userId); String after = Thread.currentThread().getName() + ":" + currentUser.get(); Map result = new HashMap(); result.put("before" , before); result.put("after" , after); return result; }
这里面每次请求,第一次获取的值都会是null吗? 其实是不一定的,可能before会有值! 原因是Tomcat使用了线程池,线程池会重用固定的几个线程,一旦线程重用,那么很可能首次从ThreadLocal
获取的值是之前其他用户的请求遗留的值。 这时,ThreadLocal 中的用户信息就是其他用户的信息。
解决方案 在代码运行结束,需要显示的清除
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @GetMapping("right") public Map right (@RequestParam("userId") Integer userId) { String before = Thread.currentThread().getName() + ":" + currentUser.get(); currentUser.set(userId); try { String after = Thread.currentThread().getName() + ":" + currentUser.get(); Map result = new HashMap(); result.put("before" , before); result.put("after" , after); return result; } finally { currentUser.remove(); } }
这样这个线程运行结束对应的ThreadLocal数据也清除了