ThreadLocal
ThreadLocal的作用:
1、是让每个线程都拥有自己的专属本地变量,即实现了线程隔离
2、可以通过ThreadLocal在同一线程的不同组件中传递公共变量
1、成员属性
/* 用来寻址的hashcode
* 使用 threadLocalHashCode & (table.length - 1) 计算结果得到的位置就是当前 entry 需要存放的位置。
*/
private final int threadLocalHashCode = nextHashCode();
/**
* 创建ThreadLocal对象时会使用到该属性:
* 每创建一个threadLocal对象时,就会使用 nextHashCode 分配一个hash值给这个对象。
*/
private static AtomicInteger nextHashCode =
new AtomicInteger();
/**
*每创建一个ThreadLocal对象,ThreadLocal.nextHashCode的值就会增长HASH_INCREMENT(0x61c88647)
* hash增量为这个数字,带来的好处就是hash分布非常均匀
*/
private static final int HASH_INCREMENT = 0x61c88647;
/**
* 下一个hashcode
*/
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
/**
* 初始化一个起始value:
* 默认返回null,一般情况下都是需要重写这个方法的(例如第2小节的入门案例中就重写了该方法)。
* 第一次调用get时会调用这个方法,如果之前没调用过set的话
* @return the initial value for this thread-local
*/
protected T initialValue() {
return null;
}
2、get方法
public T get() {
//获取当前线程
Thread t = Thread.currentThread();
//根据线程获取map
ThreadLocalMap map = getMap(t);
//map不为空表示线程的map已经被初始化了
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//执行到这里表示map为空要进行初始化或map还没有对应的Entry
return setInitialValue();
}
private T setInitialValue() {
T value = initialValue();//初始值
Thread t = Thread.currentThread();//当前线程
ThreadLocalMap map = getMap(t); //获取map
//map不为空就设值key为ThreadLocal,value为和ThreadLocal相关的Value
if (map != null)
map.set(this, value);
//map为空就初始化
else
createMap(t, value);
return value;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
整体流程:
1、获取当前线程
2、根据线程获取map
3、根据ThreadLocal获取map里对应的entry
4、entry不为空就返回entry里的值
5、map为空或entry为空就调用setInitialValue()方法初始化map或new一个entry
3、set方法
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
整体流程:
1、获取当前线程
2、根据线程获取map
3、map不为空调用map的set()方法把Threadlocal和value设置进map里
4、map为空就初始化map
ThreadLocalMap
ThreadLocal只是起到了一个中介的作用,真正存储数据的是ThreadLocalMap,ThreadLocal是ThreadLocalMap里的Entry的key,ThreadLocal其实就是一个外壳(相当于一个工具或桥梁把Thread和ThreadLocalMap连接起来)
1、内部类,存放数据的Entry,key是ThreadLocal(弱引用),value是和ThreadLocal关联的值
static class Entry extends WeakReference<ThreadLocal<?>> {
//ThreadLocal关联的value,是强引用
Object value;
//key是弱引用
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
2、成员属性
/**
* table的初始容量,2的n次方
*/
private static final int INITIAL_CAPACITY = 16;
/**
* 存放数据的table
*/
private Entry[] table;
/**
* entry数量
*/
private int size = 0;
/**
* 扩容阈值
*/
private int threshold; // Default to 0
/**
* 设置阈值,容量的2/3
*/`在这里插入代码片`
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
// 下一索引
private static int nextIndex(int i, int len) {
//i是最后一位,下一位就是0
return ((i + 1 < len) ? i + 1 : 0);
}
/**
* 前一索引, i是第一位,上一位就是最后一位
*/
private static int prevIndex(int i, int len) {
return ((i - 1 >= 0) ? i - 1 : len - 1);
}
3、构造函数
ThreadLocalMap是延时初始化,只会才第一次调用set方法或get方法时才会初始化
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY]; //创建table
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); //获取桶位
table[i] = new Entry(firstKey, firstValue); //把entry放入对应桶位
size = 1;
setThreshold(INITIAL_CAPACITY); //设置阈值 初始化后的阈值是16*2/3 = 10
}
4、getEntry()方法
这个方法是在ThreadLocal里的get()方法被调用的
private Entry getEntry(ThreadLocal<?> key) {
//获取桶位
int i = key.threadLocalHashCode & (table.length - 1);
//桶位对应元素
Entry e = table[i];
//桶位有元素并且元素的key就是要查找的key
if (e != null && e.get() == key)
return e;
else
//桶位没元素或桶位有元素但是元素key和要查找的key不相等
return getEntryAfterMiss(key, i, e);
}
整体流程:
1、根据ThreadLocal的hashcode获取桶位
2、判断桶位是否有元素
3、如果有元素并且元素的key和要查找的key相等就返回桶位元素
4、如果桶位没元素或桶位有元素但是桶位和要查找的key不相等(这种情况一般是其他元素发生了hash冲突然后把元素放入了该位置),就调用getEntryAfterMiss()方法遍历后面桶位查找元素
5、getEntryAfterMiss()方法
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
// while循环条件:e != null 说明向后查找的范围是有限的,碰到 slot == null 的情况,则搜索结束!
while (e != null) {
ThreadLocal<?> k = e.get();
// k == key条件成立:说明向后查询过程中找到合适的entry了,直接返回entry就ok了。
if (k == key)
return e;
// 桶位元素的key被回收了
if (k == null)
// 做一次探测式过期数据回收。
expungeStaleEntry(i);
else
// 更新index,继续向后搜索
i = nextIndex(i, len);
// 获取下一个桶位中的entry。
e = tab[i];
}
//来到这里表明找不到元素
return null;
}
整体流程
1、从i开始遍历后面的桶位,包括i
2、key相等就返回桶位元素
3、key为null就从当前桶位开始进行一次探测式清理,把过期的数据清理掉
4、遍历完后面的桶位还没找到元素就返回null
6、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;
int i = key.threadLocalHashCode & (len-1); //获取桶位
//从i开始往后找,直到找到一个可以使用的桶位
// 1.k == key说明是替换
// 2.碰到一个过期的slot ,这个时候可以强行占用
// 3.查找过程中碰到 slot == null
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
//更新操作
if (k == key) {
e.value = value;
return;
}
//ThreadLocal被回收了,替换过期的ThreadLocal
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
//来到这里说明遇到一个桶位为空的
tab[i] = new Entry(key, value);
int sz = ++size;
// 做一次启发式清理:
// 条件一:!cleanSomeSlots(i, sz) 成立,说明启发式清理工作未清理到任何数据..
// 条件二:sz >= threshold 成立,说明当前table内的entry已经达到扩容阈值了..会触发rehash操作。
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
整体流程
1、根据ThreadLocal的hashcode获取桶位
2、从获取到的桶位开始往后遍历直到找到一个可以使用的桶位
3、key相等就执行更新操作,替换旧值
4、key为null就调用replaceStaleEntry()方法把过期数据和有效数据交换位置
5、3和4都没执行就执行新增操作
6、执行新增操作之后就做一次启发式清理,清理过期元素然后判断是否要扩容
7、expungeStaleEntry()方法
这个方法的作用:
从staleSlot位置开始继续向后查找过期数据,把过期数据都清理掉,直到碰到slot == null(key为null,value也是null)的情况结束,并返回结束时的数组下标位置~
这个方法在getEntryAfterMiss()方法被调用,当遇到key为null的entry时会调用这个方法进行一次过期数据的清理
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
tab[staleSlot].value = null;
// 因为staleSlot位置的entry是过期的,所以这里直接置为Null
tab[staleSlot] = null;
size--; //上面删除一个元素所以数量-1
// Rehash until we encounter null
Entry e; //当前遍历节点
int i; //当前遍历节点的index
// for循环从staleSlot + 1的位置开始搜索过期数据,直到碰到 slot == null 结束:
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
//桶位元素的key
ThreadLocal<?> k = e.get();
//key为null表示ThreadLocal被回收了,整个entry清空
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
// 执行到这里,说明当前遍历的slot中对应的entry 是非过期数据:
// 重新计算当前entry对应的 index
int h = k.threadLocalHashCode & (len - 1);
// 条件成立:说明当前entry存储时 就是发生过hash冲突,然后向后偏移过了,本来应该存在h,因为发生了hash冲突所以后移存在i
// 但是h到i这段数据有可能被清空了,所以重新找一个更靠近h的位置
if (h != i) {
//把当前位置清空
tab[i] = null;
// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
//从h开始往后找一个空的
while (tab[h] != null)
h = nextIndex(h, len);
// 将当前元素放入到 距离正确位置 更近的位置(有可能就是正确位置)
tab[h] = e;
}
}
}
return i;
}
整体流程:
1、先清除staleSlot位置的entry
2、然后从staleSlot+1的位置开始遍历
3、遇到key为null的entry就把该entry清理
4、遇到key不为null的entry,就把该entry往前移,使它更靠近正确的位置,正确的位置即发生hash冲突的位置,正确位置到当前位置这段的数据有可能被清空了,所以要往前移,使它更靠近正确的位置
5、最后返回entry为null的下标
用图来看一下流程:
当调用getEntryAfterMiss()方法遇到key为null的entry时,就调用expungeStaleEntry()方法,此时staleslot为2,因为2的entry的key是null然后把该位置的entry清空,然后往后遍历发现3的entry不为空,然后重新计算这个entry的桶位,计算得到为1,然后把3的entry清空,从1开始往后找一个空的entry,因为刚才已经把2的entry清空了,所以2的entry是可用的,把原来3的entry放到2,此时的table变成
遍历4的key为null,把该位置的entry清空,遍历到5,5为null就结束遍历,此时table变成
8、replaceStaleEntry()方法
这个方法的作用是调用set()方法遇到一个过期的数据,然后把非过期数据和这个过期的数据交换位置,也是非过期数据前移,key和value是调用set()方法的key和value
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).
// slotToExpunge表示开始探测式清理过期数据的开始下标:默认从当前staleSlot开始
int slotToExpunge = staleSlot;
//从staleSlot开始往前找过期的ThreadLocal,直到桶位为null
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
// 条件成立:说明向前找到了过期数据,更新 探测清理过期数据的开始下标为 i
if (e.get() == null)
slotToExpunge = i;
// Find either the key or trailing null slot of run, whichever
// occurs first
//从staleSlot开始往后找过期的ThreadLocal,直到桶位为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.
// key是要设置的key,k是遍历到当前桶位的key
if (k == key) {
e.value = value;
// 将table[staleSlot]这个过期数据 放到 当前循环到的 table[i] 这个位置
tab[i] = tab[staleSlot];
//把当前entry放到table[staleSlot]
//table[i]和tab[staleSlot]的数据交换了 原来过期数据的桶位变成非过期的 原来非过期的桶位变成过期的 占用步骤
//table[staleSlot]在前,当前entry再后,即过期的数据后移了
tab[staleSlot] = e;
// Start expunge at preceding stale entry if it exists
// 如果条件成立:
// 1.说明replaceStaleEntry 一开始时 的向前查找过期数据 并未找到过期的entry
if (slotToExpunge == staleSlot)
// 开始探测式清理过期数据的下标 修改为 当前循环的index。
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.
// 条件1:k == null 成立,说明当前遍历的entry是一个过期数据..
// 条件2:slotToExpunge == staleSlot 成立,一开始时的向前查找过期数据并未找到过期的entry.
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
// If key not found, put new entry in stale slot
//来到这里表明是新增操作,原来table没有一样的key
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
// If there are any other stale entries in run, expunge them
// 条件成立:除了当前staleSlot 以外 ,还发现其它的过期slot了.. 所以要开启清理数据的逻辑..
if (slotToExpunge != staleSlot)
//expungeStaleEntry(slotToExpunge)先执行第一次清理,返回null
//到执行cleanSomeSlots这个方法时就是第二次清理了
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
整体流程:
1、从staleSlot开始往前遍历(不包括staleSlot),如果找到一个key为null的元素,就更新slotToExpunge
2、从staleSlot开始往后遍历(不包括staleSlot),如果找到一个key和要设置的key相等,就把遍历位置的元素和过期的元素交换位置,遍历位置元素交换后前移,过期的元素交换后后移
3、如果往后遍历有过期元素,并且往前遍历没找到过期元素,就把当前位置更新为slotToExpunge,只会更新一次
4、往后遍历没找到一个key和要设置的key相等就要新增一个entry,新增在staleSlot处
5、如果除了当前staleSlot 以外 ,还发现其它的过期slot了就要清理其他过期的slot
用图来看一下流程:
调用set()方法时遍历到2的entry的key为null就把该位置占用,slotToExpunge的初始值是staleSlot即2,向前遍历,遍历到0的entry的key为null,更新slotToExpunge为0
1、往后遍历到3的entry和要设置的key相等,然后把2的entry和3的entry交换位置,此时table变成,此时会再清除一次过期的元素(启发式清理)然后返回
2、如果遍历到5都没有找到要查找的key,就把staleSlot的value清空然后,在该位置新增一个entry,此时table变成
然后从slotToExpunge即0开始做一次启发式清理,table变成
9、cleanSomeSlots()方法
这个方法在set()方法最后被调用,在replaceStaleEntry()方法往后遍历时找到一个key和要设置的key相等调用,位置i一定是没元素的,因为调用expungeStaleEntry()返回的是null
这个方法的作用是把所有过期的元素清理掉,而expungeStaleEntry()方法是清除一部分
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
do {
// 获取当前i的下一个下标
i = nextIndex(i, len);
// 获取table中当前下标为i的元素
Entry e = tab[i];
// 条件一:e != null 成立
// 条件二:e.get() == null 成立,说明当前slot中保存的entry 是一个过期的数据..
if (e != null && e.get() == null) {
// 重新更新n为 table数组长度 len是不变的,n一直在变
n = len;
removed = true;
// 以当前过期的slot为开始节点 做一次 探测式清理工作,把当前过期的slot到后面桶位没元素的这段的过期slot都清理掉
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0); //n除于2
return removed;
}
整体流程:
1、从i开始向后遍历,不包括i,找到一个过期元素就执行一次expungeStaleEntry()进行探测性清理,并更新n为table的长度,这样的作用是扩大搜索范围
2、如果一直没遇到过期元素并且n为0就停止扫描
3、最后返回是否清理过过期元素
10、扩容
private void rehash() {
expungeStaleEntries();
// Use lower threshold for doubling to avoid hysteresis
if (size >= threshold - threshold / 4)
resize();
}
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;
}
在扩容前会把过期元素都清理掉,然后再判断是否需要扩容
ThreadLocal为什么要被设计成弱引用
ThreadLocal被设为弱引用的原因,当我们new一个ThreadLocal对象时,有一个强引用指向这个ThreadLocal对象,然后如果把这个强引用设为null之后,如果Entry的ThreadLocal也是强引用的话这个ThreadLocal对象就不会被gc,是要直到线程被gc掉这个ThreadLocal对象才会被gc掉,外部没有强引用访问到ThreadLocal对象这个ThreadLocal对象就没有存在的意义因为只能通过外部的强引用去访问ThreadLocal对象对应的ThreadLocalMap,所以这时ThreadLocal对象不会被gc的话会导致内存泄漏,如果Entry的ThreadLocal为弱引用的话只要外部的强引用被置为null或失效后,那么gc的时候会把ThreadLocal对象回收掉,这样就不会有内存泄漏的问题
ThreadLocal被设为弱引用的原因简单一句话总结就是为了防止内存泄漏
举个例子来说明一下
public class ThreadLocalTest {
public static class MyRunnable implements Runnable {
private final ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>();
@Override
public void run() {
threadLocal.set( (int) (Math.random() * 100D) );
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
}
System.out.println(Thread.currentThread().getName() + "的专属变量" + threadLocal.get());
}
}
public static void main(String[] args) throws InterruptedException {
MyRunnable task = new MyRunnable();
Thread t1 = new Thread(task,"线程1");
t1.start();
TimeUnit.MILLISECONDS.sleep(100);
Thread t2 = new Thread(task,"线程2");
t2.start();
TimeUnit.MILLISECONDS.sleep(100);
}
}
输出:
线程1的专属变量37
线程2的专属变量69
这段代码的threadLocal是ThreadLocal的强引用对象,如果threadLocal设为null之后,这时ThreadLocal对象只剩
Entry里的ThreadLocal弱引用,因为是弱引用,所以gc时ThreadLocal对象会被马上回收
如果Entry里的ThreadLocal是强引用的话那么ThreadLocal对象只会在内存不足或线程被回收的时候才会被回收,这样会导致内存泄漏,因为我们没有了主线程栈的ThreadLocal的强引用就不能通过ThreadLocal对象去访问ThreadLocalMap的Entry,访问不了ThreadLocalMap的Entry,那么ThreadLocal对象就没有存在的意义了所以要回收防止内存泄漏
因为ThreadLocalMap是Thread的成员变量,所以只能通过Thread访问对应的ThreadLocalMap
总结
1、一个Thread对应一个ThreadLocalMap,一个ThreadLocalMap对应多个ThreadLocal,所以是通过每个线程都拥有自己的ThreadLocalMap来实现线程隔离的,各个线程在调用同一个ThreadLocal对象的set(value)设置值的时候,是往各自的ThreadLocalMap对象数组中设置值
2、ThreadLocalMap采用的是线性冲突法,即发生hash冲突时元素就往后找
3、ThreadLocal设计成弱引用的原因是为了防止ThreadLocal对象不能被使用时无法被gc而导致内存泄漏
4、ThreadLocal调用set(),get()和remove()方法时都会把table里过期的元素清理掉
5、table的长度为2的n次方的原因和hashmap一样是为了高效取余
6、ThreadLocalMap的Entry的key为ThreadLocal,因为是弱引用,不会导致内存泄漏,但是value是强引用,只要线程不被回收掉就不会被回收,如果使用的是线程池就有可能导致value的内存泄漏,所以最好使用remove()方法把整个entry去掉
7、ThreadLocal并不是为线程保存对象的副本,它仅仅只起到一个索引的作用。它的主要目的是为每一个线程隔离一个类的实例,这个实例的作用范围仅限于线程内
8、ThreadLocal的思想是空间换时间,使每个Thread都拥有自己的ThreadLocalMap,这样虽然使用了更多的内存空间,但是不用考虑因为共享变量而带来的同步问题,因为变量没有共享了,是每个Thread私有的,只会访问到自己的ThreadLocal