为了解释ThreadLocal类的工作原理,必须同时介绍与其工作甚密的其他几个类
首先,在Thread类中有一行:
/* ThreadLocal values pertaining to this thread. This map is maintained by the ThreadLocal class. */ ThreadLocal.ThreadLocalMap threadLocals = null;
其中ThreadLocalMap类的定义是在ThreadLocal类中,真正的引用却是在Thread类中。同时,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; } }
从中我们可以发现这个Map的key是ThreadLocal类的实例对象,value为用户的值,并不是网上大多数的例子key是线程的名字或者标识。ThreadLocal的set和get方法代码:
public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, 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; } } return setInitialValue(); }
其中的getMap方法:
ThreadLocalMap getMap(Thread t) { return t.threadLocals; }
给当前Thread类对象初始化ThreadlocalMap属性:
void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }
到这里,我们就可以理解ThreadLocal究竟是如何工作的了
public class Son implements Cloneable{ public static void main(String[] args){ Son p=new Son(); System.out.println(p); Thread t = new Thread(new Runnable(){ public void run(){ ThreadLocal<Son> threadLocal = new ThreadLocal<>(); System.out.println(threadLocal); threadLocal.set(p); System.out.println(threadLocal.get()); threadLocal.remove(); try { threadLocal.set((Son) p.clone()); System.out.println(threadLocal.get()); } catch (CloneNotSupportedException e) { e.printStackTrace(); } System.out.println(threadLocal); }}); t.start(); } }
输出:
Son@7852e922 java.lang.ThreadLocal@3ffc8195 Son@7852e922 Son@313b781a java.lang.ThreadLocal@3ffc8195
也就是如果把一个共享的对象直接保存到ThreadLocal中,那么多个线程的ThreadLocal.get()取得的还是这个共享对象本身,还是有并发访问问题。 所以要在保存到ThreadLocal之前,通过克隆或者new来创建新的对象,然后再进行保存。
ThreadLocal的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用。作用:提供一个线程内公共变量(比如本次请求的用户信息),减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度,或者为线程提供一个私有的变量副本,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响。
如何实现一个线程多个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对象,都有一个final修饰的int型的threadLocalHashCode不可变属性,对于基本数据类型,可以认为它在初始化后就不可以进行修改,所以可以唯一确定一个ThreadLocal对象。
但是如何保证两个同时实例化的ThreadLocal对象有不同的threadLocalHashCode属性:在ThreadLocal类中,还包含了一个static修饰的AtomicInteger([əˈtɒmɪk]提供原子操作的Integer类)成员变量(即类变量)和一个static final修饰的常量(作为两个相邻nextHashCode的差值)。由于nextHashCode是类变量,所以每一次调用ThreadLocal类都可以保证nextHashCode被更新到新的值,并且下一次调用ThreadLocal类这个被更新的值仍然可用,同时AtomicInteger保证了nextHashCode自增的原子性。
为什么不直接用线程id来作为ThreadLocalMap的key?
这一点很容易理解,因为直接用线程id来作为ThreadLocalMap的key,无法区分放入ThreadLocalMap中的多个value。比如我们放入了两个字符串,你如何知道我要取出来的是哪一个字符串呢?
而使用ThreadLocal作为key就不一样了,由于每一个ThreadLocal对象都可以由threadLocalHashCode属性唯一区分或者说每一个ThreadLocal对象都可以由这个对象的名字唯一区分(下面的例子),所以可以用不同的ThreadLocal作为key,区分不同的value,方便存取。
public class Son implements Cloneable{ public static void main(String[] args){ Thread t = new Thread(new Runnable(){ public void run(){ ThreadLocal<Son> threadLocal1 = new ThreadLocal<>(); threadLocal1.set(new Son()); System.out.println(threadLocal1.get()); ThreadLocal<Son> threadLocal2 = new ThreadLocal<>(); threadLocal2.set(new Son()); System.out.println(threadLocal2.get()); }}); t.start(); } }
ThreadLocal的内存泄露问题
根据上面Entry方法的源码,我们知道ThreadLocalMap是使用ThreadLocal的弱引用作为Key的。下图是本文介绍到的一些对象之间的引用关系图,实线表示强引用,虚线表示弱引用:
如上图,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设计时的对上面问题的对策:
ThreadLocalMap的getEntry函数的流程大概为:
即:
1.使用ThreadLocal,建议用static修饰 static ThreadLocal headerLocal = new ThreadLocal();
2.使用完ThreadLocal后,执行remove操作,避免出现内存溢出情况。
关于ThreadLocalMap内部类的简单介绍
初始容量16,负载因子2/3,解决冲突的方法是再hash法,也就是:在当前hash的基础上再自增一个常量。