背景图

Java中ThreadLocal学习

在java中,并发的操作包中有copyOnWrite,但是这个类不是用来处理数据隔离的,也就是不能达到保持线程上下文的问题,但是ThreadLocal类可以。这里我们简单介绍一下,就不做更多阐述了。以下代码是Java8的实现。

ThreadLocalMap

这个类就是ThreadLocal里面存放数据使用的数据结构了。不用我说大家都知道这个类是单例模式的,key呢就是ThreadLocal,也就是说ThreadLocal本身是多例程序,是的就是这样,因为Thread是通过TheadLocal获取的ThreadLocalMap里面的值。

这个Map没有继承Map,而且是利用数组进行的实现,比较简单。特别需要说明的就是hash冲突的问题,一旦发生hash冲突就会从当前位置一步一步向下找空余的地方放置新值,找的时候如果没找到就从hash位置开始一个一个向下找。以前在学习数据结构时候就比较好奇这种解决hash键冲突的方式,当时还是觉得这种方式比较不靠谱(主要当前基本都是使用C和C++,特别喜欢使用链表),今天看到这个应用觉得也还是不错的解决之道。

关于Hash计算就是ThreadLocal中的threadLocalHashCode & (length-1)计算出来。

关于初始化和扩容:默认创建16个元素,满三分之二后进行扩容。

每次数据遍历访问都会进行key的判断,如果元素已经失效就会进行删除,但是这不能完全解决ThreadLocal的内存泄露问题。

Thread

在Thread里面有两个属性,系统退出时会设置为null,用于释放资源。

1
2
3
// 包内访问的元素。
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

看一下exit方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private void exit() {
if (group != null) {
group.threadTerminated(this);
group = null;
}
/* Aggressively null out all reference fields: see bug 4006245 */
target = null;
/* Speed the release of some of these resources */
threadLocals = null;
inheritableThreadLocals = null;
inheritedAccessControlContext = null;
blocker = null;
uncaughtExceptionHandler = null;
}

关于ThreadLocal

其实这个类只是起着链接ThreadLocalMap和Thread的链接访问而已。具体可以查看代码实现。这里就不做更多的说明了。

SuppliedThreadLocal

java8新的类,这里就是将ThreadLocal对象封装在supplier里面,防止为null而已,实现也比较简单。

关于内存泄露的问题

我们来看一下ThreadLocalMap中Entry的数据结构,发现是一个弱引用。

1
2
3
4
5
6
7
8
9
10
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;

// key为ThreadLocal(这个需要深入学习一下)
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}

ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,那么系统 GC 的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。

其实,ThreadLocalMap的设计中已经考虑到这种情况,也加上了一些防护措施:在ThreadLocal的get(),set(),remove()的时候都会清除线程ThreadLocalMap里所有key为null的value。

但是这些被动的预防措施并不能保证不会内存泄漏:

使用static的ThreadLocal,延长了ThreadLocal的生命周期,可能导致的内存泄漏(参考ThreadLocal 内存泄露的实例分析)。
分配使用了ThreadLocal又不再调用get(),set(),remove()方法,那么就会导致内存泄漏。

为什么使用弱引用

为了应对非常大和长时间的用途,哈希表使用弱引用的 key。

ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。

那么如何解决这个问题呢?

解决

向Entry中注册ReferenceQueue,利用一个线程定期清除这个Queue里面的被垃圾回收的数据就可以了。但是多付出了一个线程的开销。

参考和引用

深入分析 ThreadLocal 内存泄漏问题

0%