ThreadLocal分析
来源:互联网 发布:照片合成软件下载 编辑:程序博客网 时间:2024/05/21 01:48
ThreadLocal分析
1、概述
当访问共享的可变数据时,通常需要使用同步。一种避免使用同步的方式就是不共享数据,这种技术被称为线程封闭。ThreadLocal<T>做到了线程的封闭,通过提供get和set方法,每个线程可以设置变量T的独立的副本,并随后获取。ThreadLocal<T>的实现可以想到一个简单的方案,通过在类中持有线程安全的ConcurrentHashMap<Thread,T>类型的实例变量map来实现,以当前线程Thread作为key,多个线程共享map容器,相关源码如下:
import java.util.Map;import java.util.concurrent.ConcurrentHashMap;public class ThreadLocal<T> {private Map<Thread, T> map = new ConcurrentHashMap<>();public void put(T t) {map.put(Thread.currentThread(), t);}public T get() {return map.get(Thread.currentThread());}}
但JDK下的实现远非如此,JDK的实现着重考虑了以下几点:
a)Local
每个Thread持有各自的实例变量map用以存储T,而不是多个Thread共享一个map,所以不同Thread的T存储在Thread的Local map下,避免并发
b)GC
Thread下的变量副本随着Thread的GC而消亡,另外ThreadLocal变量GC后Thread下实例变量map中当前T也会随着get或set被GC,避免内存泄露
每个Thread下持有一个ThreadLocalMap的实例变量,实例变量ThreadLocalMap以ThreadLocal实例作为key,如下图所示:
了解了基本结构后,开始看源码 。
2、源码分析
从源码注释中看到ThreadLocal实例通常被private static所修饰,多个线程可以共用一个ThreadLocal实例作为key进行存储。
ThreadLocal类中存在以下几个重要的方法:
1)protected T initialValue()
protected T initialValue() { return null; }
被protected修饰,很显然当需要设定一个非null初始值的时候,需要子类去Override该方法,通常ThreadLocal的子类以匿名类的形式出现,如下:
private static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>(){@Overrideprotected Integer initialValue() {// 根据业务设置初始值return Integer.MIN_VALUE;};};该方法在第一次调用get且未调用过set方法时调用
2)public T get()
public T get() {// 获取当前线程实例Thread t = Thread.currentThread();// 获取ThreadLocalMap实例,该类为ThreadLocal下的内部类ThreadLocalMap map = getMap(t);// 当map不为空时,从map中获取value值if (map != null) {ThreadLocalMap.Entry e = map.getEntry(this);if (e != null)return (T)e.value;}// 设置初始化值并返回return setInitialValue();}ThreadLocalMap的获取是通过getMap(Thread t),如下:
ThreadLocalMap getMap(Thread t) {// 此处可以看出内部类ThreadLocalMap作为Thread下的实例变量 ThreadLocal.ThreadLocalMap threadLocals = null;return t.threadLocals;}
2.1 先来看map为null的情况,调用setInitialValue(),如下:
private T setInitialValue() {// 获取初始值T value = initialValue();// 通过当前线程实例获取mapThread t = Thread.currentThread();ThreadLocalMap map = getMap(t);// 如果map非空、则设置当前值if (map != null)map.set(this, value);else// 否则创建map,并设置value值createMap(t, value);// 返回初始值return value;}当map为空时,调用createMap(Thread t, T firstValue)进行初始化,方法如下:
void createMap(Thread t, T firstValue) {// 延迟实例化Thread下的ThreadLocalMap,将当前ThreadLocal实例作为keyt.threadLocals = new ThreadLocalMap(this, firstValue);}当map非空时,以当前ThreadLocal实例作为key值,设置当前value
2.2 再来看map非空时,以当前ThreadLocal实例作为key值去map中查找,下面看看内部类ThreadLocalMap的数据结构及相关实现,如下:
static class ThreadLocalMap{/**继承WeakReference,将ThreadLocal实例作为弱引用 *因此当ThreadLocal实例引用设置为null时,不影响ThreadLocal实例的GC*/static class Entry extends WeakReference<ThreadLocal> {Object value;Entry(ThreadLocal k, Object v) {super(k);value = v;}}// map初始容量private static final int INITIAL_CAPACITY = 16;// 数组table的length必须为2的n次方,通过特定算法实现不同实现private Entry[] table;// 当前map的大小private int size = 0;// 重新调整数组table的size时的一个阈值private int threshold;... ...}
由源码可以看出ThreadLocalMap通过数组实现,每个Entry都是一个WeakReference实例,引用ThreadLocal实例,所以当ThreadLocal实例引用设置为null时,不影响ThreadLocal实例的GC。而数组中的Entry非链表结构,那么ThreadLocalMap是如何解决hash冲突的呢?一个神奇的数字0x61c88647
public class ThreadLocal<T> { private final int threadLocalHashCode = nextHashCode();// 类成员 private static AtomicInteger nextHashCode = new AtomicInteger(); /** * The difference between successively generated hash codes - turns * implicit sequential thread-local IDs into near-optimally spread * multiplicative hash values for power-of-two-sized tables. * 生成的hash值能够均匀的分布在2的n次方的数组tables中 */ private static final int HASH_INCREMENT = 0x61c88647; private static int nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT); }... ...}
每当在JVM下生成一个ThreadLocal实例时,会初始化一个threadLocalHashCode作为当前ThreadLocal实例的唯一标示,而生成这个threadLocalHashCode是通过原子变量nextHashCode当前值 +0x61c88647 (二进制为:01100001110010001000011001000111)生成 。而当调用map的get方法时,如下:
private Entry getEntry(ThreadLocal key) {// threadLocalHashCode = 0//000000000000000000000000000000000//000000000000000000000000000001111// & = 0// threadLocalHashCode = 0x61c88647 + 0//001100001110010001000011001000111//000000000000000000000000000001111// & = 7// threadLocalHashCode = 0x61c88647 + 0x61c88647// 011000011100100010000110010001110// 000000000000000000000000000001111// & = 14int i = key.threadLocalHashCode & (table.length - 1);Entry e = table[i];if (e != null && e.get() == key)return e;elsereturn getEntryAfterMiss(key, i, e);}
通过将当前ThreadLocal实例的threadLocalHashCode与map中tables.length-1进行与(&)运算,计算出数组索引,该算法能确保计算出的索引值均匀的分布在数组中,实验如下:
import java.util.Arrays;import java.util.concurrent.atomic.AtomicInteger;public class HashTest {private final static int HASH_INCREMENT = 0x61c88647;private static AtomicInteger nextHashCode = new AtomicInteger();public static void main(String[] args) {genHashCode(8);nextHashCode.set(0);genHashCode(16);nextHashCode.set(0);genHashCode(32);}private static void genHashCode(int length) {int[] array = new int[length];for (int i = 0; i < length; i++) {array[i] = nextHashCode() & (length - 1);}System.out.println("排序前:" + Arrays.toString(array));Arrays.sort(array);System.out.println("排序后:" + Arrays.toString(array));}private static int nextHashCode() {return nextHashCode.getAndAdd(HASH_INCREMENT);}}输出结果如下:===================================================排序前:[0, 7, 6, 5, 4, 3, 2, 1]排序后:[0, 1, 2, 3, 4, 5, 6, 7]排序前:[0, 7, 14, 5, 12, 3, 10, 1, 8, 15, 6, 13, 4, 11, 2, 9]排序后:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]排序前:[0, 7, 14, 21, 28, 3, 10, 17, 24, 31, 6, 13, 20, 27, 2, 9, 16, 23, 30, 5, 12, 19, 26, 1, 8, 15, 22, 29, 4, 11, 18, 25]排序后:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31]
通过计算所得的索引值获取到数组中的Entry,如果当前entry不为null,并且entry中的ThreadLocal为当前实例,则返回;否则进行后续处理getEntryAfterMiss,如下:
private Entry getEntryAfterMiss(ThreadLocal key, int i, Entry e) {Entry[] tab = table;int len = tab.length;while (e != null) {ThreadLocal k = e.get();// 再次判断是否为当前ThreadLocal实例if (k == key)return e;// 如果key为null,则删除当前索引处的entryif (k == null)expungeStaleEntry(i);else// 否则获取下一个索引处的entryi = nextIndex(i, len);e = tab[i];}// 如果entry为null 直接返回return null;}
其中删除当前索引处的entry方法expungeStaleEntry(int staleSlot)如下:
private int expungeStaleEntry(int staleSlot) {Entry[] tab = table;int len = tab.length;// 首先删除索引staleSlot下的entrytab[staleSlot].value = null;tab[staleSlot] = null;size--;// 并且继续判断后续索引处的值是否有效Entry e;int i;for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {ThreadLocal k = e.get();// 如果索引key为null,删除索引i处的entryif (k == null) {e.value = null;tab[i] = null;size--;} else {// 否则重新计算当前索引int h = k.threadLocalHashCode & (len - 1);// 如果当前索引位置不等于计算的索引,则将当前索引i位置的entry设置为null,并且重新设置索引h处的元素为当前entry eif (h != i) {tab[i] = null;// Unlike Knuth 6.4 Algorithm R, we must scan until// null because multiple entries could have been stale.// 直到索引h处的entry为null,则设置h处的entry为当前entry ewhile (tab[h] != null)h = nextIndex(h, len);tab[h] = e;}}}return i;}
3)public void set(T value)
下面来看set方法,如下:
public void set(T value) {// 同样获取当前线程实例Thread t = Thread.currentThread();// 获取Thread下的ThreadLocalMapThreadLocalMap map = getMap(t);if (map != null)// 如果map非空则设置值map.set(this, value);else// 否则创建mapcreateMap(t, value);}
在此主要分析下map非空时的逻辑,如下:
private void set(ThreadLocal key, Object value) {Entry[] tab = table;int len = tab.length;// 同样计算出当前索引值iint i = key.threadLocalHashCode & (len-1);// 判断当前索引i处是否存在entryfor (Entry e = tab[i] ; e != null ; e = tab[i = nextIndex(i, len)]) {// 如果存在ThreadLocal k = e.get();// 如果当前key等于e.get(),那么设置为当前的Valueif (k == key) {e.value = value;return;}// 如果当前entry的key为null,则替换if (k == null) {replaceStaleEntry(key, value, i);return;}}// 如果当前索引i处的entry为null,则创建entry,并赋值给当前索引tab[i] = new Entry(key, value);int sz = ++size;// 清理部分失效的元素,并判断达到重新调整数组大小的要求if (!cleanSomeSlots(i, sz) && sz >= threshold)// 重新计算rehash();}
3.1 其中当索引i处entry非空,但是key为null时,执行replaceStaleEntry,如下:
private void replaceStaleEntry(ThreadLocal key, Object value, int staleSlot) {Entry[] tab = table;int len = tab.length;Entry e;int slotToExpunge = staleSlot;// 获取当前索引staleSlot之前 且 entry不等于null 且entry的key为null的索引slotToExpungefor (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len))if (e.get() == null)slotToExpunge = i;// 获取当前索引staleSlot之后的下一个索引,如果entry不等于空for (int i = nextIndex(staleSlot, len);(e = tab[i]) != null; i = nextIndex(i, len)) {ThreadLocal k = e.get();// 如果当前索引i下的key等于当前ThreadLocal ,则将当前entry的value设置为入参value,且设置数组索引staleSlot处的值为entryif (k == key) {e.value = value;// 将当前索引staleSlot的旧值赋值给tab[i]tab[i] = tab[staleSlot];// tab[i]下的值赋给当前tab[staleSlottab[staleSlot] = e;// 如果staleSlot前边的索引处不存在无效的元素(前边索引处的entry为null 或者 entry的key不为null)那么将需要清除的索引设置为iif (slotToExpunge == staleSlot)slotToExpunge = i;// 清除失效的索引元素cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);// 返回return;}// 如果索引i下entry的key等于null 并且 // 如果staleSlot前边的索引处不存在无效的元素(前边索引处的entry为null 或者 entry的key不为null)// 那么将需要清除的索引设置为iif (k == null && slotToExpunge == staleSlot)slotToExpunge = i;}// 如果入参中的key没有找到,则创建entrytab[staleSlot].value = null;tab[staleSlot] = new Entry(key, value);// 如果当前还存在失效的entry,那么清除他们if (slotToExpunge != staleSlot)cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);}
其中方法expungeStaleEntry(slotToExpunge)已经分析过,作用就是清除索引slotToExpunge下的元素,并且循环判断索引slotToExpunge的下一个索引处是否存在无效的元素,存在则清除,然后该方法返回最后一次执行清理后的索引i,再调用cleanSomeSlots(int i, int n)方法,如下:
private boolean cleanSomeSlots(int i, int n) {boolean removed = false;Entry[] tab = table;int len = tab.length;do {// 获取下一个索引处的entryi = nextIndex(i, len);Entry e = tab[i];// 如果当前entry无效,那么删除当前索引i处的元素,并且n重新恢复到数组的长度lenif (e != null && e.get() == null) {n = len;removed = true;i = expungeStaleEntry(i);}// 当n除以2不等于0} while ( (n >>>= 1) != 0);return removed;}
3.2 如果当前索引i处的entry为null,则创建entry,并赋值给当前索引元素。当没有清理元素,并且当前数组的长度大于resize阈值时进行重新rehash操作,如下:
private void rehash() {// 进行重新rehash之前,先清理一下无效元素expungeStaleEntries();// 到达这个条件、则重新设置数组大小if (size >= threshold - threshold / 4)resize();}
重新rehash之前,再次清理一遍
// 该方法循环便利数组,查找所有无效的元素进行清理private void expungeStaleEntries() {Entry[] tab = table;int len = tab.length;for (int j = 0; j < len; j++) {Entry e = tab[j];if (e != null && e.get() == null)expungeStaleEntry(j);}}
resize方法如下:
private void resize() {Entry[] oldTab = table;int oldLen = oldTab.length;// 新数组变为原来的2倍,保证为2的n次方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) {// 此处如果key无效了,则设置为null,等待GCe.value = null;} else {// 重新计算索引hint h = k.threadLocalHashCode & (newLen - 1);while (newTab[h] != null)h = nextIndex(h, newLen);newTab[h] = e;count++;}}}// 重新设置当前resize阈值setThreshold(newLen);size = count;table = newTab;}
4)public void remove()
remove方法的代码就比较少了,如下:
public void remove() {ThreadLocalMap m = getMap(Thread.currentThread());if (m != null)// 如果当前map非空,执行removem.remove(this);}调用map下的remove方法,如下:
private void remove(ThreadLocal key) {Entry[] tab = table;int len = tab.length;// 获取当前key的索引int i = key.threadLocalHashCode & (len-1);for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {// 找到当前key后清理if (e.get() == key) {e.clear();expungeStaleEntry(i);return;}}}
注意在线程池中使用ThreadLocal,当业务处理完成时,最好显式的调用remove()方法
3、总结
就不总结了,懒!阅读全文
0 0
- ThreadLocal 分析
- ThreadLocal 分析
- ThreadLocal-分析
- ThreadLocal-分析
- ThreadLocal分析
- ThreadLocal分析
- ThreadLocal使用分析
- JDK ThreadLocal分析:
- (转) ThreadLocal-分析-总结
- ThreadLocal-分析-总结
- ThreadLocal分析学习
- ThreadLocal分析学习
- ThreadLocal-分析-总结
- ThreadLocal源码分析
- ThreadLocal类分析
- Threadlocal源码分析
- ThreadLocal-分析-总结
- ThreadLocal源代码分析
- 线程同步工具-CyclicBarrier
- Python3.x和Python2.x的区别
- 命令行用编译(javac)并运行(java)Jav啊文件
- R语言中管道操作 %>%, %T>%, %$% 和 %<>%
- c++实现队列
- ThreadLocal分析
- (完结)Android官方培训课-支持不同的语言
- 『HIVE』hive基础学习笔记
- 数据库性能优化之SQL语句优化1
- Spring mvc中Controller参数绑定注解详解
- HDU 1166 敌兵布阵 zwk线段树
- dbvisualizer 如何将Exel数据导入数据库表中
- 欢迎使用CSDN-markdown编辑器
- js 合并数组