一、使用用法
用于给每一个线程创建一份独立的变量副本,每次调用threadLocal的方法实际上都是对当前线程的变量进行操作
分析:
- ThreadLocal内部有一个内部类ThreadLocalMap结构,ThreadLocal的相关操作都是由ThreadLocalMap来完成。
- ThreadLocalMap类有一个 Entry[] table 数组,以数组作为数据结构,数组长度默认16,阈值默认为长度的2/3
- Entry的key是ThreadLocal对象(key是弱引用对象),value是 threadLocal赋的值
- Thread类有一个属性 threadLocals 指向ThreadLocalMap,threadLocal的操作,实际是是获取当前线程的ThreadLocalMap类,因此多线程对ThreadLocal的操作,实际上都是对自己线程的ThreadLocalMap进行操作,不会有线程安全问题
二、ThreadLocal源码分析
1. 属性
//创建一个threadlocal对象,生成一个hashcode,//线程获取 threadLocal.get()时,如果是第一次在某个 threadLocal对象上get时,会给当前线程分配一个value,//这个value跟当前的thraedLocal对象被包装成为一个entry 其中key是threadlocal对象,value是threadLocal对象给当前线程分配的value,//这个entry存放到当前线程 threadLocals 这个map的那个桶位?与当前threadLocal对象的 threadLocalHashCode值有关系//使用threadLocalHashCode & table.length-1 得到的位置就是 当前entry存放的位置private final int threadLocalHashCode = nextHashCode();//每创建一个threadLocal对象,这个 threadLocalHashCode 就会增长//0x61c88647,值很特殊,是一个黄金分割数,使用这个hash增量的好处,就是hash分布均匀private static final int HASH_INCREMENT = 0x61c88647;private static int nextHashCode() {return nextHashCode.getAndAdd(HASH_INCREMENT);}//调用当前threadLocal对象的initialValue方法,默认初始化value 为null,一般我们需要重写protected T initialValue() {return null;}
2. get()
//返回当前线程与当前ThreadLocal对象相关联的 线程局部变量,这个线程局部变量只有当前线程能够访问到public T get() {//获取当前线程Thread t = Thread.currentThread();//返回当前线程对象的ThreadLocalMapThreadLocalMap map = getMap(t);//条件成立, 说明当前线程已经拥有自己的threadLocalMap对象了,ThreadLocalMap已经初始化过了if (map != null) {//从ThreadLocalMap根据key获取value,key是 当前ThreadLocal对象ThreadLocalMap.Entry e = map.getEntry(this);//条件成立:说明当前线程 初始化过 与当前threadLocal对象相关联的 线程局部变量if (e != null) {//返回对应的值@SuppressWarnings("unchecked")T result = (T)e.value;return result;}}//执行到这里//1. 当前线程的ThreadLocalMap 为空//2. 当前线程与当前threadlocal对象 没有设置 相关联的 键值对//进行初始化操作 包含初始化map 和初始化value两部分return setInitialValue();}
2.1 getMap()
//返回当前线程的ThreadLocalMapThreadLocalMap getMap(Thread t) {return t.threadLocals;}
2.2 setInitialValue()
private T setInitialValue() {//获取初始值,留给子类重新,一般会在创建threadLocal时候重写方法T value = initialValue();//获取当前线程Thread t = Thread.currentThread();//获取当前线程的 threadLocals ThreadLocalMap对象ThreadLocalMap map = getMap(t);//如果ThreadLocalMap 不为空,只需要保存当前线程跟valueif (map != null)map.set(this, value);//否则即需要创建 ThreadLocalMap,也需要生成当前线程的valueelsecreateMap(t, value);//返回当前线程与当前threadlocal对象绑定的 valuereturn value;}
2.3 createMap()
void createMap(Thread t, T firstValue) {//创建一个ThreadLocalMap,同时初始value值t.threadLocals = new ThreadLocalMap(this, firstValue);}
3.set()
//修改 当前线程与当前threadlocal对象 相关联的valuepublic void set(T value) {//获取当前线程Thread t = Thread.currentThread();//获取当前线程的threadLocalMap对象ThreadLocalMap map = getMap(t);//threadLocalMap已经初始化过了,设置值if (map != null)map.set(this, value);//否则生成map,设置值elsecreateMap(t, value);}
4 remove()
//移除当前线程与当前threadlocal对象 相关联的 valuepublic void remove() {//获取当前线程的ThreadLocalMap对象ThreadLocalMap m = getMap(Thread.currentThread());if (m != null)//调用threadLocalMap.remove方法m.remove(this);}
5.内部类ThreadLocalMap
5.1 属性
//默认容量16private static final int INITIAL_CAPACITY = 16;//threadLocalMap 散列表数组引用,长度必须是2的倍数private Entry[] table;//当前数组占用情况,存放了多少个entryprivate int size = 0;//扩容触发阈值,初始值为 数组长度的2/3private int threshold;//将扩容阈值设置为数组长度的三分之二private void setThreshold(int len) {threshold = len * 2 / 3;}//获取下一个位置private static int nextIndex(int i, int len) {return ((i + 1 < len) ? i + 1 : 0);}//获取前一个位置private static int prevIndex(int i, int len) {return ((i - 1 >= 0) ? i - 1 : len - 1);}//构造函数//延迟初始化,只有线程第一次调用get或set方法时候 才会去创建table数组ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {//初始化散列表,长度16table = new Entry[INITIAL_CAPACITY];//计算entry的存储位置int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);//创建一个entry,放到指定数组下标下面table[i] = new Entry(firstKey, firstValue);//占用情况设置为1size = 1;//设置扩容阈值setThreshold(INITIAL_CAPACITY);}
5.2 getEntry()
//找到了返回entry,找不到返回null,在找的过程中如果碰到了过期数据会对过期数据进行清理并优化未过期数据保存的位置private Entry getEntry(ThreadLocal<?> key) {//根据当前线程的threadLocal对象获取entry位置int i = key.threadLocalHashCode & (table.length - 1);Entry e = table[i];if (e != null && e.get() == key)return e;else//几种情况走到这里?//1. e==null//2. e.key!=key//getEntryAfterMiss 会继续向当前桶位下标后面继续搜索 e.key==key的entery//为什么这么做?//是因为存储时 发生hash冲突时候,并没有在当前entry位置形成链表,而是 线性的向后找到一个合适的slot桶位,然后放进去return getEntryAfterMiss(key, i, e);}
5.3 getEntryAfterMiss()
/*key -> threadlocal对象i -> 计算出来的数组索引e -> table[i] 的entry*/private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {//获取散列表Entry[] tab = table;//获取散列表数组长度int len = tab.length;//碰到entry==null 搜索结束while (e != null) {//获取当前entry的key,就是threadLocal对象ThreadLocal<?> k = e.get();//条件成立,说明向后查询过程中找到了合适的entry,直接返回entryif (k == key)return e;//条件成立,说明当前桶entry的key 关联的threalocal对象被回收了,因为key是弱引用,key=e.get()==nullif (k == null)//做一次 探测式过期数据回收,从i位置开始向后清除过期数据并对未过期数据进行位置优化,直到桶位为空结束expungeStaleEntry(i);else//更新index,继续向后搜索i = nextIndex(i, len);//获取下一个i的entrye = tab[i];}//执行到这里,说明从i坐标开始之后的桶位没有找到对应的entry,返回nullreturn null;}
5.4 expungeStaleEntry()
/*从staleSlot位置开始(包括该位置)向后清除过期数据,然后对当时存储时候发生了hash冲突的未过期数据进行优化存储位置,尽量放在原本正确位置或靠近正确位置的地方,直到碰到桶位为空,退出探测式过期数据清理逻辑return返回table数组没数据的下标*/private int expungeStaleEntry(int staleSlot) {//当前tableEntry[] tab = table;//table长度int len = tab.length;//回收过期数据,因为此处的数据已经是过期数据,threaLocal对象以及被回收了, 所以直接将value设置为null,help gctab[staleSlot].value = null;//也将该桶位设置为nulltab[staleSlot] = null;//数组长度减1size--;// Rehash until we encounter null//当前遍历节点Entry e;int i;//循环从当前过期数据的下一个位置开始搜索过期数据,直到碰到tab[i]==null为止for (i = nextIndex(staleSlot, len);(e = tab[i]) != null;i = nextIndex(i, len)) {//进入循环里面,当前entry一定不为空//获取当前遍历节点entry的keyThreadLocal<?> k = e.get();//条件成立:说明当前key表示的threadlocal对象为null了,表示已经threadLocal对象被回收了,说明当前entry是脏数据了。需要清理if (k == null) {//help gc...e.value = null;tab[i] = null;//长度减1size--;} else {//执行到这里,说明当前遍历的entry不是过期数据//重新获取当前entry的下标,因为当前entry 存储时候有可能遇到hash冲突了,往后偏移存储了,这个时候 应该去优化位置,让当前entry更靠近原本正确的位置,这样的话,查询起来效率更高//重新计算当前entry的索引int h = k.threadLocalHashCode & (len - 1);//条件成立,当前entry存储时候发生hash冲突了,往后偏移存储了if (h != i) {//将entry当前保存的位置 设置为nulltab[i] = null;// Unlike Knuth 6.4 Algorithm R, we must scan until// null because multiple entries could have been stale.//h是正确位置,这里是从h正确位置开始,查找第一个可以保存当前entry的位置while (tab[h] != null)h = nextIndex(h, len);//将当前entry元素放入 距离正确位置最近的位置(也有可能就是正确位置)tab[h] = e;}}}return i;}
5.4 set()
private void set(ThreadLocal<?> key, Object value) {//获取散列表Entry[] tab = table;//获取散列表长度int len = tab.length;int i = key.threadLocalHashCode & (len-1);//退出循环条件 table[i]桶位为空for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {ThreadLocal<?> k = e.get();//条件成立,说明当前set操作是一个替换操作,直接替换值if (k == key) {e.value = value;return;}//条件成立,说明碰到entry是过期数据情况了if (k == null) {//替换过期数据的逻辑replaceStaleEntry(key, value, i);return;}}//执行到这里,说明是一个新数据,直接在合适的位置创建一个entrytab[i] = new Entry(key, value);int sz = ++size;//条件一 !cleanSomeSlots(i, sz) 成立说明 启发式清理 未清理到过期数据//条件二 sz >= threshold 成立 table元素个数超过阈值,进行扩容if (!cleanSomeSlots(i, sz) && sz >= threshold)rehash();}
5.5 replaceStaleEntry() 替换过期Entry
/*该方法 是在当前staleSlot位置(过期数据)开始,像后面搜索,是否有相同的key,有则将数据保存到staleSlot位置,然后搜索到的下标设置为过期数据,然后清理数据;没有向后搜索到就将数据添加到当前staleSlot位置,然后清理数据。*///staleSlot set方法查找时,发现当前桶位是过期数据的indexprivate void replaceStaleEntry(ThreadLocal<?> key, Object value,int staleSlot) {//获取散列表Entry[] tab = table;//数组长度int len = tab.length;Entry e;//开始 探测式清理过期数据的开始下标,默认从当前staleSlot开始int slotToExpunge = staleSlot;//以当前staleSlot位置开始,向前查找有没有过期数据,for循环直到entry为空结束for (int i = prevIndex(staleSlot, len);(e = tab[i]) != null;i = prevIndex(i, len))//条件成立,说明向前找到了过期数据,更新 探测清理过期数据的开始下标为iif (e.get() == null)slotToExpunge = i;//以当前staleSlot开始,向后查找for (int i = nextIndex(staleSlot, len);(e = tab[i]) != null;i = nextIndex(i, len)) {ThreadLocal<?> k = e.get();//条件成立,说明找到相同key,是替换操作if (k == key) {//替换新数据e.value = value;//交换位置的逻辑//将tab[staleSlot]这个过期数据,放入到table[i]这个位置tab[i] = tab[staleSlot];//将匹配的entry放到tab[staleSlot]位置,这样,这个数据的位置就被优化了tab[staleSlot] = e;// Start expunge at preceding stale entry if it exists//条件成立://1.说明一开始向前查找过期数据下标,并未找到过期数据的entry//2.向后也没有查找到过期数据if (slotToExpunge == staleSlot)//将 探测式清理过期数据的开始下标 修改为当前islotToExpunge = i;//先探测式清理然后启发式清理cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);return;}//条件1 k == null 成立 说明当前遍历的entry是过期数据//条件2 slotToExpunge == staleSlot 之前向前找过期数据下标未找到if (k == null && slotToExpunge == staleSlot)//条件成立,表明向后查找过程中找到一个过期数据了,更新slotToExpunge下标为当前i位置//前提条件是 向前查找过期数据,未找到slotToExpunge = i;}//什么时候执行到这里? 向后查找中并未找到key=e.key,说明set操作是一个添加逻辑//直接将新数据添加到tab[staleSlot],第一次的过期下标位置tab[staleSlot].value = null;tab[staleSlot] = new Entry(key, value);//条件成立,说明除了staleSlot以外,还是有其他的过期数据,要开启清理数据的逻辑if (slotToExpunge != staleSlot)//先探测式清理然后启发式清理cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);}
5.6 cleanSomeSlots() 启发式清理过期Entry
//int i 启发式清理工作开始位置 数组桶位元素为null的下标//int n 一般是table.length 这里也表示结束条件//返回true 表示清理过过期数据,false 未清理过过期数据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];//条件成立,当前entry的key是过期数据if (e != null && e.get() == null) {//更新结束条件位lenn = len;//表示清理过数据removed = true;//从i开始 做一次 探测式清理工作i = expungeStaleEntry(i);}} while ( (n >>>= 1) != 0);return removed;}
5.7 rehash()
private void rehash() {//执行该方法后,数组所有的过期数据都会被清除掉expungeStaleEntries();//条件成立 说明清理完过期数据,当前table数量仍然达到 threshold*3/4 扩容条件,真正触发扩容操作if (size >= threshold - threshold / 4)//扩容resize();}
5.8 resize()扩容
//扩容private void resize() {//获取散列表Entry[] oldTab = table;//获取散列表长度int oldLen = oldTab.length;int newLen = oldLen * 2;//新数组Entry[] newTab = new Entry[newLen];//表示新table中 entry数量int count = 0;//将老表数据进行迁移for (int j = 0; j < oldLen; ++j) {//获取老表的数据Entry e = oldTab[j];if (e != null) {//获取entry的key,threadLocal对象ThreadLocal<?> k = e.get();//条件成立,说明老表中当前entry是一个过期数据,help gcif (k == null) {e.value = null; // Help the GC} else {//老表数据是非过期数据,正常数据//计算出当前entry在新表保存的位置int h = k.threadLocalHashCode & (newLen - 1);//有可能当前entry在新表中发生hash冲突了,所以找向后找一个最近的位置存放while (newTab[h] != null)h = nextIndex(h, newLen);//entry数据保存到新表中newTab[h] = e;//entry数量+1count++;}}}//设置扩容后的阈值setThreshold(newLen);//更新数组元素个数size = count;table = newTab;}
5.9 remove()
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;}}}
ThreadLocal源码总结:
ThreadLocal的操作都交给了ThreadLocalMap来完成,ThreadLocalMap是一个entry数组,entry的key是ThreadLocal对象,key是一个弱引用对象,value是对应的值,当保存发生hash冲突后会继续向后查找,找到一个合适的位置保存entry。
1. get方法
- 先获取当前线程的ThreadLocalMap,如果map为空,表示未初始化,给当前线程创建一个map并新建一个entry,添加进去,返回value
map不为空,则用threadLocal的hashcode & 数组长度-1,找到对应桶位下标,从下标位置开始循环向后查找,key相同找到返回值,找不到返回null,如果在查找过程中遇到了过期数据(key == null的数据),则从当前桶位开始,直到遇到桶位为空结束:进行探测式清理数据工作(清理key==null的桶位,并优化后面未过期数据的桶位位置,尽可能靠近原本正确的位置)
2. set方法
获取当前线程的ThreadLocalMap,如果map为空,表示未初始化,给当前线程创建一个map并新建一个entry,添加进去
- 如果map不为空,则根据threadLocal的hashcode找到对应的桶位,如果桶位没数据,直接添加一个entry进去,然后进行启发式清理过期数据,清理后超过阈值,则rehash再次清理,如果再次清理之后超过了阈值的3/4,那么进行扩容;如果桶位有数据,那么从当前桶位开始向后遍历查询,找到key相同的桶位的话,则进行值更新,找的过程中如果发现了过期数据,那么从当前过期数据下标开始,进行第二次查找,这里查找分两步
- 第一步从当前过期数据下标开始向前查询过期数据,找到一个用变量A保存过期数据下标
- 第二步继续从当前过期数据下标开始遍历向后查询,直至桶位为空结束,如果找到key相同的桶位则在当前桶位更新数据,并与当前过期数据下标进行位置交换,然后探测式清理、启发式清理;找的过程中如果发现了过期数据,会用值A保存过期数据下标,如果循环结束未找到,会在当前过期数据下标进行更新,然后判断A是否与当前过期数据下标一致,不一致进行探测式清理、启发式清理过期数据
remove方法
根据threadLocal hashcode找到桶位下标,从下标开始向后循环遍历,找到了则清除entry,并从该下标开始进行探测式清理
