ThreadLocal解决了访问共享变量的阻塞问题,并且不需要像CAS操作一样牺牲CPU资源,它为每一个线程维护了一个变量副本,每个线程在访问ThrealLocal里面的变量时实际上访问的是自己线程内的变量副本,并且这个线程内的变量副本与其他线程的变量副本相互隔离,互不影响。也就是说,ThreadLocal包裹的变量是线程级变量。
ThreadLocal通过一个内部类ThreadLocalMap进行数据的保存,并将自己本身作为key,从get方法入手。
public T get() { // 取得当前线程 Thread t = Thread.currentThread(); // 取得当前线程内的ThreadLocalMap ThreadLocalMap map = getMap(t); if (map != null) { // 使用threadlocal作为key在ThreadLocalMap内取得Entry对象 ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") // 取得threadlocal包裹的值在该线程内的副本 T result = (T)e.value; return result; } } return setInitialValue(); }
在get方法内发现,Threadlocal首先获取了当前线程,然后使用当前线程作为key取得ThreadLocalMap对象,那么这个ThreadLocalMap对象仅对当前线程可见,ThreadLocalMap内包含的内容也仅对当前线程可见,查看getMap方法:
ThreadLocalMap getMap(Thread t) { return t.threadLocals; }
此时发现ThreadLocalMap实际上保存在Thread类的threadLocals变量中,查看Thread类代码,其内部保存了ThreadLocalMap类变量threadLocals,即ThreadLocalMap定义在ThreadLocal类中,却实际保存在Thread类中。
/* ThreadLocal values pertaining to this thread. This map is maintained * by the ThreadLocal class. */ ThreadLocal.ThreadLocalMap threadLocals = null;
观察ThreadLocalMap类的定义
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; } } }
使用弱引用的原因主要是为了帮助jvm进行垃圾回收(可以参考WeakHashMap)
通过内存引用关系图可以发现,对ThreadLocal的引用有两处,即自定义的threadlocal变量和Entry内的key,如果Entry内的key持有ThreadLocal的强引用,那么即使将自定义的threadlocal变量设置为空,由于key的存在,也无法对threadlocal所占用的内存进行回收,就会造成内存泄漏问题。
Entry内的key持有ThreadLocal的弱引用,当自定义的threadlocal变量设置为null时,只有key引用到了java堆内的ThreadLocal,因为弱引用的特性,如果没有其他强引用连接,则可以被回收,因此不会造成内存泄漏问题。
如果通过将ThreadLocal设置为null来帮助GC时发现,threadlocal变量可以被回收掉,但是如果之前未将value清空的话,value会一直持有引用,会造成内存泄漏问题。因此当某个线程内的threadlocal使用完了,一定要先调用remove方法清空value,在设置threadlocal为null。
搞清楚了弱引用的作用后,继续看ThreadLocalMap类的getEntry方法
private Entry getEntry(ThreadLocal<?> key) { // 通过threadlocal类的hashcode取得Entry在table中的下标 int i = key.threadLocalHashCode & (table.length - 1); // 在table中取得Entry对象 Entry e = table[i]; if (e != null && e.get() == key) return e; else return getEntryAfterMiss(key, i, e); }
通过以上代码发现,ThreadLocalMap实际的存储结构是Entry[] table,而table的结构为hash表,为了验证这一观点,继续查看ThreadLocalMap类的set方法
private void set(ThreadLocal<?> key, Object value) { // 取得Entry表 Entry[] tab = table; // 取得表格长度 int len = tab.length; // 通过threadlocal类的hashcode取得Entry在table中的下标位置 int i = key.threadLocalHashCode & (len-1); // 如果Entry[i]不为空,从下标i开始遍历表格 for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { ThreadLocal<?> k = e.get(); // threallocal与entry中的key相同,直接替换值 if (k == key) { e.value = value; return; } // key为null,设置key,value并修改hashcode if (k == null) { replaceStaleEntry(key, value, i); return; } } // Entry[i]为空,直接设置key和value tab[i] = new Entry(key, value); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }
看完了set方法,此时大致对table的结构有了一定的掌握。
设置值时通过threadlocal的hash码与table的长度来获取要存储的下标位置,获取value时也是同样的方式。