想必不少小伙伴们对ThreadLocal
并不陌生,ThreadLocal
叫作线程本地变量,也就是ThreadLocal为变量在每一个线程中都建立了一个副本,每一个线程能够访问本身内部的副本变量。那么,咱们使用ThreadLocal必定线程安全么?话很少说,先上结论:java
若是threadlocal.get以后的副本,只在当前线程中使用,那么是线程安全的;若是对其余线程暴露,不必定是线程安全的。git
为了演示下错误的使用方式,先看下以下代码(虽然小伙伴们都不会这样写代码 ^_^):github
static class Container { int num; } public static void main(String[] args) throws InterruptedException { ThreadLocal<Container> tl = new ThreadLocal<>(); tl.set(new Container()); // 先set下ThreadLocal Container container = tl.get(); Runnable task = () -> { for (int i = 0; i < 10000; i++) { container.num++; } }; Thread t1 = new Thread(task); Thread t2 = new Thread(task); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(tl.get().num); }
笔者的一次结果输出为:17581数组
结合代码,咱们知道,在执行threadlcoal.get
获取到线程变量副本以后,不要让其余线程来访问它了,不然就是多线程操做同一个变量,可能形成线程安全问题。安全
除了上述讨论的ThreadLocal线程安全性问题以外,ThreadLocal若是使用不当,可能存在内存泄露问题。ThreadLocal变量是保存在Thread.threadLocals
中(ThreadLocalMap类型)以Entry类型保存的,其中Entry.key(也就是弱引用referent实际指向对象)为ThreadLocal变量,该变量为弱类型;Entry.value为实际set的value。多线程
// Entry,里面保存在ThreadLocal变量,也就是key,是弱引用 static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }
虽然Entry.referent是弱类型,指向ThreadLocal变量,可是若是ThreadLocal变量自己引用不置为null的话,这里的Entry.referent指向对象是不会释放的。好比咱们经常使用的定义方式:app
// 静态变量和对象属性 static ThreadLocal<String> tls = new ThreadLocal<>(); ThreadLocal<Integer> tli = new ThreadLocal<>();
相似于静态变量和对象属性这种引用,若是不将tls或tli设置为null,那么ThreadLocal变量没法释放(这不是废话么,人家但是强引用呀),此时的Entry.referent弱类型没啥卵用;只有在tls或tli为null时,Entry.referent弱类型就起做用了,在第一次GC时就会将Entry.referent弱类型指向的对象回收。性能
若是Entry.referent弱类型指向的对象回收了(没调用ThreadLocal.remove操做),Entry.value对象还在,而且Entry.value但是强引用的,此时就发生了内存泄露。这也就是ThreadLocal使用不当(没调用ThreadLocal.remove)时产生的内存泄漏问题。不过,伴随着其余ThreadLocal对象的set/get/remove
的进行,会清除一部分Entry.referent为null可是Entry.value不为null的对象的,也就是修复内存泄露问题,注意,这个只是清除部分这样的Entry,并不能保证一次就能清除所有这样的Entry,因此仍是要遵循ThreadLocal.set,用完以后就remove。优化
讨论完了ThreadLocal的潜在问题以后,你是否是意犹未尽,想深刻了解下ThreadLocal实现原理
?OK,那就搬起小板凳,一块儿唠唠吧~this
ps:若是小伙伴对ThreadLocal原理已经熟悉了,那么恭喜你,后面的内容能够不看了~
ThreadLocal变量主要有get/set/remove
三个操做,理解了这三个操做流程,基本上就理解了ThreadLocal实现原理。
get流程以下:
/** * 当前线程的threadLocalMap中获取当前ThreadLocal对应的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; } } // 设置null值,下次直接返回null了 return setInitialValue(); } /** * 若是一次找到了entry,直接返回;不然就是set时hash冲突了 * 遍历后续的slot,进行查找 * 这里其实JDK能够作个优化,在set以后,将slot位置记录在Threadlocal变量中,下次直接到对应slot位置get便可 */ 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); }
注意:线程的threadLocals是一个基于开放定址法实现的map结构。
INITIAL_CAPACITY = 16
,默认扩容阈值为threshold = INITIAL_CAPACITY * 2 / 3
,扩容时按照成倍扩容。/** * 获取当前线程的threadLocalMap,非空直接set value; * 不然新建一个包含value的threadLocalMap。 * threadLocalMap的key对应程序中定义的ThreadLocal变量,value对应要set的值 */ public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); // Thread.threadLocals if (map != null) map.set(this, value); else createMap(t, value); } // Entry,里面保存在ThreadLocal变量,也就是key,是弱引用 static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } /** * hash码的生成,这里全部的ThreadLocal对象hash生成都是基于static变量nextHashCode来作的 * 建立ThreadLocal对象时threadLocalHashCode已初始化完成 */ private final int threadLocalHashCode = nextHashCode(); private static int nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT); } private static AtomicInteger nextHashCode = new AtomicInteger(); /** * 当前线程的threadLocalMap非空直接set value */ private void set(ThreadLocal<?> key, Object value) { Entry[] tab = table; int len = tab.length; int i = key.threadLocalHashCode & (len-1); // 若是当前table[i] hash冲突,那么就以i为起点,遍历后续table[i], // 这其实就是hash冲突中的开放定址法,另一种是分离连接法 for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { ThreadLocal<?> k = e.get(); // key已存在,更新vlaue便可 if (k == key) { e.value = value; return; } // key为null,复制value便可 if (k == null) { replaceStaleEntry(key, value, i); return; } } // 新建Entry,清理一部分Entry.key为null,value不为null的数据,避免内存泄露 // 超过了threshold时rehash操做 tab[i] = new Entry(key, value); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }
/** * 从ThreadLocalMap删除对应key */ public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) m.remove(this); } private void remove(ThreadLocal<?> key) { 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)]) { if (e.get() == key) { // 清除Entry.key弱引用,设置为null e.clear(); // 清除Entry.value引用,可能还涉及部分key为null的Entry数据清理 expungeStaleEntry(i); return; } } }
小结
从ThreadLocal的get/set
操做流程来看,ThreadLocal的value 是 Lazy Init(延迟初始化的)
。ThreadLocal为何是延迟初始化,这个问题应该是容易理解的,缘由是:在没有具体业务场景前提下,这样的作法避免内存浪费。
ThreadLocal变量默认放在基于开放定址法实现的map结构中,这种结构在hash冲突时会形成屡次get/set
操做,理论上能够经过记录ThreadLocal变量set时的位置,这样下次直接经过该位置获取对应value便可,能够参考netty的FastThreadLocal
,它的实现思路就是这样的,提升了set/get的效率。
最后来一张ThreadLocal的总体图:
参考资料:
一、https://luoxn28.github.io/2019/04/27/ni-de-threadlocal-yi-ding-xian-cheng-an-quan-ma/