ThreadLocal是线程本地变量,用来解决并发下数据隔离性的问题,不能解决共享。
他可以将一个变量拷贝的线程内,线程调用时再线程内进行使用,相当于给每个线程复制一个副本供各个线程使用。
ThreadLocal简单使用
他的目的很简单,就是让每个线程操控同一个变量做不同的事情,互不干扰
我们这里使用栈进行对比,用来查看高并发下的问题
@Slf4j
public class TestThreadLocal {
public static void main(String[] args) {
ThreadLocal<String> local = new ThreadLocal<>();
Deque<String> stack = new ArrayDeque<>();
for (int i = 0; i < 10; i++) {
final int j = i;
new Thread(()->{
String s = Thread.currentThread().getName()+"::"+j;
local.set(s);
stack.push(s);
log.debug("local--->{}",local.get());
//解决内存泄露问题
local.remove();
if(!stack.isEmpty()){
log.debug("stack-->{}",stack.pop());
}else{
log.debug("null---");
}
},"t"+i).start();
}
}
}
运行结果:
# 这里使用local没出现问题,每个线程打印的都是自己存入的数据
15:03:56.862 [t8] DEBUG JUCTest.ThreadLocalTest.TestThreadLocal - local--->t8::8
15:03:56.862 [t6] DEBUG JUCTest.ThreadLocalTest.TestThreadLocal - local--->t6::6
15:03:56.862 [t9] DEBUG JUCTest.ThreadLocalTest.TestThreadLocal - local--->t9::9
15:03:56.862 [t1] DEBUG JUCTest.ThreadLocalTest.TestThreadLocal - local--->t1::1
15:03:56.862 [t7] DEBUG JUCTest.ThreadLocalTest.TestThreadLocal - local--->t7::7
# 这里就已经出问题了,t9线程打印了t6线程存的数据
15:03:56.866 [t9] DEBUG JUCTest.ThreadLocalTest.TestThreadLocal - stack-->t6::6
15:03:56.866 [t6] DEBUG JUCTest.ThreadLocalTest.TestThreadLocal - stack-->t9::9
15:03:56.866 [t7] DEBUG JUCTest.ThreadLocalTest.TestThreadLocal - stack-->t7::7
15:03:56.862 [t2] DEBUG JUCTest.ThreadLocalTest.TestThreadLocal - local--->t2::2
15:03:56.862 [t4] DEBUG JUCTest.ThreadLocalTest.TestThreadLocal - local--->t4::4
15:03:56.866 [t8] DEBUG JUCTest.ThreadLocalTest.TestThreadLocal - stack-->t8::8
15:03:56.862 [t3] DEBUG JUCTest.ThreadLocalTest.TestThreadLocal - local--->t3::3
15:03:56.866 [t4] DEBUG JUCTest.ThreadLocalTest.TestThreadLocal - stack-->t3::3
15:03:56.862 [t5] DEBUG JUCTest.ThreadLocalTest.TestThreadLocal - local--->t5::5
15:03:56.866 [t3] DEBUG JUCTest.ThreadLocalTest.TestThreadLocal - stack-->t2::2
15:03:56.866 [t5] DEBUG JUCTest.ThreadLocalTest.TestThreadLocal - stack-->t1::1
15:03:56.862 [t0] DEBUG JUCTest.ThreadLocalTest.TestThreadLocal - local--->t0::0
15:03:56.866 [t1] DEBUG JUCTest.ThreadLocalTest.TestThreadLocal - stack-->t5::5
15:03:56.866 [t2] DEBUG JUCTest.ThreadLocalTest.TestThreadLocal - stack-->t4::4
15:03:56.866 [t0] DEBUG JUCTest.ThreadLocalTest.TestThreadLocal - stack-->t0::0
上面的例子应该已经帮我们很好的理解了ThreadLocal的作用,以及我们使用其他存储的对比,接下来我们看看ThreadLocal内部是怎么实现的
原理
看原理之前我们先了解一下ThreadLocal的存储结构,以及Thread、ThreadLocal、ThreadLocalMap之间的关系:
我们看Thread的属性
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
Thread内部属性默认是有ThreadLocalMap的,不过默认为null罢了,而ThreadLocalMap中存储的键值结构是<ThreadLocal,value>
,所以实际可以用多个ThreadLocal存取不同的值
ThreadLocal结构
class ThreadLocal{
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
//...
}
}
//....
}
每个ThreadLocal内有一个ThreadLocalMap,ThreadLocalMap类似HashMap,但是也有不同,其中包含一个Entry,类似hashmap的Entry,不过有区别,下面会详细说。
我们使用ThreadLocal无非就那几个方法,所以我们直接挑重点看
- set
public void set(T value) {
//获取到当前调用的线程
Thread t = Thread.currentThread();
//查看当前线程是否已经实例化过ThreadLocalMap
ThreadLocalMap map = getMap(t);
//如果已经实例化过,那么就将当前ThreadLocal作为key,value作为值加入map
if (map != null)
map.set(this, value);
else
//否则创建ThreadLocalMap
createMap(t, value);
}
- getMap,用来返回线程的ThreadLocalMap
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
- createMap
void createMap(Thread t, T firstValue) {
//实例化当前ThreadLocalMap
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
如果该线程的ThreadLocalMap还没被创建,那么在这将会实例化一次,并且该线程的ThreadLocalMap以后会有永远拥有,另一个ThreadLocal来存储的时候会直接使用该ThreadLocalMap
- 实例化ThreadLocalMap
//可以看到 local会是key,value作为值
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);
}
整个ThreadLocalMap内包含了一个Entry数组,用于存放不同的键值
所以这里set方法的流程是:
-
获取当前线程对象
-
查看该线程内的ThreadLocalMap是否被实例化过
-
如果没有被实例化,那么进行实例化,并将当前ThreadLocal和值存进去
-
如果被实例化过,那么将当前ThreadLocal找个合适的位置存入,
map.set(this, 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.
//获取当前map内的entry数组
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)]) {
ThreadLocal<?> k = e.get();
//如果当前ThreadLocal已经存入值,那么进行覆盖并返回
if (k == key) {
e.value = value;
return;
}
//如果当前值过期,也就是弱引用失效,将进行替换,将当前key替换进去,这里不展开讲
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
//到这说明当前ThreadLocal没有被放入过,所以这里直接new
tab[i] = new Entry(key, value);
int sz = ++size;
//解决hash冲突,如果ThreadLocal有很多的情况下
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
如果当前线程的ThreadLocalMap已经存在了,再次加入ThreadLocal和value无非两种情况,之前加入过和没加入过,加入过的找到并替换,没加入的直接new一个新的对象出来
到这里set方法基本结束
接下来看get方法
- get
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
//被实例化过
if (map != null) {
//找当前ThreadLocal存放的位置
ThreadLocalMap.Entry e = map.getEntry(this);
//如果已经找到,直接将值返回,没找到说明以前就没存过该值
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//没实例化过先实例化,然后将null设置进去,返回null
return setInitialValue();
}
- setInitialValue
private T setInitialValue() {
T value = initialValue(); //initialValue()返回值为null
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
接下来我们看怎么找到的该ThreadLocal对应的值
- getEntry
private Entry getEntry(ThreadLocal<?> key) {
//定位下标,定位到直接返回
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
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();
if (k == key)
return e;
if (k == null)
//如果为null,往后寻找一段,找到脏entry进行清理,直到找到null为止
expungeStaleEntry(i);
else
//开发地址法找i下标
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
以上是ThreadLocal常用方法的原理
差别
前文我们说了,ThreadLocalMap和HashMap类似,但是也有差别:
- HashMap实现方式为数组+链表
- Entry内保存key和value值,是强引用关系
- 采用拉链法寻值
- ThreadLocalMap实现方式为数组
- Entry仅仅存储了value值
- 采用开放地址法寻值,当前位置有hash冲突时就会一直往下找,直到找到可用的为止
内存泄漏
我们知道ThreadLocal是弱引用关系,那么当GC之后ThreadLocal被清理掉后,value还没被清理掉,此时会造成内存泄露。
之所以是弱引用,是因为每个Entry的key虽然是ThreadLocal,但是并不是直接指向,Entry内没有让ThreadLocal存储的位置,都在Entry的父类里
解决内存泄露的方法也很简单,就是在使用完之后手动remove就可以了