ThreadLocal
不少同窗都搞不懂是什么东西,能够用来干吗。但面试时却又常常问到,因此此次我和你们一块儿学习ThreadLocal
这个类。java
下面我就以面试问答的形式学习咱们的——ThreadLocal
类(源码分析基于JDK8)程序员
本文同步发布于简书 :www.jianshu.com/p/807686414…面试
问:ThreadLocal
了解吗?您能给我说说他的主要用途吗?算法
答:设计模式
从JAVA官方对ThreadLocal
类的说明定义(定义在示例代码中):ThreadLocal
类用来提供线程内部的局部变量。这种变量在多线程环境下访问(经过get
和set
方法访问)时能保证各个线程的变量相对独立于其余线程内的变量。ThreadLocal
实例一般来讲都是private static
类型的,用于关联线程和线程上下文。数组
咱们能够得知ThreadLocal
的做用是:ThreadLocal
的做用是提供线程内的局部变量,不一样的线程之间不会相互干扰,这种变量在线程的生命周期内起做用,减小同一个线程内多个函数或组件之间一些公共变量的传递的复杂度。安全
上述能够概述为:ThreadLocal
提供线程内部的局部变量,在本线程内随时随地可取,隔离其余线程。bash
示例代码:多线程
/**
* 该类提供了线程局部 (thread-local) 变量。 这些变量不一样于它们的普通对应物,
* 由于访问某个变量(经过其 get 或 set 方法)的每一个线程都有本身的局部变量
* 它独立于变量的初始化副本。ThreadLocal 实例一般是类中的 private static 字段
* 它们但愿将状态与某一个线程(例如,用户 ID 或事务 ID)相关联。
*
* 例如,如下类生成对每一个线程惟一的局部标识符。
*
* 线程 ID 是在第一次调用 UniqueThreadIdGenerator.getCurrentThreadId() 时分配的,
* 在后续调用中不会更改。
* <pre>
* import java.util.concurrent.atomic.AtomicInteger;
*
* public class ThreadId {
* // 原子性整数,包含下一个分配的线程Thread ID
* private static final AtomicInteger nextId = new AtomicInteger(0);
*
* // 每个线程对应的Thread ID
* private static final ThreadLocal<Integer> threadId =
* new ThreadLocal<Integer>() {
* @Override protected Integer initialValue() {
* return nextId.getAndIncrement();
* }
* };
*
* // 返回当前线程对应的惟一Thread ID, 必要时会进行分配
* public static int get() {
* return threadId.get();
* }
* }
* </pre>
* 每一个线程都保持对其线程局部变量副本的隐式引用,只要线程是活动的而且 ThreadLocal 实例是可访问的
* 在线程消失以后,其线程局部实例的全部副本都会被垃圾回收,(除非存在对这些副本的其余引用)。
*
* @author Josh Bloch and Doug Lea
* @since 1.2
*/
public class ThreadLocal<T> {
·····
/**
* 自定义哈希码(仅在ThreadLocalMaps中有用)
* 可用于下降hash冲突
*/
private final int threadLocalHashCode = nextHashCode();
/**
* 生成下一个哈希码hashCode. 生成操做是原子性的. 从0开始
*
*/
private static AtomicInteger nextHashCode =
new AtomicInteger();
/**
* 表示了连续分配的两个ThreadLocal实例的threadLocalHashCode值的增量
*/
private static final int HASH_INCREMENT = 0x61c88647;
/**
* 返回下一个哈希码hashCode
*/
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
·····
}复制代码
nextHashCode()
方法就是一个原子类不停地去加上0x61c88647,这是一个很特别的数,叫斐波那契散列(Fibonacci Hashing),斐波那契又有一个名称叫黄金分割,也就是说将这个数做为哈希值的增量将会使哈希表的分布更为均匀。问:ThreadLocal
实现原理是什么,它是怎么样作到局部变量不一样的线程之间不会相互干扰的?并发
答:
一般,若是我不去看源代码的话,我猜ThreadLocal
是这样子设计的:每一个ThreadLocal
类都建立一个Map
,而后用线程的ID threadID
做为Map
的key
,要存储的局部变量做为Map
的value
,这样就能达到各个线程的值隔离的效果。这是最简单的设计方法,JDK最先期的ThreadLocal
就是这样设计的。
可是,JDK后面优化了设计方案,现时JDK8 ThreadLocal
的设计是:每一个Thread
维护一个ThreadLocalMap
哈希表,这个哈希表的key
是ThreadLocal
实例自己,value
才是真正要存储的值Object
。
这个设计与咱们一开始说的设计恰好相反,这样设计有以下几点优点:
1) 这样设计以后每一个Map
存储的Entry
数量就会变小,由于以前的存储数量由Thread
的数量决定,如今是由ThreadLocal
的数量决定。
2) 当Thread
销毁以后,对应的ThreadLocalMap
也会随之销毁,能减小内存的使用。
上述解释主要参考自:ThreadLocal和synchronized的区别?
问:您能说说ThreadLocal
经常使用操做的底层实现原理吗?如存储set(T value)
,获取get()
,删除remove()
等操做。
答:
调用get()
操做获取ThreadLocal
中对应当前线程存储的值时,进行了以下操做:
1 ) 获取当前线程Thread
对象,进而获取此线程对象中维护的ThreadLocalMap
对象。
2 ) 判断当前的ThreadLocalMap
是否存在:
ThreadLocal
为 key
,调用ThreadLocalMap
中的getEntry
方法获取对应的存储实体 e。找到对应的存储实体 e,获取存储实体 e 对应的 value
值,即为咱们想要的当前线程对应此ThreadLocal
的值,返回结果值。若是不存在,则证实此线程没有维护的ThreadLocalMap
对象,调用setInitialValue
方法进行初始化。返回setInitialValue
初始化的值。
setInitialValue
方法的操做以下:
1 ) 调用initialValue
获取初始化的值。
2 ) 获取当前线程Thread
对象,进而获取此线程对象中维护的ThreadLocalMap
对象。
3 ) 判断当前的ThreadLocalMap
是否存在:
若是存在,则调用map.set
设置此实体entry
。
若是不存在,则调用createMap
进行ThreadLocalMap
对象的初始化,并将此实体entry
做为第一个值存放至ThreadLocalMap
中。
PS:关于ThreadLocalMap
对应的相关操做,放在下一个问题详细说明。
示例代码:
/**
* 返回当前线程对应的ThreadLocal的初始值
* 此方法的第一次调用发生在,当线程经过{@link #get}方法访问此线程的ThreadLocal值时
* 除非线程先调用了 {@link #set}方法,在这种状况下,
* {@code initialValue} 才不会被这个线程调用。
* 一般状况下,每一个线程最多调用一次这个方法,
* 但也可能再次调用,发生在调用{@link #remove}方法后,
* 紧接着调用{@link #get}方法。
*
* <p>这个方法仅仅简单的返回null {@code null};
* 若是程序员想ThreadLocal线程局部变量有一个除null之外的初始值,
* 必须经过子类继承{@code ThreadLocal} 的方式去重写此方法
* 一般, 能够经过匿名内部类的方式实现
*
* @return 当前ThreadLocal的初始值
*/
protected T initialValue() {
return null;
}
/**
* 建立一个ThreadLocal
* @see #withInitial(java.util.function.Supplier)
*/
public ThreadLocal() {
}
/**
* 返回当前线程中保存ThreadLocal的值
* 若是当前线程没有此ThreadLocal变量,
* 则它会经过调用{@link #initialValue} 方法进行初始化值
*
* @return 返回当前线程对应此ThreadLocal的值
*/
public T get() {
// 获取当前线程对象
Thread t = Thread.currentThread();
// 获取此线程对象中维护的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
// 若是此map存在
if (map != null) {
// 以当前的ThreadLocal 为 key,调用getEntry获取对应的存储实体e
ThreadLocalMap.Entry e = map.getEntry(this);
// 找到对应的存储实体 e
if (e != null) {
@SuppressWarnings("unchecked")
// 获取存储实体 e 对应的 value值
// 即为咱们想要的当前线程对应此ThreadLocal的值
T result = (T)e.value;
return result;
}
}
// 若是map不存在,则证实此线程没有维护的ThreadLocalMap对象
// 调用setInitialValue进行初始化
return setInitialValue();
}
/**
* set的变样实现,用于初始化值initialValue,
* 用于代替防止用户重写set()方法
*
* @return the initial value 初始化后的值
*/
private T setInitialValue() {
// 调用initialValue获取初始化的值
T value = initialValue();
// 获取当前线程对象
Thread t = Thread.currentThread();
// 获取此线程对象中维护的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
// 若是此map存在
if (map != null)
// 存在则调用map.set设置此实体entry
map.set(this, value);
else
// 1)当前线程Thread 不存在ThreadLocalMap对象
// 2)则调用createMap进行ThreadLocalMap对象的初始化
// 3)并将此实体entry做为第一个值存放至ThreadLocalMap中
createMap(t, value);
// 返回设置的值value
return value;
}
/**
* 获取当前线程Thread对应维护的ThreadLocalMap
*
* @param t the current thread 当前线程
* @return the map 对应维护的ThreadLocalMap
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}复制代码
调用set(T value)
操做设置ThreadLocal中对应当前线程要存储的值时,进行了以下操做:
1 ) 获取当前线程Thread
对象,进而获取此线程对象中维护的ThreadLocalMap
对象。
2 ) 判断当前的ThreadLocalMap
是否存在:
若是存在,则调用map.set
设置此实体entry
。
若是不存在,则调用createMap
进行ThreadLocalMap
对象的初始化,并将此实体entry
做为第一个值存放至ThreadLocalMap
中。
示例代码:
/**
* 设置当前线程对应的ThreadLocal的值
* 大多数子类都不须要重写此方法,
* 只须要重写 {@link #initialValue}方法代替设置当前线程对应的ThreadLocal的值
*
* @param value 将要保存在当前线程对应的ThreadLocal的值
*
*/
public void set(T value) {
// 获取当前线程对象
Thread t = Thread.currentThread();
// 获取此线程对象中维护的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
// 若是此map存在
if (map != null)
// 存在则调用map.set设置此实体entry
map.set(this, value);
else
// 1)当前线程Thread 不存在ThreadLocalMap对象
// 2)则调用createMap进行ThreadLocalMap对象的初始化
// 3)并将此实体entry做为第一个值存放至ThreadLocalMap中
createMap(t, value);
}
/**
* 为当前线程Thread 建立对应维护的ThreadLocalMap.
*
* @param t the current thread 当前线程
* @param firstValue 第一个要存放的ThreadLocal变量值
*/
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}复制代码
调用remove()
操做删除ThreadLocal中对应当前线程已存储的值时,进行了以下操做:
1 ) 获取当前线程Thread
对象,进而获取此线程对象中维护的ThreadLocalMap
对象。
2 ) 判断当前的ThreadLocalMap
是否存在, 若是存在,则调用map.remove
,以当前ThreadLocal
为key
删除对应的实体entry
。
/**
* 删除当前线程中保存的ThreadLocal对应的实体entry
* 若是此ThreadLocal变量在当前线程中调用 {@linkplain #get read}方法
* 则会经过调用{@link #initialValue}进行再次初始化,
* 除非此值value是经过当前线程内置调用 {@linkplain #set set}设置的
* 这可能会致使在当前线程中屡次调用{@code initialValue}方法
*
* @since 1.5
*/
public void remove() {
// 获取当前线程对象中维护的ThreadLocalMap对象
ThreadLocalMap m = getMap(Thread.currentThread());
// 若是此map存在
if (m != null)
// 存在则调用map.remove
// 以当前ThreadLocal为key删除对应的实体entry
m.remove(this);
}复制代码
ThreadLocal
的经常使用操做实际是对线程Thread
中的ThreadLocalMap
进行操做,核心是ThreadLocalMap
这个哈希表,你能谈谈ThreadLocalMap
的内部底层实现吗?答:
ThreadLocalMap
的底层实现是一个定制的自定义HashMap
哈希表,核心组成元素有:
1 ) Entry[] table;
:底层哈希表 table, 必要时须要进行扩容,底层哈希表 table.length 长度必须是2的n次方。
2 ) int size;
:实际存储键值对元素个数 entries
3 ) int threshold;
:下一次扩容时的阈值,阈值 threshold = 底层哈希表table的长度 len * 2 / 3
。当size >= threshold
时,遍历table
并删除key
为null
的元素,若是删除后size >= threshold*3/4
时,须要对table
进行扩容(详情请查看set(ThreadLocal<?> key, Object value)
方法说明)。
其中Entry[] table;
哈希表存储的核心元素是Entry
,Entry
包含:
1 ) ThreadLocal<?> k;
:当前存储的ThreadLocal
实例对象
2 ) Object value;
:当前 ThreadLocal 对应储存的值value
须要注意的是,此Entry
继承了弱引用 WeakReference
,因此在使用ThreadLocalMap
时,发现key == null
,则意味着此key ThreadLocal
不在被引用,须要将其从ThreadLocalMap
哈希表中移除。(弱引用相关问题解释请查看 问答 5)
示例代码:
/**
* ThreadLocalMap 是一个定制的自定义 hashMap 哈希表,只适合用于维护
* 线程对应ThreadLocal的值. 此类的方法没有在ThreadLocal 类外部暴露,
* 此类是私有的,容许在 Thread 类中以字段的形式声明 ,
* 以助于处理存储量大,生命周期长的使用用途,
* 此类定制的哈希表实体键值对使用弱引用WeakReferences 做为key,
* 可是, 一旦引用不在被使用,
* 只有当哈希表中的空间被耗尽时,对应再也不使用的键值对实体才会确保被 移除回收。
*/
static class ThreadLocalMap {
/**
* 实体entries在此hash map中是继承弱引用 WeakReference,
* 使用ThreadLocal 做为 key 键. 请注意,当key为null(i.e. entry.get()
* == null) 意味着此key再也不被引用,此时实体entry 会从哈希表中删除。
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** 当前 ThreadLocal 对应储存的值value. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
/**
* 初始容量大小 16 -- 必须是2的n次方.
*/
private static final int INITIAL_CAPACITY = 16;
/**
* 底层哈希表 table, 必要时须要进行扩容.
* 底层哈希表 table.length 长度必须是2的n次方.
*/
private Entry[] table;
/**
* 实际存储键值对元素个数 entries.
*/
private int size = 0;
/**
* 下一次扩容时的阈值
*/
private int threshold; // 默认为 0
/**
* 设置触发扩容时的阈值 threshold
* 阈值 threshold = 底层哈希表table的长度 len * 2 / 3
*/
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
/**
* 获取该位置i对应的下一个位置index
*/
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
/**
* 获取该位置i对应的上一个位置index
*/
private static int prevIndex(int i, int len) {
return ((i - 1 >= 0) ? i - 1 : len - 1);
}
}复制代码
ThreadLocalMap
的构造方法是延迟加载的,也就是说,只有当线程须要存储对应的ThreadLocal
的值时,才初始化建立一次(仅初始化一次)。初始化步骤以下:
1) 初始化底层数组table
的初始容量为 16。
2) 获取ThreadLocal
中的threadLocalHashCode
,经过threadLocalHashCode & (INITIAL_CAPACITY - 1)
,即ThreadLocal 的 hash 值 threadLocalHashCode % 哈希表的长度 length 的方式计算该实体的存储位置。
3) 存储当前的实体,key 为 : 当前ThreadLocal value:真正要存储的值
4)设置当前实际存储元素个数 size 为 1
5)设置阈值setThreshold(INITIAL_CAPACITY)
,为初始化容量 16 的 2/3。
示例代码:
/**
* 用于建立一个新的hash map包含 (firstKey, firstValue).
* ThreadLocalMaps 构造方法是延迟加载的,因此咱们只会在至少有一个
* 实体entry存放时,才初始化建立一次(仅初始化一次)。
*/
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
// 初始化 table 初始容量为 16
table = new Entry[INITIAL_CAPACITY];
// 计算当前entry的存储位置
// 存储位置计算等价于:
// ThreadLocal 的 hash 值 threadLocalHashCode % 哈希表的长度 length
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
// 存储当前的实体,key 为 : 当前ThreadLocal value:真正要存储的值
table[i] = new Entry(firstKey, firstValue);
// 设置当前实际存储元素个数 size 为 1
size = 1;
// 设置阈值,为初始化容量 16 的 2/3。
setThreshold(INITIAL_CAPACITY);
}复制代码
ThreadLocal
的get()
操做实际是调用ThreadLocalMap
的getEntry(ThreadLocal<?> key)
方法,此方法快速适用于获取某一存在key
的实体 entry
,不然,应该调用getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e)
方法获取,这样作是为了最大限制地提升直接命中的性能,该方法进行了以下操做:
1 ) 计算要获取的entry
的存储位置,存储位置计算等价于:ThreadLocal
的 hash
值 threadLocalHashCode
% 哈希表的长度 length
。
2 ) 根据计算的存储位置,获取到对应的实体 Entry
。判断对应实体Entry
是否存在 而且 key
是否相等:
存在对应实体Entry
而且对应key
相等,即同一ThreadLocal
,返回对应的实体Entry
。
不存在对应实体Entry
或者 key
不相等,则经过调用getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e)
方法继续查找。
getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e)
方法操做以下:
1 ) 获取底层哈希表数组table
,循环遍历对应要查找的实体Entry
所关联的位置。
2 ) 获取当前遍历的entry
的 key ThreadLocal
,比较key
是否一致,一致则返回。
3 ) 若是key
不一致 而且 key
为 null
,则证实引用已经不存在,这是由于Entry
继承的是WeakReference
,这是弱引用带来的坑。调用expungeStaleEntry(int staleSlot)
方法删除过时的实体Entry
(此方法不单独解释,请查看示例代码,有详细注释说明)。
4 ) key
不一致 ,key
也不为空,则遍历下一个位置,继续查找。
5 ) 遍历完毕,仍然找不到则返回null
。
示例代码:
/**
* 根据key 获取对应的实体 entry. 此方法快速适用于获取某一存在key的
* 实体 entry,不然,应该调用getEntryAfterMiss方法获取,这样作是为
* 了最大限制地提升直接命中的性能
*
* @param key 当前thread local 对象
* @return the entry 对应key的 实体entry, 若是不存在,则返回null
*/
private Entry getEntry(ThreadLocal<?> key) {
// 计算要获取的entry的存储位置
// 存储位置计算等价于:
// ThreadLocal 的 hash 值 threadLocalHashCode % 哈希表
的长度 length
int i = key.threadLocalHashCode & (table.length - 1);
// 获取到对应的实体 Entry
Entry e = table[i];
// 存在对应实体而且对应key相等,即同一ThreadLocal
if (e != null && e.get() == key)
// 返回对应的实体Entry
return e;
else
// 不存在 或 key不一致,则经过调用getEntryAfterMiss继续查找
return getEntryAfterMiss(key, i, e);
}
/**
* 当根据key找不到对应的实体entry 时,调用此方法。
* 直接定位到对应的哈希表位置
*
* @param key 当前thread local 对象
* @param i 此对象在哈希表 table中的存储位置 index
* @param e the entry 实体对象
* @return the entry 对应key的 实体entry, 若是不存在,则返回null
*/
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
// 循环遍历当前位置的全部实体entry
while (e != null) {
// 获取当前entry 的 key ThreadLocal
ThreadLocal<?> k = e.get();
// 比较key是否一致,一致则返回
if (k == key)
return e;
// 找到对应的entry ,但其key 为 null,则证实引用已经不存在
// 这是由于Entry继承的是WeakReference,这是弱引用带来的坑
if (k == null)
// 删除过时(stale)的entry
expungeStaleEntry(i);
else
// key不一致 ,key也不为空,则遍历下一个位置,继续查找
i = nextIndex(i, len);
// 获取下一个位置的实体 entry
e = tab[i];
}
// 遍历完毕,找不到则返回null
return null;
}
/**
* 删除对应位置的过时实体,并删除此位置后对应相关联位置key = null的实体
*
* @param staleSlot 已知的key = null 的对应的位置索引
* @return 对应过时实体位置索引的下一个key = null的位置
* (全部的对应位置都会被检查)
*/
private int expungeStaleEntry(int staleSlot) {
// 获取对应的底层哈希表 table
Entry[] tab = table;
// 获取哈希表长度
int len = tab.length;
// 擦除这个位置上的脏数据
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// 直到咱们找到 Entry e = null,才执行rehash操做
// 就是遍历完该位置的全部关联位置的实体
Entry e;
int i;
// 查找该位置对应全部关联位置的过时实体,进行擦除操做
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
// 咱们必须一直遍历直到最后
// 由于还可能存在多个过时的实体
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
/**
* 删除全部过时的实体
*/
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);
}
}复制代码
ThreadLocal
的set(T value)
操做实际是调用ThreadLocalMap
的set(ThreadLocal<?> key, Object value)
方法,该方法进行了以下操做:
1 ) 获取对应的底层哈希表table
,计算对应threalocal
的存储位置。
2 ) 循环遍历table
对应该位置的实体,查找对应的threadLocal
。
3 ) 获取当前位置的threadLocal
,若是key threadLocal
一致,则证实找到对应的threadLocal
,将新值赋值给找到的当前实体Entry
的value
中,结束。
4 ) 若是当前位置的key threadLocal
不一致,而且key threadLocal
为null
,则调用replaceStaleEntry(ThreadLocal<?> key, Object value,int staleSlot)
方法(此方法不单独解释,请查看示例代码,有详细注释说明),替换该位置key == null
的实体为当前要设置的实体,结束。
5 ) 若是当前位置的key threadLocal
不一致,而且key threadLocal
不为null
,则建立新的实体,并存放至当前位置 i tab[i] = new Entry(key, value);
,实际存储键值对元素个数size + 1
,因为弱引用带来了这个问题,因此要调用cleanSomeSlots(int i, int n)
方法清除无用数据(此方法不单独解释,请查看示例代码,有详细注释说明),才能判断如今的size
有没有达到阀值threshhold
,若是没有要清除的数据,存储元素个数仍然 大于 阈值 则调用rehash
方法进行扩容(此方法不单独解释,请查看示例代码,有详细注释说明)。
示例代码:
/**
* 设置对应ThreadLocal的值
*
* @param key 当前thread local 对象
* @param value 要设置的值
*/
private void set(ThreadLocal<?> key, Object value) {
// 咱们不会像get()方法那样使用快速设置的方式,
// 由于一般不多使用set()方法去建立新的实体
// 相对于替换一个已经存在的实体, 在这种状况下,
// 快速设置方案会常常失败。
// 获取对应的底层哈希表 table
Entry[] tab = table;
// 获取哈希表长度
int len = tab.length;
// 计算对应threalocal的存储位置
int i = key.threadLocalHashCode & (len-1);
// 循环遍历table对应该位置的实体,查找对应的threadLocal
for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {
// 获取当前位置的ThreadLocal
ThreadLocal<?> k = e.get();
// 若是key threadLocal一致,则证实找到对应的threadLocal
if (k == key) {
// 赋予新值
e.value = value;
// 结束
return;
}
// 若是当前位置的key threadLocal为null
if (k == null) {
// 替换该位置key == null 的实体为当前要设置的实体
replaceStaleEntry(key, value, i);
// 结束
return;
}
}
// 当前位置的k != key && k != null
// 建立新的实体,并存放至当前位置i
tab[i] = new Entry(key, value);
// 实际存储键值对元素个数 + 1
int sz = ++size;
// 因为弱引用带来了这个问题,因此先要清除无用数据,才能判断如今的size有没有达到阀值threshhold
// 若是没有要清除的数据,存储元素个数仍然 大于 阈值 则扩容
if (!cleanSomeSlots(i, sz) && sz >= threshold)
// 扩容
rehash();
}
/**
* 当执行set操做时,获取对应的key threadLocal,并替换过时的实体
* 将这个value值存储在对应key threadLocal的实体中,不管是否已经存在体
* 对应的key threadLocal
*
* 有一个反作用, 此方法会删除该位置下和该位置nextIndex对应的全部过时的实体
*
* @param key 当前thread local 对象
* @param value 当前thread local 对象对应存储的值
* @param staleSlot 第一次找到此过时的实体对应的位置索引index
* .
*/
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
// 获取对应的底层哈希表 table
Entry[] tab = table;
// 获取哈希表长度
int len = tab.length;
Entry e;
// 往前找,找到table中第一个过时的实体的下标
// 清理整个table是为了不由于垃圾回收带来的连续增加哈希的危险
// 也就是说,哈希表没有清理干净,当GC到来的时候,后果很严重
// 记录要清除的位置的起始首位置
int slotToExpunge = staleSlot;
// 从该位置开始,往前遍历查找第一个过时的实体的下标
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;
// 找到key一致的ThreadLocal或找到一个key为 null的
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
// 若是咱们找到了key,那么咱们就须要把它跟新的过时数据交换来保持哈希表的顺序
// 那么剩下的过时Entry呢,就能够交给expungeStaleEntry方法来擦除掉
// 将新设置的实体放置在此过时的实体的位置上
if (k == key) {
// 替换,将要设置的值放在此过时的实体中
e.value = value;
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
// 若是存在,则开始清除以前过时的实体
if (slotToExpunge == staleSlot)
slotToExpunge = i;
// 在这里开始清除过时数据
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
// / 若是咱们没有在日后查找中找没有找到过时的实体,
// 那么slotToExpunge就是第一个过时Entry的下标了
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
// 最后key仍没有找到,则将要设置的新实体放置
// 在原过时的实体对应的位置上。
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
// 若是该位置对应的其余关联位置存在过时实体,则清除
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
/**
* 启发式的扫描查找一些过时的实体并清除,
* 此方法会再添加新实体的时候被调用,
* 或者过时的元素被清除时也会被调用.
* 若是实在没有过时数据,那么这个算法的时间复杂度就是O(log n)
* 若是有过时数据,那么这个算法的时间复杂度就是O(n)
*
* @param i 一个肯定不是过时的实体的位置,从这个位置i开始扫描
*
* @param n 扫描控制: 有{@code log2(n)} 单元会被扫描,
* 除非找到了过时的实体, 在这种状况下
* 有{@code log2(table.length)-1} 的格外单元会被扫描.
* 当调用插入时, 这个参数的值是存储实体的个数,
* 但若是调用 replaceStaleEntry方法, 这个值是哈希表table的长度
* (注意: 全部的这些均可能或多或少的影响n的权重
* 可是这个版本简单,快速,并且彷佛执行效率还能够)
*
* @return true 返回true,若是有任何过时的实体被删除。
*/
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) {
n = len;
removed = true;
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0);
return removed;
}
/**
* 哈希表扩容方法
* 首先扫描整个哈希表table,删除过时的实体
* 缩小哈希表table大小 或 扩大哈希表table大小,扩大的容量是加倍.
*/
private void rehash() {
// 删除全部过时的实体
expungeStaleEntries();
// 使用较低的阈值threshold加倍以免滞后
// 存储实体个数 大于等于 阈值的3/4则扩容
if (size >= threshold - threshold / 4)
resize();
}
/**
* 扩容方法,以2倍的大小进行扩容
* 扩容的思想跟HashMap很类似,都是把容量扩大两倍
* 不一样之处仍是由于WeakReference带来的
*/
private void resize() {
// 记录旧的哈希表
Entry[] oldTab = table;
// 记录旧的哈希表长度
int oldLen = oldTab.length;
// 新的哈希表长度为旧的哈希表长度的2倍
int newLen = oldLen * 2;
// 建立新的哈希表
Entry[] newTab = new Entry[newLen];
int count = 0;
// 逐一遍历旧的哈希表table的每一个实体,从新分配至新的哈希表中
for (int j = 0; j < oldLen; ++j) {
// 获取对应位置的实体
Entry e = oldTab[j];
// 若是实体不会null
if (e != null) {
// 获取实体对应的ThreadLocal
ThreadLocal<?> k = e.get();
// 若是该ThreadLocal 为 null
if (k == null) {
// 则对应的值也要清除
// 就算是扩容,也不能忘了为擦除过时数据作准备
e.value = null; // Help the GC
} else {
// 若是不是过时实体,则根据新的长度从新计算存储位置
int h = k.threadLocalHashCode & (newLen - 1);
// 将该实体存储在对应ThreadLocal的最后一个位置
while (newTab[h] != null)
h = nextIndex(h, newLen);
newTab[h] = e;
count++;
}
}
}
// 从新分配位置完毕,则从新计算阈值Threshold
setThreshold(newLen);
// 记录实际存储元素个数
size = count;
// 将新的哈希表赋值至底层table
table = newTab;
}复制代码
ThreadLocal
的remove()
操做实际是调用ThreadLocalMap
的remove(ThreadLocal<?> key)
方法,该方法进行了以下操做:
1 ) 获取对应的底层哈希表 table
,计算对应threalocal
的存储位置。
2 ) 循环遍历table
对应该位置的实体,查找对应的threadLocal
。
3 ) 获取当前位置的threadLocal
,若是key threadLocal
一致,则证实找到对应的threadLocal
,执行删除操做,删除此位置的实体,结束。
示例代码:
/**
* 移除对应ThreadLocal的实体
*/
private void remove(ThreadLocal<?> key) {
// 获取对应的底层哈希表 table
Entry[] tab = table;
// 获取哈希表长度
int len = tab.length;
// 计算对应threalocal的存储位置
int i = key.threadLocalHashCode & (len-1);
// 循环遍历table对应该位置的实体,查找对应的threadLocal
for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {
// 若是key threadLocal一致,则证实找到对应的threadLocal
if (e.get() == key) {
// 执行清除操做
e.clear();
// 清除此位置的实体
expungeStaleEntry(i);
// 结束
return;
}
}
}复制代码
问:ThreadLocalMap
中的存储实体Entry
使用ThreadLocal
做为key
,但这个Entry
是继承弱引用WeakReference
的,为何要这样设计,使用了弱引用WeakReference
会形成内存泄露问题吗?
答:
咱们在正常状况下,广泛使用的是强引用:
A a = new A();
B b = new B();复制代码
当 a = null;b = null;
时,一段时间后,JAVA垃圾回收机制GC会将 a 和 b 对应所分配的内存空间给回收。
但考虑这样一种状况:
C c = new C(b);
b = null;复制代码
当 b 被设置成null
时,那么是否意味这一段时间后GC工做能够回收 b 所分配的内存空间呢?答案是否认的,由于即便 b 被设置成null
,但 c 仍然持有对 b 的引用,并且仍是强引用,因此GC不会回收 b 原先所分配的空间,既不能回收,又不能使用,这就形成了 内存泄露。
那么如何处理呢?
能够经过c = null;
,也可使用弱引用WeakReference w = new WeakReference(b);
。由于使用了弱引用WeakReference
,GC是能够回收 b 原先所分配的空间的。
上述解释主要参考自:对ThreadLocal实现原理的一点思考
ThreadLocal
的层面上,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
的设计中已经考虑到这种状况,也加上了一些防御措施:在ThreadLocal
的get()
,set()
,remove()
的时候都会清除线程ThreadLocalMap
里全部key
为null
的value
。
可是这些被动的预防措施并不能保证不会内存泄漏:
使用static
的ThreadLocal
,延长了ThreadLocal
的生命周期,可能致使的内存泄漏(参考ThreadLocal 内存泄露的实例分析)。
分配使用了ThreadLocal
又再也不调用get()
,set()
,remove()
方法,那么就会致使内存泄漏。
从表面上看内存泄漏的根源在于使用了弱引用。网上的文章大多着重分析ThreadLocal
使用了弱引用会致使内存泄漏,可是另外一个问题也一样值得思考:为何使用弱引用而不是强引用?
咱们先来看看官方文档的说法:
To help deal with very large and long-lived usages,
the hash table entries use WeakReferences for keys.复制代码
为了应对很是大和长时间的用途,哈希表使用弱引用的 key
。
下面咱们分两种状况讨论:
key
使用强引用:引用的ThreadLocal
的对象被回收了,可是ThreadLocalMap
还持有ThreadLocal
的强引用,若是没有手动删除,ThreadLocal
不会被回收,致使Entry
内存泄漏。
key
使用弱引用:引用的ThreadLocal
的对象被回收了,因为ThreadLocalMap
持有ThreadLocal
的弱引用,即便没有手动删除,ThreadLocal
也会被回收。value
在下一次ThreadLocalMap
调用get()
,set()
,remove()
的时候会被清除。
比较两种状况,咱们能够发现:因为ThreadLocalMap
的生命周期跟Thread
同样长,若是都没有手动删除对应key
,都会致使内存泄漏,可是使用弱引用能够多一层保障:弱引用ThreadLocal
不会内存泄漏,对应的value
在下一次ThreadLocalMap
调用get()
,set()
,remove()
的时候会被清除。
所以,ThreadLocal
内存泄漏的根源是:因为ThreadLocalMap
的生命周期跟Thread
同样长,若是没有手动删除对应key
就会致使内存泄漏,而不是由于弱引用。
综合上面的分析,咱们能够理解ThreadLocal
内存泄漏的来龙去脉,那么怎么避免内存泄漏呢?
每次使用完ThreadLocal
,都调用它的remove()
方法,清除数据。
在使用线程池的状况下,没有及时清理ThreadLocal
,不只是内存泄漏的问题,更严重的是可能致使业务逻辑出现问题。因此,使用ThreadLocal
就跟加锁完要解锁同样,用完就清理。
上述解释主要参考自:深刻分析 ThreadLocal 内存泄漏问题
问:ThreadLocal
和synchronized
的区别?
答:ThreadLocal
和synchronized
关键字都用于处理多线程并发访问变量的问题,只是两者处理问题的角度和思路不一样。
ThreadLocal
是一个Java类,经过对当前线程中的局部变量的操做来解决不一样线程的变量访问的冲突问题。因此,ThreadLocal
提供了线程安全的共享对象机制,每一个线程都拥有其副本。
Java中的synchronized
是一个保留字,它依靠JVM的锁机制来实现临界区的函数或者变量的访问中的原子性。在同步机制中,经过对象的锁机制保证同一时间只有一个线程访问变量。此时,被用做“锁机制”的变量时多个线程共享的。
synchronized
关键字)采用了以“时间换空间”的方式,提供一份变量,让不一样的线程排队访问。而ThreadLocal
采用了“以空间换时间”的方式,为每个线程都提供一份变量的副本,从而实现同时访问而互不影响。问:ThreadLocal
在现时有什么应用场景?
答:总的来讲ThreadLocal
主要是解决2种类型的问题:
解决并发问题:使用ThreadLocal
代替synchronized
来保证线程安全。同步机制采用了“以时间换空间”的方式,而ThreadLoca
l采用了“以空间换时间”的方式。前者仅提供一份变量,让不一样的线程排队访问,然后者为每个线程都提供了一份变量,所以能够同时访问而互不影响。
解决数据存储问题:ThreadLocal
为变量在每一个线程中都建立了一个副本,因此每一个线程能够访问本身内部的副本变量,不一样线程之间不会互相干扰。如一个Parameter
对象的数据须要在多个模块中使用,若是采用参数传递的方式,显然会增长模块之间的耦合性。此时咱们可使用ThreadLocal
解决。
应用场景:
Spring
使用ThreadLocal
解决线程安全问题
咱们知道在通常状况下,只有无状态的Bean
才能够在多线程环境下共享,在Spring
中,绝大部分Bean
均可以声明为singleton
做用域。就是由于Spring
对一些Bean
(如RequestContextHolder
、TransactionSynchronizationManager
、LocaleContextHolder
等)中非线程安全状态采用ThreadLocal
进行处理,让它们也成为线程安全的状态,由于有状态的Bean
就能够在多线程中共享了。
通常的Web
应用划分为展示层、服务层和持久层三个层次,在不一样的层中编写对应的逻辑,下层经过接口向上层开放功能调用。在通常状况下,从接收请求到返回响应所通过的全部程序调用都同属于一个线程ThreadLocal
是解决线程安全问题一个很好的思路,它经过为每一个线程提供一个独立的变量副本解决了变量并发访问的冲突问题。在不少状况下,ThreadLocal
比直接使用synchronized
同步机制解决线程安全问题更简单,更方便,且结果程序拥有更高的并发性。
示例代码:
public abstract class RequestContextHolder {
····
private static final boolean jsfPresent =
ClassUtils.isPresent("javax.faces.context.FacesContext", RequestContextHolder.class.getClassLoader());
private static final ThreadLocal<RequestAttributes> requestAttributesHolder =
new NamedThreadLocal<RequestAttributes>("Request attributes");
private static final ThreadLocal<RequestAttributes> inheritableRequestAttributesHolder =
new NamedInheritableThreadLocal<RequestAttributes>("Request context");
·····
}复制代码
ThreadLocal
提供线程内部的局部变量,在本线程内随时随地可取,隔离其余线程。
ThreadLocal
的设计是:每一个Thread
维护一个ThreadLocalMap
哈希表,这个哈希表的key
是ThreadLocal
实例自己,value
才是真正要存储的值Object
。
对ThreadLocal
的经常使用操做实际是对线程Thread
中的ThreadLocalMap
进行操做。
ThreadLocalMap
的底层实现是一个定制的自定义HashMap
哈希表,ThreadLocalMap
的阈值threshold
= 底层哈希表table
的长度 len * 2 / 3
,当实际存储元素个数size
大于或等于 阈值threshold
的 3/4
时size >= threshold*3/4
,则对底层哈希表数组table
进行扩容操做。
ThreadLocalMap
中的哈希表Entry[] table
存储的核心元素是Entry
,存储的key
是ThreadLocal
实例对象,value
是ThreadLocal
对应储存的值value
。须要注意的是,此Entry
继承了弱引用 WeakReference
,因此在使用ThreadLocalMap
时,发现key == null
,则意味着此key ThreadLocal
不在被引用,须要将其从ThreadLocalMap
哈希表中移除。
ThreadLocalMap
使用ThreadLocal
的弱引用做为key
,若是一个ThreadLocal
没有外部强引用来引用它,那么系统 GC 的时候,这个ThreadLocal
势必会被回收。因此,在ThreadLocal
的get()
,set()
,remove()
的时候都会清除线程ThreadLocalMap
里全部key
为null
的value
。若是咱们不主动调用上述操做,则会致使内存泄露。
为了安全地使用ThreadLocal
,必需要像每次使用完锁就解锁同样,在每次使用完ThreadLocal
后都要调用remove()
来清理无用的Entry
。这在操做在使用线程池时尤其重要。
ThreadLocal
和synchronized
的区别:同步机制(synchronized
关键字)采用了以“时间换空间”的方式,提供一份变量,让不一样的线程排队访问。而ThreadLocal
采用了“以空间换时间”的方式,为每个线程都提供一份变量的副本,从而实现同时访问而互不影响。
ThreadLocal
主要是解决2种类型的问题:A. 解决并发问题:使用ThreadLocal
代替同步机制解决并发问题。B. 解决数据存储问题:如一个Parameter
对象的数据须要在多个模块中使用,若是采用参数传递的方式,显然会增长模块之间的耦合性。此时咱们可使用ThreadLocal
解决。
深刻浅出ThreadLocal
ThreadLocal和synchronized的区别?
深刻剖析ThreadLocal
ThreadLocal内部机制
聊一聊Spring中的线程安全性
对ThreadLocal实现原理的一点思考
深刻分析 ThreadLocal 内存泄漏问题
学习Spring必学的Java基础知识(6)----ThreadLocal
ThreadLocal设计模式
ThreadLocal案例分析
Spring单例模式与线程安全ThreadLocal