ThreadLocal的简介
多线程访问同一个共享变量时特别容易出现并发问题,特别是在多个线程需要对一个共享变量进行写入时,为了保证线程安全,一般使用者在访问共享变量时需要进行适当的同步,同步的措施一般就是加锁,加锁显然就是会阻塞其他线程,拉高了等待时间。
ThreadLocal时JDK包提供的,它提供了线程本地变量,也就是如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个本地副本。当多个线程操作这个变量时,实际操作的是自己本地内存里面的变量,从而避免了线程安全的问题。
ThreadLocal使用示例
实例开启了两个线程,在每个线程内部都设置了本地变量的值,然后调用print函数打印调用当前本地变量的值。如果打印后调用了本地变量的remove方法,则会删除本地的内存中的该变量、
public class ThreadLocalTest {
//(1)printf函数
static void printf(String str){
//1.1打印当前线程本地内存中localVariable变量的值
System.out.println(str+":"+localVariable.get());
//1.2清除当前线程本地内存中的localVariable变量的值
localVariable.remove();
}
//(2)ThreadLocal变量
static ThreadLocal<String> localVariable = new ThreadLocal<>();
public static void main(String[] args) {
//(3)创建线程one
Thread threadone = new Thread(new Runnable() {
@Override
public void run() {
localVariable.set("thread one local variable");
//调用打印函数
printf("Thread one");
//打印本地变量值
System.out.println("ThreadOne remove after"+":"+localVariable.get());
}
});
//创建线程two
Thread threadtwo = new Thread(new Runnable() {
@Override
public void run() {
localVariable.set("thread two local variable");
//调用打印函数
printf("Thread two");
//打印本地变量值
System.out.println("ThreadTwo remove after"+":"+localVariable.get());
}
});
//启动线程
threadone.start();
threadtwo.start();
}
}
线程one中的代码通过set方法设置了localVariable的值,这其实设置的是线程one本地内存的副本,这个副本Two是不能访问不了的,线程two的执行类似于线程One。
ThreadLocal的实现原理
ThreadLocal的几个核心方法
Set方法
分析源码:
1.Thread t = thread.currentThread();//获取当前线程的实例对象
2.ThreadLocalMap map = getMap(t);//通过当前线程实例获取到ThreadLocalMap对象
3.if (map != null)// 如果Map不为null,则以当前threadLocl实例为key,值为value进行存入
map.set(this, value);
4. createMap(t, value);//4.map为null,则新建ThreadLocalMap并存入value
方法逻辑很清晰,通过源码我们知道value是存放在ThreadLocalMap里了。并且以threadLocal实例为key,接下来介绍ThreadLocalMap
该方法直接返回的就是当前线程对象t的一个成员变量threadLocals
也就是说ThreadLocalMap的引用作为Thread的一个成员变量,被Thread进行维护的,回头看set()方法,当map为Null的时候会通过createMap(t,value)方法、
该方法就是new一个ThreadLocalMap实例对象,然后同样以当前threadLocal实例作为key,值为value存放到threadLocalMap中,然后将当前线程对象的threadLocals赋值为threadLocalMap
现在对set()方法进行总结一下:通过当前线程对象thread获取该thread所维护的threadLocalMap,若threadLocalMap不为null,则以threadLocal实例为key,值为value的键值对存入threadLocalMap,若threadLocalMap为null的话,就新建threadLocalMap然后在以threadLocal为键,值为value的键值对存入即可
get方法
弄懂了set方法的逻辑,看get方法只需带着逆向思维去看就好了,如果是那样存的,反过来去拿就好,代码逻辑很清楚。另外需要看setInitialValue主要做了些什么?
这段代码和set的逻辑差不多,需要关注的是initialValue()
remove()方法
get,set()方法实现了存数据和读数据,我们当然还得学会如何删除数据。删除数据当然是从map中删除数据,
先获取当前线程相关联得threadLocalMap然后从map中删除该threadLocal实例为key的键值对即可
ThreadLocalMap详解
从上面的分析我们已经知道,数据其实都是放在了threadLocalMaop中,threadLocal的get,set和remove()方法实际上具体是通过threadLocalMap的getEntry,set和remove方法实现的。如果想全方位的弄懂threadLocal,势必得在对threadLocalMap做一番理解。
3.1 Entry数据结构
ThreadLocalMap是threadLocal一个静态内部类,和大多数容器一样维护了一个数组,同样的threadLocalMap维护了一个entry类型的table数组。
通过注释可以看出,table数组的长度为2的幂次方,接下来可以看看Entry是什么:
static class Entry extends WeakRefence<ThreadLocal<?>>{
Object value;
Entry(ThreadLocal<?> k , object v){
super(k);
value = v;
}
}
Entry是一个以Threadlocal为key,object为value的键值对,另外需要注意的是这里的threadLocal是**弱引用,因为Entry继承了WeakReference,在Entry的构造方法中,调用了super(k)方法就会将threadLocal实例包装成一个WeakReferece,**<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/22732206/1650093557887-66a78334-6f6a-457a-9d24-8c8957dd1d4e.png#clientId=u378d4e90-f4f2-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=343&id=u15effee3&margin=%5Bobject%20Object%5D&name=image.png&originHeight=425&originWidth=755&originalType=binary&ratio=1&rotation=0&showTitle=false&size=201061&status=done&style=none&taskId=uc062da17-af5e-4e10-aa94-31682a0c41e&title=&width=609.0756107274028)
内存泄露的问题
因为ThreadlocalMap和大多数容器一样维护了一个entry类型的table数组,Entry是一个以ThreadLocal为key,Object为value的键值对,另外需要注意的是threadlocal的弱引用,因为Entry继承了WeakReference,在Entry的构造方法中,调用了super(k)方法就会将threadlocal实例包装成一个WeakReference,Entry中的key是弱引用,当threadlocal外部强引用被置为null,那么系统GC的时候,根据可达性原则,这个Threadlical实例就没有任何一条链路能够引用到它,这个ThreadLocal势必就会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,而value不为空,就会出现内存泄露的问题。
解决方法:用完threadlocal就立即调用remove方法,在其内部的get,set,remove,它方法体也隐式清除了
3.2 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;
//根据threadLocal的hashCode确定Entry应该存放的位置
int i = key.threadLocalHashCode & (len-1);
//采用开放地址法,hash冲突的时候使用线性探测
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
//覆盖旧Entry
if (k == key) {
e.value = value;
return;
}
//当key为null时,说明threadLocal强引用已经被释放掉,那么就无法
//再通过这个key获取threadLocalMap中对应的entry,这里就存在内存泄漏的可能性
if (k == null) {
//用当前插入的值替换掉这个key为null的“脏”entry
replaceStaleEntry(key, value, i);
return;
}
}
//新建entry并插入table中i处
tab[i] = new Entry(key, value);
int sz = ++size;
//插入后再次清除一些key为null的“脏”entry,如果大于阈值就需要扩容
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
1.根据threadLocal的hascode确定Entry应该存放的位置
2.如果发现Entry已经存放了值了就利用开放地址法向后找为空的Entry,如果发现Entry的key或者为null为同一个key就覆盖掉。
3.插入后再次清除一些key为null的脏”entry”,如果大于阈值就进行扩容
threshold的确定
private int threshold; // Default to 0
/**
* The initial capacity -- MUST be a power of two.
*/
private static final int INITIAL_CAPACITY = 16;
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
/**
* Set the resize threshold to maintain at worst a 2/3 load factor.
*/
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
根据源码已知,在第一次为threadLocal进行赋值的时候会创建初始大小的thraedLocalMap,并且通过setThraedhold方法设置thresholde,其值为当前哈希数组长度乘以2/3
/**
* Double the capacity of the table.
*/
private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
//新数组为原数组的2倍
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();
//遍历过程中如果遇到脏entry的话直接另value为null,有助于value能够被回收
if (k == null) {
e.value = null; // Help the GC
} else {
//重新确定entry在新数组的位置,然后进行插入
int h = k.threadLocalHashCode & (newLen - 1);
while (newTab[h] != null)
h = nextIndex(h, newLen);
newTab[h] = e;
count++;
}
}
}
//设置新哈希表的threshHold和size属性
setThreshold(newLen);
size = count;
table = newTab;
}
方法逻辑请看注释,新建一个大小为原来数组长度的两倍的数组,然后遍历旧数组中的entry并将其插入到新的hash数组中,主要注意的是,在扩容的过程中针对脏entry的话会令value为null,以便能够被垃圾回收器能够回收,解决隐藏的内存泄漏的问题。
3.3 getEntry方法private Entry getEntry(ThreadLocal<?> key) {
private Entry getEntry(ThreadLocal<?> key) {
//1. 确定在散列数组中的位置
int i = key.threadLocalHashCode & (table.length - 1);
//2. 根据索引i获取entry
Entry e = table[i];
//3. 满足条件则返回该entry
if (e != null && e.get() == key)
return e;
else
//4. 未查找到满足条件的entry,额外在做的处理
return getEntryAfterMiss(key, i, e);
}
方法逻辑很简单,若能当前定位的entry的key和查找的key相同的话就直接返回这个entry,否则的话就是在set的时候存在hash冲突的情况,需要通过getEntryAfterMiss做进一步处理。getEntryAfterMiss方法为:
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)
//找到和查询的key相同的entry则返回
return e;
if (k == null)
//解决脏entry的问题
expungeStaleEntry(i);
else
//继续向后环形查找
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
这个方法同样很好理解,通过nextIndex往后环形查找,如果找到和查询的key相同的entry的话就直接返回,如果在查找过程中遇到脏entry的话使用expungeStaleEntry方法进行处理。到目前为止,为了解决潜在的内存泄漏的问题,在set,resize,getEntry这些地方都会对这些脏entry进行处理,可见为了尽可能解决这个问题几乎无时无刻都在做出努力。
3.4 remove
/**
* Remove the entry for 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) {
//将entry的key置为null
e.clear();
//将该entry的value也置为null
expungeStaleEntry(i);
return;
}
}
}
该方法逻辑很简单,通过往后环形查找到与指定key相同的entry后,先通过clear方法将key置为null后,使其转换为一个脏entry,然后调用expungeStaleEntry方法将其value置为null,以便垃圾回收时能够清理,同时将table[i]置为null。