1、简介
ThreadLocal指的就是线程本地变量。程序会存在线程安全问题,就是同一份资源在同一个时刻会有多个线程进行操作,导致最终结果与预想不一致。
ThreadLocal中每个线程维护自己的一份变量(ThreadLocalMap),这样这个变量就是这个线程私有的,而且其生命周期和这个线程的生命周期等同。
ThreadLocal其实就是封装了线程本地变量的一系列操作合集。也就是Thread对象中的一个局部私有属性ThreadLocal.ThreadLocalMap threadLocals
下面是截取的Thread的部分代码:
public class Thread implements Runnable {// 与该线程相关的ThreadLocal值,这个map由ThreadLocal类维护ThreadLocal.ThreadLocalMap threadLocals = null;}
2、简单使用
这个地方就使用官方的例子来说明,代码如下
import java.util.concurrent.atomic.AtomicInteger;public class ThreadId {// Atomic integer containing the next thread ID to be assignedprivate static final AtomicInteger nextId = new AtomicInteger(0);// Thread local variable containing each thread's IDprivate static final ThreadLocal<Integer> threadId =new ThreadLocal<Integer>() {@Overrideprotected Integer initialValue() {return nextId.getAndIncrement();}};// Returns the current thread's unique ID, assigning it if necessarypublic static int get() {return threadId.get();}}
这段代码主要是给每个线程生成一个唯一标识的id,线程的id,在调用ThreadId.get()方法的手才进行赋值,而且后面的调用将不会改变这个值。
每个线程访问自己的本地变量的时候(通过get或者set方法)都会初始化一个变量副本ThreadLocalMap(如果已经存在则直接使用)。
ThreadLocal实例通常是类中的私有静态字段。目的是将一些状态和线程管理起来。
3、ThreadLocal实现原理
线程本地变量副本的初始化是懒加载的,都是第一次调用get或者set方法的时候才会进行初始化。并且这个副本并不是维护在ThreadLocal中,而是调用线程的一个变量 ThreadLocal.ThreadLocalMap threadLocals = null
3.1 get方法
源码分析:
public T get() {// 获取当前调用线程Thread t = Thread.currentThread();// 获取当前调用线程的threadLocals map引用ThreadLocalMap map = getMap(t);if (map != null) {// 如果map不为空则当前map已经初始化了// 获取entryThreadLocalMap.Entry e = map.getEntry(this);if (e != null) { // 说明初始化过当前ThreadLocal对象关联的线程局部变量@SuppressWarnings("unchecked")T result = (T)e.value;// 直接拿到值并返回return result;}}// 如果map为空代表还未初始化,则需要进行初始化// 当前线程的threadLocals是空// 当前线程与当前ThreadLocal对象没有生成关联关系return setInitialValue();}// 返回调用线程的threadLocals变量值ThreadLocalMap getMap(Thread t) {return t.threadLocals;}private T setInitialValue() {// 获取初始值T value = initialValue();// 获取当前调用线程Thread t = Thread.currentThread();// 获取当前调用线程的ThreadLocalMap对象ThreadLocalMap map = getMap(t);if (map != null) {// 存在则设置初始化值map.set(this, value);} else {// 给当前调用线程创建ThreadLocalMap对象createMap(t, value);}// 返回本地变量值return value;}// 本地变量初始值protected T initialValue() {return null;}// 给当前调用线程初始化ThreadLocalMap对象void createMap(Thread t, T firstValue) {t.threadLocals = new ThreadLocalMap(this, firstValue);}
3.2 set方法
set方法主要是修改当前本地变量的值
public void set(T value) {// 获取当前线程Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null)// 设置值map.set(this, value);else// 创建mapcreateMap(t, value);}// 给当前调用线程初始化ThreadLocalMap对象void createMap(Thread t, T firstValue) {t.threadLocals = new ThreadLocalMap(this, firstValue);}
3.3 remove方法
移除当前ThreadLocal对象与当前调用线程相关联的线程局部变量。这个方法本身很简单
public void remove() {// 获取mapThreadLocalMap m = getMap(Thread.currentThread());if (m != null)// 移除keym.remove(this);}
相对来说ThreadLocal本身的方法都比较简单,ThreadLocal的主要难点和功能都在ThreadLocalMap中。比如
3.4 hash相关属性
ThreadLocal主要是操作ThreadLocalMap,ThreadLocalMap是一个hash map,但是没有jdk的HashMap那么复杂。
ThreadLocalMap中的key和value被封装成一个弱引用的Entry对象。key就是ThreadLocal。所以ThreadLocal中一定会绑定一个hashCode。这个地方主要来分析一下这个hashcode
public class ThreadLocal<T> {// ThreadLocal对象的hashCodeprivate final int threadLocalHashCode = nextHashCode();// 下一个hashCodeprivate static AtomicInteger nextHashCode =new AtomicInteger();// hash增长值// 这是一个斐波拉契数,黄金分割数,使用这个数据累加可以使元素更加的散列,不容易hash冲突private static final int HASH_INCREMENT = 0x61c88647;// 下一个hashCodeprivate static int nextHashCode() {return nextHashCode.getAndAdd(HASH_INCREMENT);}}
从上面的代码可以看出,ThreadLocal对象使用一个原子类来存储下一个唯一的hashCode.并且使用一个斐波拉契作为一个递增数保证数据更加散列。
4、真正的内核ThreadLocalMap
上一节分析了ThreadLocal的主要方法,可以发现其实其使用以及原理非常简单。
但是还有更加深入细致的核心内容没有分析就是ThreadLocalMap这个数据结构。
ThreadLocalMap也是一个Map类型的数据结构。但是它和jdk的HashMap是不一样的,它的操作会相对简单,不需要向HashMap那样通过链表以及红黑树来解决hash冲突,更加没有什么链化和树化操作。ThreadLocalMap实现要相对简单些,也很巧妙,下面开始分析
先上一张核心方法执行图

1 Entry分析
首先需要理解一个概念弱引用,直接上代码,解释弱引用:
public class Wref {public static void main(String[] args) {// t 是Test的强引用Test t = new Test();// weakRef是Test的弱引用WeakReference<Test> weakRef = new WeakReference<>(t);System.out.println("弱引用:" + weakRef.get());// 断开强引用t = null;System.out.println("去除强应用,垃圾未回收:" + weakRef.get());// 手动执行垃圾回收System.gc();// 打印垃圾回收后弱引用的值System.out.println("去除强引用,垃圾已回收:" + weakRef.get());}public static class Test{}}// 打印结果//弱引用:com.cx.ref.Wref$Test@5cad8086//去除强应用,垃圾未回收:com.cx.ref.Wref$Test@5cad8086//去除强引用,垃圾已回收:null
所以对于弱引用,只要弱引用对象没有强引用了,下次垃圾回收,这个对象肯定被回收,那么弱引用的对象就会是null。
ThreadLocalMap中的Entry对象就是一个弱应用
static class Entry extends WeakReference<ThreadLocal<?>> {/** The value associated with this ThreadLocal. */Object value;Entry(ThreadLocal<?> k, Object v) {// ThreadLocal对象为key,那么当ThreadLocal对象没有强引用的时候,等待一次GC后// Entry对象的key肯定是null// 这个机制对后面识别无效节点非常有用super(k);value = v;}}
当ThreadLocal对象没有强引用后,当经过一次垃圾回收后,这个对象会被回收,那么Entry的key就是是null
2 相关属性介绍
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;}}/*** The initial capacity -- MUST be a power of two.* 散列表的初始容量,一定是2的次方数,* 这样求取桶位 可以使用 hashcode & (len - 1)来求取*/private static final int INITIAL_CAPACITY = 16;/*** The table, resized as necessary.* table.length MUST always be a power of two*/// 数组private Entry[] table;/*** The number of entries in the table.*/// 元素大小private int size = 0;/*** The next size value at which to resize.*/// 阈值 默认0, 扩容阈值private int threshold; // Default to 0/*** Set the resize threshold to maintain at worst a 2/3 load factor.*/// 阈值为散列表长度的2/3private void setThreshold(int len) {threshold = len * 2 / 3;}/*** Increment i modulo len.*/// 获取下一个index,// i + 1 < len就返回 i + 1 否则返回 0// 从这个地方可以看出,这个数组是存放元素的方式是环状存储,或者说可以环绕式访问private static int nextIndex(int i, int len) {return ((i + 1 < len) ? i + 1 : 0);}/*** Decrement i modulo len.*/// 获取前一个index// i -1 <= 0 则返回 i -1 否则是 len -1private static int prevIndex(int i, int len) {return ((i - 1 >= 0) ? i - 1 : len - 1);}}
3 构造方法
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {// 初始化数组table = new Entry[INITIAL_CAPACITY];// 获取桶位int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);// 存值table[i] = new Entry(firstKey, firstValue);// 元素大小size = 1;// 设置扩容阈值 16 * 2 / 3 = 10setThreshold(INITIAL_CAPACITY);}
这个地方就是ThreadLocal中createMap方法的具体执行。ThreadLocalMap是懒加载的,只有第一次访问的时候才会初始化
4 getEntry方法
ThreadLocal中的get方法就是通过getEntry来获取值的
private Entry getEntry(ThreadLocal<?> key) {// 获取数组indexint i = key.threadLocalHashCode & (table.length - 1);Entry e = table[i];if (e != null && e.get() == key)// 如果当前桶位有值且key是当前key,则返回return e;else// 数组在i 没有值或者key不一致return getEntryAfterMiss(key, i, e);}// key 当前ThreadLocal对象// i 数组当前索引// e 当前处理的Entryprivate Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {Entry[] tab = table;int len = tab.length;while (e != null) {// 获取当前e的keyThreadLocal<?> k = e.get();// 如果k 就是当前的key则直接返回Entryif (k == key)return e;if (k == null)// 在找到key对应的的Entry之前,已经发现了失效的Entry则需要删除所有的过期EntryexpungeStaleEntry(i);else// 如果还不是当前Key则继续往后找i = nextIndex(i, len);e = tab[i];}// 如果向后查找到最后都没有找到,说明真的没有这个Entry直接返回nullreturn null;}
5 expungeStaleEntry(i)方法
获取Entry时,在找到对应key的Entry之前发现了失效的Entry,则需要执行此操作将当前失效节点以及后续失效节点进行删除
// staleSlot过期索引private int expungeStaleEntry(int staleSlot) {Entry[] tab = table;int len = tab.length;// expunge entry at staleSlot// 删除插槽中的无效数据tab[staleSlot].value = null;tab[staleSlot] = null;size--;// Rehash until we encounter null// 重新hash直到遇到nullEntry e;int i;// 从staleSlot的下一个索引开始找:staleSlot + 1// 直到找到null停止for (i = nextIndex(staleSlot, len);(e = tab[i]) != null;i = nextIndex(i, len)) {ThreadLocal<?> k = e.get();if (k == null) {// 找到失效的Entry然后移除e.value = null;tab[i] = null;size--;} else { // 如果不是失效的Entry那么重新hash这个Entry的位置int h = k.threadLocalHashCode & (len - 1);if (h != i) {// 如果h != i 代表 之前设置这个Entry的时候存在hash冲突,h这个位置已经有元素了// 那么需要将当前Entry从i移除,然后再从h这个位置开始往后找,直至找到空桶// 清空i位置Entrytab[i] = null;// Unlike Knuth 6.4 Algorithm R, we must scan until// null because multiple entries could have been stale.// 从h开始往查找,找到第一个桶位为null的元素while (tab[h] != null)h = nextIndex(h, len);tab[h] = e;}// 如果 h = i说明没有hash冲突,直接放置就可以}}// 返回 最后值为null的索引return i;}
6 set方法
ThreadLocal中set方法会使用这个方法
private void set(ThreadLocal<?> key, Object value) {// We don't use a fast path as with get() because it is at// least as common to use set() to create new entries as// it is to replace existing ones, in which case, a fast// path would fail more often than not.Entry[] tab = table;// 数组长度int len = tab.length;// 当前key在数组中的位置int i = key.threadLocalHashCode & (len-1);// 如果key产生了hash冲突,则执行for循环里面的代码for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {// 获取当前Entry的keyThreadLocal<?> k = e.get();// key相同,说明找到了,直接替换然后结束if (k == key) {e.value = value;return;}// k == null 说明数组当前位置元素为过期元素if (k == null) {// 替换逻辑,然后直接返回replaceStaleEntry(key, value, i);return;}}// 说明key没有产生hash冲突,直接进行设置tab[i] = new Entry(key, value);int sz = ++size;// 判断是否需要进行hash// 如果清除了一部分slot,那么肯定不需要进行扩容// 如果没有清除并且数量大于阈值了,那么必须要进行扩容了if (!cleanSomeSlots(i, sz) && sz >= threshold)rehash();}
7 replaceStaleEntry
set方法中,当查找时发现过期key时,可以进行占用
private void replaceStaleEntry(ThreadLocal<?> key, Object value,int staleSlot) {Entry[] tab = table;int len = tab.length;Entry e;// Back up to check for prior stale entry in current run.// We clean out whole runs at a time to avoid continual// incremental rehashing due to garbage collector freeing// up refs in bunches (i.e., whenever the collector runs).// 开始删除的桶位int slotToExpunge = staleSlot;// 从staleSlot往前查找 返回null结束,将找到的Entry中key == null的索引赋值给slotToExpungefor (int i = prevIndex(staleSlot, len);(e = tab[i]) != null;i = prevIndex(i, len))if (e.get() == null)slotToExpunge = i;// Find either the key or trailing null slot of run, whichever// occurs first// 从staleSlot + 1 开始往后查找,找到null结束for (int i = nextIndex(staleSlot, len);(e = tab[i]) != null;i = nextIndex(i, len)) {ThreadLocal<?> k = e.get();// If we find key, then we need to swap it// with the stale entry to maintain hash table order.// The newly stale slot, or any other stale slot// encountered above it, can then be sent to expungeStaleEntry// to remove or rehash all of the other entries in run.if (k == key) {// 找到key对应的Entry了// 替换值e.value = value;//tab[i] = tab[staleSlot];tab[staleSlot] = e;// Start expunge at preceding stale entry if it existsif (slotToExpunge == staleSlot)// 如果找到key了,并且staleSlot前面没有失效的entry,并且再找到key之前也没有// 失效的entry,那么就从当前找到key这个桶位开始往后进行探测删除slotToExpunge = i;cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);return;}// If we didn't find stale entry on backward scan, the// first stale entry seen while scanning for key is the// first still present in the run.if (k == null && slotToExpunge == staleSlot)// staleSlot是肯定会被替换的// 如果staleSlot前面没有找到失效的entry,那么如果在staleSlot后面发现的第一个就是// 后面开始删除的开始桶位slotToExpunge = i;}// If key not found, put new entry in stale slot// 向后查找过程中没有找到对应的key,将新的Entry放在失效的桶位tab[staleSlot].value = null;tab[staleSlot] = new Entry(key, value);// If there are any other stale entries in run, expunge themif (slotToExpunge != staleSlot)// 不相等说明staleSlot前面有过期数据或者后面又过期数据cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);}
这个地方解释下slotToExpunge的作用,就是记录这一段连续桶位最前面的那个失效的索引值
如果这个索引值存在则从这个索引值往后面进行失效数据清理
8 cleanSomeSlots
一般只有删除一个无效数据或者新增一个数据的时候才会执行这个清理
// i 是expungeStaleEntry清理完成后返回的最后一个桶位为null的索引// n 初始值table.length,private boolean cleanSomeSlots(int i, int n) {// 是否清楚数据标志boolean removed = false;Entry[] tab = table;int len = tab.length;do {i = nextIndex(i, len);Entry e = tab[i];if (e != null && e.get() == null) {// 过期keyn = len;removed = true;// 清理过期数据i = expungeStaleEntry(i);}//} while ( (n >>>= 1) != 0);return removed;}
这个地方的n会有两个取值
- table.length replaceStaleEntry方法调用
- size 插入的时候
9 rehash
这个方法就是用来扩容的。
只有添加数据的时候才会执行,并且需要满则以下条件
- cleanSomeSlots 没有清除掉任何过期数据
size >= threshold(数组长度的2/3)
private void rehash() {// 删除所有过期数据expungeStaleEntries();// 如果size > 3/4的阈值了就进行扩容// Use lower threshold for doubling to avoid hysteresisif (size >= threshold - threshold / 4)resize();}
首先会清除所有的过期数据
- 如果这个时候清除了过期数据,元素数量大于3/4的阈值则必须扩容
10 resize
这个方法特别简单,
- 新建一个两倍长度的数组
- 将原数组中未过期的数据全部重新计算hash值然后确定位置再重新填充到新的数组中
- 设置新的阈值
- 设置新的长度
将旧的数组替换成新的数组
private void resize() {Entry[] oldTab = table;int oldLen = oldTab.length;int newLen = oldLen * 2;Entry[] newTab = new Entry[newLen];int count = 0;for (int j = 0; j < oldLen; ++j) {Entry e = oldTab[j];if (e != null) {ThreadLocal<?> k = e.get();if (k == null) {e.value = null; // Help the GC} else {int h = k.threadLocalHashCode & (newLen - 1);while (newTab[h] != null)h = nextIndex(h, newLen);newTab[h] = e;count++;}}}setThreshold(newLen);size = count;table = newTab;}
11 remove
remove方法的逻辑
根据hash值计算出索引 index
- 从索引 index 开始,往后查找知道找到对应的entry,如果没有找到则返回
- 如果找到了将entry中的key清除,让后执行过期entry的清理
/*** Remove the entry for key.*/private void remove(ThreadLocal<?> key) {Entry[] tab = table;int len = tab.length;int i = key.threadLocalHashCode & (len-1);for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {if (e.get() == key) {e.clear();expungeStaleEntry(i);return;}}}
5、子线程不可继承性
从代码中可以知道,副本数据是保存在Thread对象中的。所以同一个ThreadLocal变量在父线程中被设置值后,在子线程中是没法获取到的。
6、InheritableThreadLocal
这个类可以解决子线程不能共享父线程ThreadLocal数据的问题
因为在线程new的时候会执行下面这行代码,会将父线程的inheritableThreadLocals给子线程
if (inheritThreadLocals && parent.inheritableThreadLocals != null)this.inheritableThreadLocals =ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
下面主要看看createInheritedMap
主要是将父线程的ThreadLocalMap传入,让后根据每个entry生成中的key和value生成一个新的Entry
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {return new ThreadLocalMap(parentMap);}private ThreadLocalMap(ThreadLocalMap parentMap) {Entry[] parentTable = parentMap.table;int len = parentTable.length;setThreshold(len);table = new Entry[len];for (int j = 0; j < len; j++) {Entry e = parentTable[j];if (e != null) {@SuppressWarnings("unchecked")ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();if (key != null) {// 将父线程的值传入子线程让子线程重新初始化值Object value = key.childValue(e.value);Entry c = new Entry(key, value);int h = key.threadLocalHashCode & (len - 1);while (table[h] != null)h = nextIndex(h, len);table[h] = c;size++;}}}}
public class InheritableThreadLocal<T> extends ThreadLocal<T> {// 值初始化protected T childValue(T parentValue) {return parentValue;}// 获取inheritableThreadLocalsThreadLocalMap getMap(Thread t) {return t.inheritableThreadLocals;}// 将生成的map挂在到inheritableThreadLocals上void createMap(Thread t, T firstValue) {t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);}}
从代码可以看出,虽然InheritableThreadLocal解决了子线程不能共享父线程的数据问题。但是这个值是线程是咧化的时候赋值上去的。如果是使用线程池,对于已经存在的线程这个就没有效果了
7、ThreadLocal内存泄漏问题
1、一般情况下ThreadLocal对象都是被修饰成static final, 这个也是官方的推荐使用方法,所以ThreadLocal对象基本不会存在过期现象,所以虽然ThreadLocal的方法都回进行过期数据的删除,但是其实并没有过期的数据。所以如果这个线程长期存活,当线程比较多的时候,或者ThreadLocal比较多的时候,这些数据都没法进行删除,那么这些数据将无法进行回收,而造成内存泄漏。所以set或者get之后一定要remove
8、遗留问题
前面说了,对于父子线程,使用InheritableThreadLocal可以解决子线程访问父线程的ThreadLocalMap.但是对于线程池中的已存在线程,这个没法解决。所以这个问题怎么解决呢。使用阿里开源的TransmittableThreadLocal可以解决这个问题。这个在这里不分析了,后面在专门分析
