前言
线程封闭:当访问共享的可变数据时,通常需要同步。一种避免同步的方式就是不共享数据。如果仅在单线程内访问数据,就不需要同步,这种技术称为线程封闭(thread confinement)【摘自《Java并发编程实战》】
实现线程封闭的三种手段:
(1)Ad-hoc线程封闭: 开发者自己实现线程封闭,这种方式严重依赖开发者的水平,最后效果可能会相当的脆弱,因此基本不被推荐;
(2)栈封闭: JVM为我们实现的线程封闭,对应的是JVM区域中的栈区,当一个方法被多个线程调用时,这个方法内部的局部变量在每个线程内都是独立不被共享的,达到了线程封闭的效果,这是JVM的固有特性;
(3)ThreadLocal线程封闭: Java针对线程封闭规范封装的一个关键字,本文就针对这一关键字的实现原理做简单的阅读和分析。
介绍
ThreadLocal,线程私有变量,JDK提供的该工具类可以使得一种类型的对象单独存放在各自的线程里,不与其他线程共享,从而达到线程封闭的效果,因此存放在其中的变量也是线程安全的。
通常情况下,ThreadLocal变量设置为一个全局静态变量,本次学习基于JDK1.8
设计原理
- ThreadLocal内部定义了一个
ThreadLocalMap
静态内部类,ThreadLocalMap内部又维护了一个Entry<T> extends WeakReference<ThreadLocal<?>>
类型的table数组,该Entry类并非HashMap中的Entry键值对,但是具有类似的功能,即键-值存储功能;该Entry类内部有一个Object value
字段,该value存放的就是实际每个线程需要存放的ThreadLocal对象;Entry继承自弱引用类WeakReference
,此设定存在内存泄漏风险,关于这点后面后有说明;
static class ThreadLocalMap {
// 定义一个嵌套类
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
// 存放变量的数组
private Entry[] table;
}
- 同时,
Thread
线程类也维护了一个属性threadLocals:ThreadLocal.ThreadLocalMap threadLocals = null;
该属性的作用就是维护一个Entry数组table,每个元素的referent对象为一个ThreadLocal对象,value为实际需要线程封闭的对象;Thread对象在初始化时,threadlocals属性赋值为null,所以第一次调用时获取到的threadLocals对象必然为null,这个时候就会去初始化这个threadLocals对象,并且赋予初值,如果重写了initialValue
方法就返回重写方法体内构造的对象,否则就默认返回null
;
public class Thread implements Runnable {
// 当前线程占有的线程局部变量值,map由ThreadLocal类维护
ThreadLocal.ThreadLocalMap threadLocals = null;
}
源码解读
- get()方法:获得当前线程的局部变量值。
// 返回当前当前线程局部变量值,如果当前线程的局部变量map还未被初始化,那么执行setInitialValue方法进行初始化
public T get() {
// 首先获取当前线程
Thread t = Thread.currentThread();
// 获取当前线程中维护的ThreadLocal.ThreadLocalMap threadLocals变量
ThreadLocalMap map = getMap(t);
if (map != null) { // threadLocals变量未被初始化
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// threadLocals变量未被初始化
return setInitialValue();
}
get方法流程图:
在执行get方法时,有一个重要的方法也必须被提及,这个方法由ThreadLocal.ThreadLocalMap
来维护,即getEntry()
方法,
/**
* 获取当前ThreadLocal对象对象的entry,如果存在对应的entry,对应的entry会被命中,且该过程将是快速的;
* 如果不存在对应的entry,那么将出发getEntryAfterMiss方法;
*
* @param key ThreadLocal对象
* @return 与传入key关联的Entry对象,不存在则返回null
*/
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);
}
getEntry方法流程图:
- set()方法:为当前线程指定一个局部变量值。
/**
* 为当前线程指定一个局部变量值.
*/
public void set(T value) {
// 获取当前线程
Thread t = Thread.currentThread();
// 获取当前线程维护的ThreadLocal.ThreadLocalMap threadLocals变量
ThreadLocalMap map = getMap(t);
if (map != null) // threadLocals变量已被初始化过则存放值
map.set(this, value);
else // threadLocals变量未被初始化,出发创建流程:createMap
createMap(t, value);
}
set方法流程图:
关于内存泄漏
学习借鉴于(侵删):深入分析 ThreadLocal 内存泄漏问题
-
为什么会内存泄漏?
ThreadLocalMap
内维护的Entry数组映射表,key为ThreadLocal对象本身,value为实际需要被隔离的线程私有对象;由于Entry继承自弱引用类
WeakReference<T>
,使得key的引用为弱引用,弱引用的特性(引用强度比强引用和软引用都更弱,被其关联的对象只能生存到下一次GC之前,JDK1.2之后提供WeakReference来实现软引用),当线程存活时间足够长,直到下一次GC发生,key就会被回收,如果GC后线程依然存活,那么就会存在key为null的Entry.value
,这个value就永远不会被访问到,导致内存泄漏。
- 如何防止内存泄漏?
- ThreadLocal在设计时,加上了一些防护措施,即ThreadLocal的
get()
,set()
,remove()
方法都会清除key为null
的entry; - 在使用完后及时调用
remove()
方法;
-
Entry.key为何不使用强引用?
从内存泄漏的原因分析来看,似乎是因为key使用了弱引用WeakReference,那么key为什么不使用引用强度更强的强引用来避免内存泄漏呢?
我们可以确定的是,ThreadLocalMap的生命周期和Thread一样长,假设key为强引用对象,如果ThreadLocal对象不需要了被回收,但是由于ThreadLocalMap还持有ThreadLocal对象的强引用,是不会被GC回收掉的,而且ThreadLocal自身的机制也不能清除掉这个Entry,除非线程结束或者手动置null
,否则ThreadLocal对象和对应的Entry会一直占用内存也造成内存泄漏;
反之,如果是弱引用,那么至少在下次GC时ThreadLocal对象就会被回收,再辅以ThreadLocal自身的清除机制,整个Entry也会被清除。
总结
-
ThreadLocal
能够实现线程内,局部变量读写的功能(线程封闭); - 其原理,较之HashMap容易理解得多,主要是利用内部定义的
ThreadLocalMap
内部类以及Thread
线程类使用并维护这个内部类共同实现,其中ThreadLocal类并不起到存储的作用,真正实现存储功能的是内部定义的ThreadLocalMap类,ThreadLocal对象作为key来获取对应的局部变量值; - 由于ThreadLocalMap内Entry继承自弱引用
WeakReference<T>
,如果ThreadLocal没有外部引用来引用它,那么GC的时候就会被回收掉;因此使用ThreadLocal存在内存泄漏的风险,所以每次使用完成之后,尽量都调用remove()
方法来避免内存泄漏;