ThreadLocal
发布日期:2021-05-06 17:45:28 浏览次数:30 分类:原创文章

本文共 4213 字,大约阅读时间需要 14 分钟。

1 ThreadLocal提供了线程本地变量,它可以保证访问到的变量属于当前线程,每个线程都保存有一个变量副本,每个线程的变量都不同。
2 ThreadLocal相当于提供了一种线程隔离,将变量与线程相绑定。
3 ThreadLocal本身不存储任何值,存在当前线程的ThreadLocalMap中
4 类似 Map 的结构存储变量。每个Thread里面都有一个ThreadLocal.ThreadLocalMap成员变量,也就是说每个线程通过ThreadLocal.ThreadLocalMap与ThreadLocal相绑定。

1 ThreadLocal

//获取值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();}//初始化private T setInitialValue() {       T value = initialValue();    Thread t = Thread.currentThread();    ThreadLocalMap map = getMap(t);    if (map != null)        map.set(this, value);    else        createMap(t, value);    return value;}//绑定map到当前线程//设置值public void set(T value) {       Thread t = Thread.currentThread();    ThreadLocalMap map = getMap(t);    if (map != null)        map.set(this, value);    else        createMap(t, value);}//获取线程关联的ThreadLocalMapThreadLocalMap getMap(Thread t) {       return t.threadLocals;}//创建ThreadLocalMapvoid createMap(Thread t, T firstValue) {       t.threadLocals = new ThreadLocalMap(this, firstValue);}

1.1 直接调用,没有set,返回值

返回空,如果重写了 intValue(),则返回这个对象。

2ThreadLocalMap

ThreadLocal.ThreadLocalMap内部类,维护着每个线程自己的多个ThreadLocal变量, key为当前的threadLocal实例(ThreadLocal本身不存值,只是引用),value为设置的值 (值存放在ThreadLocal.ThreadLocalMap.Entry实例中)。关键定义如下:

static class ThreadLocalMap {   		//存储数据的数组  	private Entry[] table;		//获取Entry,key的类型为ThreadLocal    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);    }  }        /**         * Set the resize threshold to maintain at worst a 2/3 load factor.         */        private void setThreshold(int len) {               threshold = len * 2 / 3;        }

Entry类是ThreadLocalMap的静态内部类,用于存储数据。key为ThreadLocal本身的弱引用,值为set进来的值。

static class Entry extends WeakReference<ThreadLocal<?>> {       / The value associated with this ThreadLocal. */    Object value;    Entry(ThreadLocal<?> k, Object v) {           super(k);        value = v;    }}

Entry类继承了WeakReference<ThreadLocal<?>>,即每个Entry对象都有一个ThreadLocal本身的弱引用(作为key),这是为了防止内存泄露。一旦线程结束,key变为一个不可达的对象,这个Entry就可以被GC了。

3 内存泄漏的原因

1 Entry的key是个WeakReference弱引用的ThreadLocal对象,会被垃圾回收,回收后,key会变为null,但value还存在,而又无法通过已经变为null的key索引到value,因此value所在内存便无法使用,又无法回收,导致内存泄露。
2 ThreadLocalMap(即位于Thread中的变量)threadLocals本身的生命周期与线程一致,即使ThreadLocal本身弱引用已经回收,但value还存在于ThreadLocalMap中的Entry中,导致内存泄露。

4 如果绑定了ThreadLocal的线程,提交到线程池?

线程,一般不会消亡,ThreadLocalMap,相当一个全局变量,ThreadLocal则会被反复创建,导致内存泄漏。

5 如何避免泄漏

1、使用完线程共享变量后,显示调用ThreadLocalMap.remove方法清除线程共享变量;
既然Key是弱引用,那么我们要做的事,就是在调用ThreadLocal的get()、set()方法时完成后再调用remove方法,将Entry节点和Map的引用关系移除,这样整个Entry对象在GC Roots分析后就变成不可达了,下次GC的时候就可以被回收。

2、JDK建议ThreadLocal定义为private static(只有一个实例不会反复创建对象),这样ThreadLocal的弱引用问题则不存在了。

总结

每个 Thread 里都含有一个 ThreadLocalMap 的成员变量,这种机制将 ThreadLocal 和线程巧妙地绑定在了一起,即可以保证无用的 ThreadLocal 被及时回收,不会造成内存泄露,又可以提升性能。假如我们把 ThreadLocalMap 做成一个 Map<t extends Thread, ?> 类型的 Map,那么它存储的东西将会非常多(相当于一张全局线程本地变量表),这样的情况下用线性探测法解决哈希冲突的问题效率会非常差。而 JDK 里的这种利用 ThreadLocal 作为 key,再将 ThreadLocalMap 与线程相绑定的实现,完美地解决了这个问题。

什么时候无用的 Entry 会被清理:
1 Thread 结束的时候
2 插入元素时,发现 staled entry,则会进行替换并清理
3 插入元素时,ThreadLocalMap 的 size 达到 threshold,并且没有任何 staled entries 的时候,会调用 rehash 方法清理并扩容
4 调用 ThreadLocalMap 的 remove 方法或set(null) 。

尽管不会造成内存泄露,但是可以看到无用的 Entry 只会在以上四种情况下才会被清理,这就可能导致一些 Entry 虽然无用但还占内存的情况。因此,我们在使用完 ThreadLocal 后一定要remove一下,保证及时回收掉无用的 Entry。

特别地,当应用线程池的时候,由于线程池的线程一般会复用,Thread 不结束,这时候用完更需要 remove 了。

1 那key为什么不使用强引用?
如果key使用强引用,即使调用threadLocalA = null,此时线程中threadLocalMap中仍然持有threadLocal实例的引用,threadLocalA实例仍 然不会被GC回收,造成异常情况

2 value为什么不使用弱引用?
value只存在thread引用->堆区thread实例->threadLocalMap->entryTable->entry->value这一条引用链,假设value为弱引用,则GC后会被回收,再也无法通过ThreadLocal.get()方法获取value值

总的来说,对于多线程资源共享的问题,同步机制采用了 以时间换空间 的方式,而 ThreadLocal 则采用了 以空间换时间 的方式。前者仅提供一份变量,让不同的线程排队访问;而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。

上一篇:java线程安全的集合
下一篇:线程池体系(二)-- ForkJoinPool

发表评论

最新留言

哈哈,博客排版真的漂亮呢~
[***.90.31.176]2025年03月25日 12时20分40秒