一、ThreadLocal的使用场景:
我们先来看一下源码中的定义:
* This class provides thread-local variables. These variables differ from
* their normal counterparts in that each thread that accesses one (via its
* {@code get} or {@code set} method) has its own, independently initialized
* copy of the variable. {@code ThreadLocal} instances are typically private
* static fields in classes that wish to associate state with a thread (e.g.,
* a user ID or Transaction ID).
也就是说ThreadLocal这个类提供了线程局部的变量,为每个线程都提供了变量的副本,因此,不同线程在同一时间访问到的同名变量是不同的。
那么这个类的意义是什么呢?举一个简单的例子:jdbc对数据库访问需要获取一个数据库的连接“connection”,当同一个线程对数据库进行增删改查操作时,没问题。但是,如果在多线程的情况下就很有可能出现这种情况:线程A利用变量“connection”向数据库中插入了一条数据“record1”,A还没有对这条数据进行提交,而此时线程B执行了同一个变量“connection”的提交操作,显然,这条记录将会被提交到数据库,但是线程A在线程B提交之后,发现数据有误,放弃提交,但是显然“record1”已经被提交了,这时就出现了“脏数据”!而如果使用ThreadLocal为线程分配变量“connection”,那么不同线程的操作的都是变量“connection”的不同副本,因此不同线程对“connection”的操作是互不影响的,上面的问题就迎刃而解了~
二、ThreadLocal的使用与作用:
1、ThreadLocal类中的4个基本方法:
public class ThreadLocal<T> {
public T get();
public void set(T value);
public void remove();
protected T initialValue();
}
get():返回当前线程中的线程局部变量副本的值,如果变量没有没有作用于当前线程,则需要先将其用initialValue()方法进行初始化;
set():将线程局部变量在当前线程中的副本的值设置为指定值;
remove():移除线程局部变量在当前线程中的副本的值,注意:如果当前线程对应的ThreadLocal没有被set(),则执行remove()方法后,会调用initialValue()方法重新初始化其值,并且,在ThreadLocal没有被set()的情况下,remove()方法只会被执行一次,而如果ThreadLocal经过了set(),即人为设置了副本的值,那么remove()方法可以被调用2次;
initialValue():如果当前线程对应的ThreadLocal没有调用set()方法,那么,该方法会在线程第一次使用get()方法时,提前被调用,为当前线程对应的线程局部变量的副本值进行初始化。
2、ThreadLocal类的基本用例:
/**
* Created by rongshuai on 2019/10/28 15:45
*/
public class ThreadLocalDemo extends Thread{
private ResultData data;
public ThreadLocalDemo(ResultData data) {
this.data = data;
}
@Override
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName() + "---" + "i---" + i + "--num:" + data.getNum());
}
}
public static void main(String[] args) throws InterruptedException {
ResultData data = new ResultData();//该类里面存在一个静态变量count
//创建了3个线程
ThreadLocalDemo threadLocaDemo1 = new ThreadLocalDemo(data);
ThreadLocalDemo threadLocaDemo2 = new ThreadLocalDemo(data);
ThreadLocalDemo threadLocaDemo3 = new ThreadLocalDemo(data);
//启动这三个线程
threadLocaDemo1.start();
threadLocaDemo2.start();
threadLocaDemo3.start();
Thread.sleep(300);
System.out.println(ResultData.count);//将被共享的静态变量的结果输出
}
}
class ResultData{
// 生成序列号共享变量
public static Integer count=0;
private static ThreadLocal<Integer> threadLocal=new ThreadLocal<Integer>(){//用来存储整型的ThreadLocal,是一个静态变量
protected Integer initialValue() {
return 0;
}
};
public Integer getNum() {
int count = threadLocal.get() + 1;//将线程局部变量在当前线程中的副本+1
threadLocal.set(count);
return count;
}
}
我们来看一下输出的结果:
可以发现:这三个线程在逻辑上均对“count”这个静态变量进行了修改,但是我们发现:每个线程对应的“count”的值均是1,2,3递增的,也就是说,没有发生因为不同线程间没有同步而导致的对同一个变量的值的共同修改(count没有超过3,且count的值有重复)。此外,我们发现最后输出count的值为0,也就是说在主线程中,count这个静态变量的值也没有被修改。
因此,我们可以得到这样的结论:不同线程的ThreadLocal变量如果共享某一个静态变量,那么他们其实仅仅是获得了这个静态变量的多个不同的副本,他们对该静态变量的修改并不会影响到其他线程中的该变量的副本。
三、ThreadLocal的源码分析:
1、get()源码分析:
public T get() {
//获取当前线程
Thread t = Thread.currentThread();
//从当前线程中获取对应的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
//如果ThreadLocalMap对象不为空
if (map != null) {
//从ThreadLocalMap对象中获得以当前线程对应的ThreadLocal对象作为key的实体
ThreadLocalMap.Entry e = map.getEntry(this);
//如果该实体存在,则将该实体对应的值(也就是该线程对应的线程局部变量的副本的值)返回
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//如果上面没有获得变量副本的值,那么就进行变量值的初始化
return setInitialValue();
}
我们来梳理一下get()方法的流程:
我们首先分析getMap(t)方法:
ThreadLocalMap getMap(Thread t) {
//返回当前线程中的threadLocals
return t.threadLocals;
}
我们继续进入到threadLocals看一下他是什么:
好,我们继续看一下ThreadLocalMap是什么:
其实这个ThreadLocalMap就是一个map,其中的key是当前线程的ThreadLocal对象,value是Entry。注意这里Entry继承了WeakReference,也就是弱引用,而若一个对象具有弱引用,那么在GC线程扫描内存的过程中,不管当前内存空间是否足够,都会将这块内存回收。
我们再来看一下getEntry()方法:
private Entry getEntry(ThreadLocal<?> key) {
//从作为key的ThreadLocal对象中,获取其对应的哈希码,并计算出当前实体对应的数组下标
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
//如果找到了实体,并且该实体对应的key值和输入的key值一致
if (e != null && e.get() == key)
return e;
else//否则的话,说明hash冲突了,则调用getEntryAfterMiss(key,i,e)重新获得该实体
return getEntryAfterMiss(key, i, e);
}
那我们继续进入到getEntryAfterMiss(key,i,e):
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
//获得实体数组
Entry[] tab = table;
//获得实体数组的长度
int len = tab.length;
while (e != null) {//如果没有找到该实体则退出
ThreadLocal<?> k = e.get();//获得实体对应的key值(最初传入的实体为刚刚找到的hash冲突的实体)
if (k == key)//如果找到了,则将该实体返回
return e;
if (k == null)//如果当前实体不存在key值,则调用expungeStaleEntry()方法
expungeStaleEntry(i);
else//如果当前实体的key存在,则使用线性冲突法获取下一个实体坐标
i = nextIndex(i, len);
e = tab[i];//获取下一个实体
}
return null;
}
我们看一下expungeStaleEntry(i)方法:
(顾名思义,该方法的作用是“抹去不新鲜”的实体,可以理解为清理掉实体存在但是key值为空的“脏实体”,也就是ThreadLocal被GC掉的实体)
private int expungeStaleEntry(int staleSlot) {
//获取实体数组
Entry[] tab = table;
//获取数组
int len = tab.length;
// expunge entry at staleSlot
tab[staleSlot].value = null;//将当前位置(没有找到key的位置)对应的实体值置空
tab[staleSlot] = null;//将当前位置置空,即清除操作
size--;//将实体数组的长度减一
// Rehash until we encounter null
//从原有的hash数组中删除掉一个元素,则需要重新hash实体数组(因为删掉一个位置,该位置如果被探测到,则会告知为空,但是,有可能该位置是因为冲突而被替代过的)
Entry e;
int i;
for (i = nextIndex(staleSlot, len);(e = tab[i]) != null;i = nextIndex(i, len)) {
//从当前位置向后循环遍历获取数组的下标(目的是找到下一个哈希值在当前位置,但是因为冲突而被替代过的元素,退出循环的条件为查找到的数组元素为空)
ThreadLocal<?> k = e.get();//获取当前实体的key值
if (k == null) {//如果key值为空,说明下一个位置也为空,则当前位置不存在因为哈希冲突而被替换的情况
e.value = null;//将当前实体的值置空
tab[i] = null;//将当前实体对应的数组置空
size--;//将数组的长度减一
} else {//如果key值非空,说明下一个位置非空,即:当前位置的元素之前曾因为冲突,被移动到了下一个位置
int h = k.threadLocalHashCode & (len - 1);//获取当前key柱子对应的哈希码,并计算出对应的数组下标
if (h != i) {//如果得到了和当前数组下标不同的数组下标
tab[i] = null;//将当前下标对应的数组元素置空
// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
while (tab[h] != null)//以h为新的下标,继续进行hash冲突处理,处理完所有遇到的位置被占用的实体,不断将下标向后推移
h = nextIndex(h, len);
tab[h] = e;//相当于为当前节点i的下一个节点的实体重新找一个位置,而这个位置在最后一个没有被占用的数组位置(相当于把i节点的下一个被占用的节点重新分配了位置)
}
}
}
return i;
}
分析源码可知,expungeStaleEntry(i)这个方法实现了对“脏实体”的清除,并对删除该实体后的重新进行hash调整。
get()源码总结:threadLocals是线程Thread的一个类型为ThreadLocal.ThreadLocalMap的成员变量,其本质是一个key为ThreadLocal对象,value为Entry的一个哈希数组。get()方法用来根据当前线程对应的哈希数组,然后根据以当前线程的ThreadLocal对象作为key,去这个哈希数组中去找对应的实体,如果找到,则将实体对应的值返回;如果找到的实体对应的key值和当前key值不同,说明出现了hash冲突,需要继续向下找;如果找到的实体对应的key值为空,说明该实体为“脏实体”,这时就需要将该实体在hash数组中清除掉,并且还要注意更新hash数组。所以,get()方法,主要做的就是从一个hash数组中查值、处理hash冲突以及删除hash数组中的某些失效元素的作用。
2、set()源码分析:
public void set(T value) {
//获取当前线程
Thread t = Thread.currentThread();
//获得当前线程的ThreadLocalMap
ThreadLocalMap map = getMap(t);
//如果map存在,则把当前的ThreadLocal对象作为key,把传进来的值作为value,设置map
if (map != null)
map.set(this, value);
else//如果map不存在,则创建一个map
createMap(t, value);
}
getMap()方法,我们在get()方法中已经分析过了,所以我们主要分析map.set(this,value)方法和createMap(t,value)方法。
首先分析map.set(this,value)方法:
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;
//根据ThreadLocal对象的哈希码计算出其对应实体对应的数组下标
int i = key.threadLocalHashCode & (len-1);
//从当前下标开始遍历哈希数组,直到遍历到的位置为空才退出
for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {
//获取当前实体对应的key值
ThreadLocal<?> k = e.get();
//如果当前实体对应的key值和传进来的key值相等,则更新当前实体的value值为传进来的value
if (k == key) {
e.value = value;
return;
}
//如果当前位置的key不存在,说明该位置之前的实体被GC了,所以需要用新的值替代原来的实体
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
//如果哈希数组已经遍历完,并且所有位置的key值都不等于传进来的key值(说明,当前key对应的所有位置都已经被其他实体占了)
tab[i] = new Entry(key, value);//将当前位置实体设置为传入的key-value对
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)//重新对哈希数组中的脏实体进行清除,并重新调整哈希数组
rehash();
}
可见这个set()方法,本质上就是哈希数组元素的插入操作,我们继续进入到replaceStaleEntry(key,value,i)这个方法:
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).
int slotToExpunge = staleSlot;//将需要被清除的数组下标设置为传进来的数组下标
for (int i = prevIndex(staleSlot, len);(e = tab[i]) != null;i = prevIndex(i, len))
//从当前位置开始,向前遍历哈希数组,找到第一个脏实体
if (e.get() == null)//如果遍历到key值为空的实体
slotToExpunge = i;//则将需要清除的实体位置设置为当前数组下标
// Find either the key or trailing null slot of run, whichever
// occurs first
//从当前位置开始向后遍历
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.
if (k == key) {//如果在向后环形查找的过程中发现key相同的entry就覆盖并且和脏entry进行交换
e.value = value;//将该实体的value更新为传进来的value
tab[i] = tab[staleSlot];//获取需要被清除的位置的实体
tab[staleSlot] = e;//将该位置的实体替换为刚才更新后的实体
// Start expunge at preceding stale entry if it exists
//如果在查找过程中还没有发现脏实体,那么就以当前位置作为cleanSomeSlots的起点
if (slotToExpunge == staleSlot)//清除操作
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.
//如果向前查找没有找到脏实体,但是在向后查找过程中遇到脏实体的话,后面就以此时这个位置作为起点,执行cleanSomeSlots
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
// If key not found, put new entry in stale slot
//如果查找过程中没有找到脏实体以及可以覆盖的实体,那么就将新的实体插入到脏实体
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
// If there are any other stale entries in run, expunge them
//如果存在其他的脏实体,则对该脏实体进行清理
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
由于expungeStaleEntry()方法我们在分析set()方法时介绍过了,即:清除脏实体,并重新调整哈希数组。所以,我们继续进入到cleanSomeSlots这个方法:
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;//将n修改为哈希数组的长度
removed = true;//将清除标志设为true
i = expungeStaleEntry(i);//将这个脏实体清除,并将i继续向下推
}
} while ( (n >>>= 1) != 0);//这里的n用来控制循环次数:log2(n)
return removed;
}
可见,该方法用来从指定节点进行哈希遍历,当遍历过程中遇到“脏实体”,将将其清除掉。
回到,set()方法,我们会发现,这个replaceStaleEntry方法主要做了两件事:1.将脏实体替换为我们要插入的新实体;2.请其他的脏实体清除掉;
我们在看set()方法中最后的部分:
这是在遇到哈希冲突的情况下发生的(没办法覆盖并且也没遇到脏实体),此时的i处是一个没有被占用的位置
tab[i] = new Entry(key, value);//创建一个新的entry对象,将该对象放到i位置处
int sz = ++size;//长度加一
//如果没有清除脏实体,并且数组长度达到了阈值,则需要扩容
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
我们继续跟进到rehash()方法:
private void rehash() {
expungeStaleEntries();
// Use lower threshold for doubling to avoid hysteresis
if (size >= threshold - threshold / 4)
resize();
}
继续跟进到expungeStaleEntries():
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;
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) {//如果遇到“脏实体”,则将其值置空,以帮助GC
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;//将哈希数组更新为新的哈希数组
}
可见,resize()方法实现了对哈希数组的扩容,新哈希数组的容量为原哈希数组的2倍;
至此,map.set(this,value)方法就全部分析完了,总的来说,该方法实现了在map存在的情况下,新实体的插入操作,脏实体的清除操作以及哈希数组的扩容。
当map不存在时,就会执行createMap(t,value)方法,我们跟进去看一下:
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];//新建一个哈希数组,默认容量为16
//获取第一个下标
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);//将实体添加到map中
size = 1;//将哈希数组长度设置为1
setThreshold(INITIAL_CAPACITY);//设置阈值
}
可见,就是一个创建ThreadLocalMap对象,并将当前的key-value对添加到新创建的ThreadLocalMap对象中的操作。
至此,ThreadLocal的set()部分源码分析完毕~
3、remove()源码分析:
public void remove() {
//通过getMap()获得当前线程对应的ThreadLocalMap对象
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)//如果该对象存在,则执行remove(this)操作
m.remove(this);
}
getMap()方法我们在分析get()源码的时候就已经分析过了,所以我们直接跟进到m.remove(this)方法:
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;//获取哈希数组
int len = tab.length;//获取哈希数组的长度
int i = key.threadLocalHashCode & (len-1);//获取当前key对应的数组下标
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {//从下边i开始向后环形查找
if (e.get() == key) {//如果找到key值相等的实体
e.clear();//执行clear()方法,该实体会被GC掉
expungeStaleEntry(i);//将该位置的实体清空
return;
}
}
}
可见,这段逻辑很简单,就是一个哈希数组中实体对象执行clear(),并将哈希数组中对应位置清空的操作;
四、总结:
- 每个线程都会维护一个ThreadLocalMap,其中的key是ThreadLocal对象,value是用户传进去的值。这样,不同线程维护不同的ThreadLocalMap,而ThreadLocalMap中包含了ThreadLocal和value。因此,不同线程之间使用的是同名变量的多个副本,因此他们之间对同名变量的操作是互不影响的;
- 线程中对ThreadLocal的get(),set(),remove()操作实际上就是对ThreadLocalMap这个哈希数组的查询、插入、移除操作;
- 在ThreadLocalMap中,ThreadLocal对象是弱引用的,这就意味着,每次GC,ThreadLocal都会被置空,而在get(),set(),remove()操作中,几乎时时刻刻都强调要将entry非空但是key值为空的位置清除掉,也就是将“脏实体”清除掉,这样设计就是为了防止程序员使用了ThreadLocal但是没有调用remove方法,从而导致内存泄漏。