目录
- 1. 简介
- 2. 基本使用
- 3. ThreadLocal原理说明
- 4. ThreadLocal源码解析
- 5. ThreadLocalMap源码解析
- 5.1 成员变量
- 5.2 内部类
- 5.3 构造方法
- 5.4 成员方法
- 5.4.1 nextIndex(int i, int len)
- 5.4.2 prevIndex(int i, int len)
- 5.4.3 getEntry(ThreadLocal<?> key)
- 5.4.4 getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e)方法
- 5.4.5 expungeStaleEntry(int staleSlot)方法
- 5.4.6 set(ThreadLocal<?> key, Object value)方法
- 5.4.7 replaceStaleEntry(ThreadLocal<?> key, Object value,int staleSlot)方法
- 5.4.8 cleanSomeSlots(int i, int n)方法
- 5.4.9 rehash()方法
- 5.4.10 resize()方法
- 5.4.11 remove(ThreadLocal<?> key)方法
1. 简介
ThreadLocal用于提供线程独有的变量,只有当前自身线程可以访问,而其他线程不能访问,极大的方便了一些逻辑的实现。
ThreadLocal(不同的线程有不同的数据,不存在并发问题)和Synchronized都可以解决并发问题,但是ThreadLocal是用于数据隔离的,而Synchronized是用于数据共享的。
2. 基本使用
package ThreadLocal;
/**
* 让不同线程在设置完之后再输出,保证从输出开始之后都不会有设置操作
* 如果不同的线程输出的结果不同,则说明ThreadLocal确实实现了线程
* 间的数据隔离
*/
public class Demo01 {
static ThreadLocal<String> threadLocal = new ThreadLocal<>();
// 获取当前线程的值
public static void getThread(){
System.out.println(threadLocal.get());
threadLocal.remove();
}
// 设置不同线程的值
public static void setThread(){
threadLocal.set(Thread.currentThread().getName());
}
public static void main(String[] args) {
new Thread(() -> {
Demo01.setThread();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Demo01.getThread();
}, "A").start();
new Thread(() -> {
Demo01.setThread();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Demo01.getThread();
}, "B").start();
}
}
结果:
从上面的结果确实可以看出ThreadLocal实现了线程之间数据的隔离。接下来我们就来说说ThreadLocal是怎样实现的吧!
3. ThreadLocal原理说明
每个Thread内部都有一个Map(也就是下面要讲的ThreadLocalMap),每当调用ThreadLocal对象的get()方法时,相当于往Thread的ThreadLocalMap里面传入了一个键值对(key-value),key就是ThreadLocal对象,value就是我们要存储在线程中的值。
4. ThreadLocal源码解析
4.1 成员变量
/*
threadLocalHashCode用于线程的ThreadLocalMap对象的桶位寻址,
在第一次在某个ThreadLocal对象上调用get()方法时,会给当前线程分配一个
value,当前ThreadLocal对象会和这个value包装成一个Entry,存储在Thread对象的
ThreadLocalMap(Map)对象中,threadLocalHashCode就是ThreadLocal对象的
哈希值,在寻址时通过threadLocalHashCode&(table.length - 1)方法寻址。
注:table就是ThreadLocalMap底层的Entry数组
*/
// 这个是ThreadLocal对象的哈希值,通过调用nextHashCode()方法获取
private final int threadLocalHashCode = nextHashCode();
// 存储哈希值的变量,属性是AtomicInteger原子类,是一个静态变量
private static AtomicInteger nextHashCode = new AtomicInteger();
// 每一次nextHashCode增加的数值大小,表示哈希值的增量
// 每创建一个ThreadLocal对象,nextHashCode就会增长HASH_INCREMENT
// HASH_INCREMENT是一个黄金分割数,哈希增量为这个值
// 可以使Map中的数分布均匀
private static final int HASH_INCREMENT = 0x61c88647;
// 获取哈希值
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
// 初始化一个起始value,,一般这个方法都是会重写的
protected T initialValue() {
return null;
}
4.2 构造方法
// 只有一个空参构造方法
public ThreadLocal() {
}
4.3 成员方法
4.3.1 get()及相关的方法
获取当前线程内部ThreadLocalMap中key为ThreadLocal对象的value。
public T get() {
// 获取当前线程
Thread t = Thread.currentThread();
// 获取当前线程内部的ThreadLocalMap
ThreadLocalMap map = getMap(t);
// 如果当前线程已经初始化了自己的ThreadLocalMap对象,就尝试在里面寻找值
if (map != null) {
// 获取ThreadLocalMap中key为ThreadLocal对象的键值对
ThreadLocalMap.Entry e = map.getEntry(this);
// 如果键值对不等于null,直接返回值
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 执行到这里有几种情况
// 1. 当前线程没有初始化过自己的ThreadLocalMap对象
// 2. 当前线程初始化了自己的ThreadLocalMap对象,但是没有生成当前线程与ThreadLocal相关联的变量
// setInitialValue() 会先检查当前Thread对象是否初始化了自己的ThreadLocalMap,如果没有初始化
// 就调用createMap(t, value)初始化并在createMap方法内部添加线程与ThreadLoca相关联的值
// 如果已经初始化,就调用ThreadLocalMap对象的set方法设置值
return setInitialValue();
}
private T setInitialValue() {
// 获取当前初始化的值
T value = initialValue();
// 获取当前线程
Thread t = Thread.currentThread();
// 获取当前线程的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
// 如果已经初始化,就调用ThreadLocalMap的set方法往里面设置值
if (map != null)
map.set(this, value);
else
// 如果没有初始化,就调用createMap(t, value)方法在里面抵用ThreadLocalMap的
// 构造方法(构造方法会初始化并设置值)
createMap(t, value);
// 返回值
return value;
}
// 获取线程的ThreadLocalMap对象,也就是threadLocals属性
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
// 调用ThreadLocalMap的构造方法创建并在里面设置一个值
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
4.3.2 set(T value)
设置线程与ThreadLocal对象相关联的值,对于同一个ThreadLocal对象和同一个Thread来说,如果多次调用,会覆盖掉以前的值,只会保留最后一次的值。
public void set(T value) {
// 获取当前线程
Thread t = Thread.currentThread();
// 获取当前线程的ThreadLocal对象
ThreadLocalMap map = getMap(t);
// 如果当前线程的ThreadLocalMap对象不为空,就调用ThreadLocal对象的set方法设置一个值
if (map != null)
map.set(this, value);
else
// 如果当前线程的ThreadLocalMap对象为空,就调用createMap(t, value)方法(createMap对象内部会初始化ThreadLocalMap
// 并设置一个值)
createMap(t, value);
}
4.3.3 remove()方法
移除当前线程与当前ThreadLocal对象相关联的值。
public void remove() {
// 获取当前线程的ThreadLocalMap对象
ThreadLocalMap m = getMap(Thread.currentThread());
// 如果ThreadLocalMap对象不为空,调用ThreadLocalMap对象的remove方法移除
if (m != null)
m.remove(this);
}
ThreadLocal的方法很少,最主要的是ThreadLocalMap内部的方法,ThreadLocalMap是ThreadLocal的内部类。
5. ThreadLocalMap源码解析
ThreadLocalMap是ThreadLocal的内部类,每一个Thread内部都会有一个ThreadLocalMap对象,ThreadLocalMap对象可以看成一个Map,但是会与我们平常使用的Map有一些区别,ThreadLocalMap内部采用线性探测法处理哈希冲突,这些我们下面都会讲到。
5.1 成员变量
// ThreadLocalMap内部是一个Entry数组,INITIAL_CAPACITY就是这个Entry数组的容量大小
private static final int INITIAL_CAPACITY = 16;
// table就是ThreadLocalMap内部的Entry数组
private Entry[] table;
// 当前数组占用情况
private int size = 0;
// 扩容阈值,初始值为 len * 2 / 3
// 触发后调用rehash()方法,rehash()方法
// 先做一次全面检查过期数据,把数组中所有过期的Entry移除,
// 如果移除之后,数组中的Entry个数仍然达到threshold的3/4就扩容
private int threshold; // Default to 0
// 将扩容阈值设置为当前Entry数组长度的2/3
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
/*
注:ThreadLocalMap虽然可以看成一个Map,但是其内部并不是采用拉链法来解决哈希冲突的,而是采用
线性探测法来解决哈希冲突的,每当遇到哈希冲突,往后移一位,直到没有出现哈希冲突,然后将键值对
存储进去
*/
5.2 内部类
内部类Entry继承了弱引用,弱引用的对象下一次垃圾回收时一定会把回收掉。
key采用的是弱引用,value采用的是强引用。
Entry的key为什么设置为弱引用呢?
- 如果key为强引用,当ThreadLocal对象被回收时,ThreadLocalMap中的Entry如果没有被手动回收,会造成内存泄漏问题。
- 但是如果key为弱引用,当ThreadLocal对象被回收时,ThreadLocalMap中的Entry可以被自动回收。还有一点当ThreadLocalMap的一些key被回收后,ThreadLocalMap可以区分哪些是过期的,那些不是过期的。
Entry的value为什么设置为强引用呢?
Entry的value设置为强引用的原因是如果采用弱引用假如往ThreadLocalMap里面存了一个value,GC过后value便消失了,也就达不到存储全局的效果了。
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
5.3 构造方法
线程的ThreadLocalMap是延迟初始化的,只有当线程第一次存储key-value键值对时才会进行初始化。
// 传入一个键值对,键为ThreadLocal对象,值为当前线程与当前ThreadLocal对象相关联的值
// firstKey:ThreadLocal对象
// firstValue:当前线程与当前ThreadLocal对象相关联的值
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
// 创建一个容量为初始容量INITIAL_CAPACITY(16)的Entry数组
table = new Entry[INITIAL_CAPACITY];
// 获取键值对应该存储在Entry数组中的位置
// 通过ThreadLocal对象的哈希值(也就是ThreadLocal对象的threadLocalHashCode实现)
// &
// Entry数组的长度-1(也就是INITIAL_CAPACITY - 1)求取
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
// 设置存储的元素个数为1
size = 1;
// 设置扩容阈值,将当前Entry数组的长度传进去,在setThreshold方法内部会将阈值设置为Entry数组长度 * 2 / 3
setThreshold(INITIAL_CAPACITY);
}
// 传入一个ThreadLocalMap对象
// 内部其实就是创建了一个和传入的ThreadLocalMap内部的Entry数组相同长度的Entry数组
// 并将值一个一个赋值进去
private ThreadLocalMap(ThreadLocalMap parentMap) {
// 获取传入的ThreadLocalMap对象内部的Entry数组
Entry[] parentTable = parentMap.table;
// 获取传入的ThreadLocalMap对象内部的Entry数组长度
int len = parentTable.length;
// 设置扩容阈值
setThreshold(len);
// 在当前ThreadLocalMap里面new一个相同大小的Entry数组
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++;
}
}
}
}
5.4 成员方法
5.4.1 nextIndex(int i, int len)
获取Entry数组索引i处的下一个索引位置
// 回环询问,如果i + 1 < len, 就返回len,否则返回0
// 这个方法其实是为了后面处理哈希冲突的线性探测法做准备的
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
5.4.2 prevIndex(int i, int len)
获取Entry数组索引i处的上一个索引位置
private static int prevIndex(int i, int len) {
return ((i - 1 >= 0) ? i - 1 : len - 1);
}
5.4.3 getEntry(ThreadLocal<?> key)
由键获取对应的键值对,键其实就是ThreadLocal对象。
// 参数key:ThreadLocal对象
private Entry getEntry(ThreadLocal<?> key) {
// 获取该键对应的键值对应该存储在的位置
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
// 如果对应的Entry不为空,且Entry对象对应的key和传入的key相等就返回这个Entry对象,说明找到了
if (e != null && e.get() == key)
return e;
else
// 否则调用getEntryAfterMiss(key, i, e)方法
// 执行到这里有以下几种情况:
// 1. 对应位置的Entry对象不存在
// 2. 存在,但是发生哈希冲突,调用了nextIndex获取了新的位置
return getEntryAfterMiss(key, i, e);
}
5.4.4 getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e)方法
如果调用ThreadLocalMap的getEntry(ThreadLocal<?> key)方法没有找到对应的键值对就会调用这个方法进行后续操作。
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
// 首先得到Entry数组
Entry[] tab = table;
// 然后得到Entry数组的长度
int len = tab.length;
// 如果对应位置的Entry不为空,就一个一个往后找同时清除过期数据,直到找到或下一个Entry对象为空为止
while (e != null) {
ThreadLocal<?> k = e.get();
// 找到了,直接返回这个Entry对象
if (k == key)
return e;
// 没找到往下执行
// Entry对象为空,就清除过期数据
if (k == null)
expungeStaleEntry(i);
else
// Entry不为空,获取下一个Entry对象
i = nextIndex(i, len);
e = tab[i];
}
// 没找到返回null
return null;
}
5.4.5 expungeStaleEntry(int staleSlot)方法
探测式清除过期数据,从下标staleSlot开始,直到碰到的Entry数组中的Entry对象为null时结束,并返回结束时的下标。
private int expungeStaleEntry(int staleSlot) {
// 得到Entry数组
Entry[] tab = table;
// 获取Entry数组的长度
int len = tab.length;
// 帮助GC
// 因为是从getEntryAfterMiss方法进来的,在里面已经判断过key为null才进来的
// 所以这里就不用设置key为null了,因为key一定为null
tab[staleSlot].value = null;
// 设置这个Entry对象为null
tab[staleSlot] = null;
// Entry数组中存储的键值对数量减1
size--;
// 表示当前遍历到的键值对结点
Entry e;
// 表示当前遍历到的索引
int i;
// 从staleSlot+1开始搜索过期数据,直到碰到的Entry对象为null位置
for (i = nextIndex(staleSlot, len);(e = tab[i]) != null;i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
// 如果当前Entry对象的key为null,说明是过期数据,要清理
if (k == null) {
// 当前Entry对象的key为空,说明这个是过期数据,这个Entry对象要回收
e.value = null;
// 设置当前Entry对象对应的索引处为null
tab[i] = null;
// 设置Entry数组中存储的元素数量减1
size--;
}
// 当前Entry对象的key不为null,说明当前Entry不是过期数据,
// 因为当前Entry对象存储的位置可能是发生了哈希冲突后往后移而存储的位置,
// 这个时候就应该尝试去优化位置,让这个位置更靠近本来的位置
// 这样的话,查询的时候效率才会更高
else {
// 获取本来应该存储的位置
int h = k.threadLocalHashCode & (len - 1);
// h != i,说明当前Entry对象是发生哈希冲突后往后移之后存储的位置
// 这个时候就应该去存储到本来的位置
if (h != i) {
// 将移动后的位置设为null,再重新找一个更加接近本来位置的地方
tab[i] = null;
while (tab[h] != null)
h = nextIndex(h, len);
// 将Entry对象存储到最接近本来位置的地方,也有可能就是本来的位置
tab[h] = e;
}
}
}
return i;
}
5.4.6 set(ThreadLocal<?> key, Object value)方法
给ThreadLocalMap独享添加键为key,值为value的键值对
// 参数key:键
// 参数value:值
private void set(ThreadLocal<?> key, Object value) {
// 获取Entry数组
Entry[] tab = table;
// 获取Entry数组的长度
int len = tab.length;
// 获取Entry应该存储的位置
int i = key.threadLocalHashCode & (len-1);
// 从应该存储的位置开始寻找
for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
// 如果 k == key,说明本来已经存储了一个一样的键,则只更新值,更新之后返回
if (k == key) {
e.value = value;
return;
}
// 碰到 k == null,说明碰到过期数据了,执行replaceStaleEntry(key, value, i)方法去替换过期数据
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
// 碰到了一个Entry还没有找到,就将键值对添加到这个位置
tab[i] = new Entry(key, value);
// Entry存储的元素数量加1
int sz = ++size;
// 做一次启发式清理,如果cleanSomeSlots(i, sz)返回false,说明未清理到任何Entry对象,判断是否达到扩容阈值了
// 如果达到扩容阈值,就去扩容
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
5.4.7 replaceStaleEntry(ThreadLocal<?> key, Object value,int staleSlot)方法
调用ThreadLocalMap的set方法往里面添加值时当碰到了过期数据,调用replaceStaleEntry方法替换过期数据
private void replaceStaleEntry(ThreadLocal<?> key, Object value,int staleSlot) {
// 获取Entry数组
Entry[] tab = table;
// 获取Entry数组的长度
int len = tab.length;
Entry e;
// 获取起始位置
int slotToExpunge = staleSlot;
// 一直往前找,如果还可以找到过期数据,就继续往前找,直到找到的Entry对象不为空,并将过期数据的索引位置
// 赋值给slotToExpunge,slotToExpunge其实是为了下面的探测式清除数据准备的
for (int i = prevIndex(staleSlot, len);(e = tab[i]) != null;i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;
// 从staleSlot往后找,直到碰到null为止
for (int i = nextIndex(staleSlot, len);(e = tab[i]) != null;i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
// 条件成立,尝试去替换
if (k == key) {
e.value = value;
// 替换逻辑
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
// 条件成立说明向前查找并未找到过期的Entry
if (slotToExpunge == staleSlot)
slotToExpunge = i;
// 启发式清理
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
// key == null说明当前Entry为过期数据,并且slotToExpunge == staleSlot说明
// 向前查找过期数据也没有找到,这时设置slotToExpunge为i表示从这里开始清除数据
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
// 什么时候执行到这里?
// 向后查找过程中并未发现 k == key的Entry,说明当前set是一个添加逻辑,按照普通的添加逻辑就好
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
// 发现了过期数据,要清理过期数据
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
5.4.8 cleanSomeSlots(int i, int n)方法
启发式清理过期数据
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];
// e != null 并且 e.key == null,说明当前Entry一定是一个过期数据,就从当前位置开始探测式清理数据
if (e != null && e.get() == null) {
n = len;
removed = true;
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0);
return removed;
}
5.4.9 rehash()方法
先清理一下过期数据,如果清理之后的元素数量大于阈值的3/4,就执行真正的扩容方法。
private void rehash() {
// 清除列表里所有过期数据
expungeStaleEntries();
// 如果当前数组里的元素数量还是大于阈值的3/4,就执行真正的扩容方式
if (size >= threshold - threshold / 4)
resize();
}
5.4.10 resize()方法
真正的扩容方法
private void resize() {
// 获取旧的Entry数组
Entry[] oldTab = table;
// 获取旧的Entry数组的长度
int oldLen = oldTab.length;
// 设置扩容后的Entry数组为原来的两倍
int newLen = oldLen * 2;
// 初始化一个Entry数组
Entry[] newTab = new Entry[newLen];
int count = 0;
// 接下来就是一个一个填充了,同时碰到过期数据就清理了,过期数据不用往新的Entry数组填
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;
}
5.4.11 remove(ThreadLocal<?> key)方法
移除ThreadLocalMap中键为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;
}
}
}
文章学习于:
史上最全ThreadLocal 详解
ThreadLocal源码分析_02 内核(ThreadLocalMap)
【JDK源码】线程系列之ThreadLocal
深挖ThreadLocal
ThreadLocal原理及内存泄露预防
ThreadLocal原理详解——终于弄明白了ThreadLocal
ThreadLocal使用与原理
史上最全ThreadLocal 详解