ThreadLocal是Java框架中经常使用的工具。对于这个知识点,网上博文毛毛多,但有不少都存在一些错误。不过知识就是这样不断建立,发现问题,打破重建螺旋上升的过程。在此记录一下我的认识过程:
第一层级:初识
ThreadLocal顾名思义,线程局部变量。
因此ThreadLocal是线程独占而非处理多线程同步问题的。这在一些博文中有误解。
ThreadLocal就是在使用该对象的每一个线程中创建独立的副本,多个线程彼此之间是隔离的,实际上操作的是不同对象。
示例
public class ThreadLocalDemo{
private static ThreadLocal<Integer> seqNum = new ThreadLocal<Integer>(){
public Integer initialValue(){
return 0;
}
};
public int getNextNum(){
seqNum.set(seqNum.get() + 1);
return seqNum.get();
}
public static void main(String[] args){
ThreadLocalDemo sn = new ThreadLocalDemo();
TestClient t1 = new TestClient(sn);
TestClient t2 = new TestClient(sn);
TestClient t3 = new TestClient(sn);
TestClient t4 = new TestClient(sn);
t1.start();
t2.start();
t3.start();
t4.start();
}
private static class TestClient extends Thread {
private ThreadLocalDemo sn;
public TestClient(ThreadLocalDemo sn){
this.sn = sn;
}
public void run(){
for(int i=0;i<3;i++){
System.out.println("thread[" + Thread.currentThread().getName() + "] --> sn[" + sn.getNextNum() + "]");
}
}
}
}
输出结果:
thread[Thread-0] --> sn[1]
thread[Thread-0] --> sn[2]
thread[Thread-2] --> sn[1]
thread[Thread-1] --> sn[1]
thread[Thread-1] --> sn[2]
thread[Thread-1] --> sn[3]
thread[Thread-3] --> sn[1]
thread[Thread-3] --> sn[2]
thread[Thread-2] --> sn[2]
thread[Thread-0] --> sn[3]
thread[Thread-2] --> sn[3]
thread[Thread-3] --> sn[3]
多提一句,可以发现代码没有引入任何其他类库,原来ThreadLocal和Thread都是java.lang包下的类,已经自动加载了。
源码分析
ThreadLocal的实现在java.lang.ThreadLocal类中;
ThreadLocal有一个内部类ThreadLocalMap;
Thread类中有一个threadLocals变量,类型是ThreadLoacl.ThreadLocalMap。
很多博文混淆不清就是因为不能理清楚ThreadLocal,ThreadLocalMap,Thread之间的关系。
##ThreadLocal
1,ThreadLocal是个泛型类:
public class ThreadLocal<T> {
2,ThreadLocal的域:
ThreadLocal只有三个域,
private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode =
new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;
配合一个方法:
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
做的事情就是实现一个ThreadLocal的哈希值threadLocalHashCode,这个哈希值在ThreadLocalMap中用到。
3,ThreadLocal的方法:
Thread一共四个基本方法:
(1) void set(Object value)设置当前线程的线程局部变量的值。
(2) public Object get()该方法返回当前线程所对应的线程局部变量。
(3) public void remove()将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是JDK 5.0新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。
(4) protected Object initialValue()返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次,ThreadLocal中的缺省实现直接返回一个null。
基本方法都是通过封装ThreadLocalMap的方法实现的,在看过了ThreadLocal源码后更好理解。
ThreadLocalMap
1, ThreadLocalMap没有继承任何Map,而是单独实现了一个Map功能。如果理解HashMap源码的话再看ThreadLocalMap会比较轻松。
2, ThreadLocalMap元素存储:
ThreadLocalMap定义了一个内部类Entry来存储元素:
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,值是一个Object。
有了Entry后,ThreadLocalMap持有一个Entry数组table来存储元素,和HashMap类似。
private Entry[] table;
3,ThreadLocalMap元素操作:
作为一个Map,自然要有get,set操作,在这里就用到了上面ThreadLocal中说到的threadLocalHashCode。以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);
}
可知ThreadLocalMap是使用threadLocalHashCode来做元素定位的。
Thread
Thread类和ThreadLocal相关的就是它的一个域:
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
源码已经说明了,这个域的管理是ThreadLocal来做的,而Thread不操作它,唯一与之发生关系的就是线程退出方法exit():
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;
}
把threadLocals置为空,这在下节的内存泄露问题还将提到。
再看ThreadLocal
之前我们看过Thread的大框知道了ThreadLocal的四个基本方法,也说明了基本方法是调用ThreadLocalMap实现的,那么接下来看如何实现的:
首先看获取和创建ThreadLocalMap的方法:
getMap
/**
* Get the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @param t the current thread
* @return the map
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
可知,是根据线程,获取线程的threadLocals域,但threadLocals域默认为空,所以有了创建方法createMap:
/**
* Create the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @param t the current thread
* @param firstValue value for the initial entry of the map
*/
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
创建方法,发现在创建同时还放入了一个元素firstValue,由此可以推测,基本方法应该要利用getMap和createMap配合条件来实现的,让我们一探究竟:
get
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();
}
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
我们看到其中根据线程操作threadLocalMap,再根据ThreadLocal本身操作对象的过程。
总结:
一个ThreadLocal
实例一方面本身代表着一个键,代表着一个指定类型<T>
的变量,另一方面拥有着根据键,操作线程存储和获取这个变量的方法。
每个线程Thread都拥有一个ThreadLocalMap
表,里面可以以键值对形式存不同类型的变量,其中键是ThreadLocal
类型,值是ThreadLocal<T>
的T类型。
概念之所以比较容易混淆,是因为在ThreadLocal中放了太多的东西,如果我来实现,将ThreadLocal和ThreadLocalMap分离,再将ThreadLocal的类型定义功能和线程操作功能分开,会更好理解。但写在一起可以做到更好的封装,应该会有利于安全性吧。
内存泄露问题
ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,那么系统 GC 的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreadLocalMap -> Entry -> value永远无法回收,造成内存泄漏。所以,如果这个线程对象被gc回收,这条引用链断裂,就不会出现内存泄露。但在threadLocal设为null和线程结束这两个时间点之间,value不会被回收掉,就发生了我们认为的内存泄露。比如使用线程池的时候,线程结束是不会销毁的,会再次使用的,就可能出现内存泄露。
其实,ThreadLocalMap的设计中已经考虑到这种情况,也加上了一些防护措施:在ThreadLocal的get(),set(),remove()的时候都会清除线程ThreadLocalMap里所有key为null的value。
但是这些被动的预防措施并不能保证不会内存泄漏:
使用static的ThreadLocal,延长了ThreadLocal的生命周期,可能导致的内存泄漏。
分配使用了ThreadLocal又不再调用get(),set(),remove()方法,那么就会导致内存泄漏。
ThreadLocal 最佳实践
综合上面的分析,我们可以理解ThreadLocal内存泄漏的前因后果,那么怎么避免内存泄漏呢?
每次使用完ThreadLocal,都调用它的remove()方法,清除数据。
在使用线程池的情况下,没有及时清理ThreadLocal,不仅是内存泄漏的问题,更严重的是可能导致业务逻辑出现问题。所以,使用ThreadLocal就跟加锁完要解锁一样,用完就清理。
再深入
看似理解,但纸上得来终觉浅,遇到问题还是会发现模棱两可。对于遇到的问题,记录如下,适时更新。
1,ThreadLocalMap存储的时间键值对,那么键是什么,值是什么?
答:键是ThreadLocal实例,值是对应线程的变量副本。
详细说明:ThreadLocalMap把ThreadLocal实例作为键,而在实际方法中,是获取ThreadLocal的哈希值threadLocalHashCode。在复制一次源码:
private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode =
new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
仔细分析,threadLocalHashCode是final的,在ThreadLocal实例初始化是通过方法nextHashCode()计算得来,初始化后不可变。
而nextHashCode是static,是静态变量,再看nextHashCode()方法,也是static的,操作nextHashCode自增固定长度。所以每多一个ThreadLocal实例,nextHashCode就会增加,保证了每两个ThreadLocal之间的threadLocalHashCode都不样,这样把它作为键值对的键就可行了。
思考一下threadLocalHashCode有没有可能重复,答案是很难,当ThreadLocal实例很多,可能超过int值范围,这样可能会转回来,使两个哈希值相同。但这样的情况微乎其微,首先这需要有相当多的ThreadLocal实例,其次每次递增的步长HASH_INCREMENT可能也是有讲究的(我猜测,不确定)。所以此事不足为虑。