前言
在java并发编程中,很多地方都可以看到ThreadLocal,其作用是为了解决线程安全,即实现多线程隔离的。今天笔者主要来分析分析对Threadlocal的学习历程。
一、什么是ThreadLocal?
要搞懂这玩意的原理,得先知道它是个啥?
ThreadLocal叫做线程变量,意思是ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的,也就是说该变量是当前线程独有的变量。
ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。
看了基本的概念,我们看一个例子便就清楚:
public class Demo {
//定义一个全局的ThreadLocal变量
public static final ThreadLocal<String> STRING_THREAD_LOCAL = ThreadLocal.withInitial(() -> "HELLO");
public static void main(String[] args) throws InterruptedException {
System.out.println(Thread.currentThread().getName() + "-->start:" +STRING_THREAD_LOCAL.get());
Thread t1 = new Thread(() -> {
String value = STRING_THREAD_LOCAL.get();
if(Objects.nonNull(value) && value.equals("HELLO")){
STRING_THREAD_LOCAL.set("WORLD");
}
System.out.println(Thread.currentThread().getName() + "-->" + STRING_THREAD_LOCAL.get());
},"t1");
t1.start();
t1.join();
System.out.println(Thread.currentThread().getName() + "-->end:" +STRING_THREAD_LOCAL.get());
}
}
复制代码
- 我们看一下结果:
可以看出main线程的值并没有发生变化,而线程t1却已经是修改。
二、ThreadLocal原理分析
既然已经通过上述案例看出,threadlocal可以实现多线程之间的隔离,使每个线程之间修改共享变量变的副本仅自己可见。解决了线程之间共享的问题
2.1 方法解析
看一下官方的set源码,可以发现一个很关键的:ThreadLocalMap
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
复制代码
看来了解set方法之前,我们得先看看ThreadLocalMap
2.2ThreadLocalMap
ThreadLocalMap其实是每一个线程都会维护的一个成员变量。
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
//.....
}
复制代码
从源码可以看出ThreadLocalMap内部还有一个继承弱引用的entry对象数组。
ThreadLocalMap的key其实就是ThreadLocal实例的弱引用,而value便是ThreadLocal的初始值或者线程set进去的值。
2.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);
}
复制代码
不难看出:set方法其实就是给当前线程中设置一个值,并且存放于ThreadLocalMap中。
createMap方法则是在getMap为空时,去对Map进行一个初始化并设置值
- ThreadLocalMap未初始化
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
//初始化一个长度为16的table数组
table = new Entry[INITIAL_CAPACITY];
//通过路由算法,进行索引下标的计算
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
复制代码
- ThreadLocalMap已初始化
如果已经初始化过,则直接调用ThreadLocalMap.set()保持即可。
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
//根据索引下标遍历Entry数组
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
//key相等,则覆盖原有的value
if (k == key) {
e.value = value;
return;
}
//key为空,则用key、value覆盖。
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
//如果超过了阈值、进行扩容
if (!cleanSomeSlots(i, sz) && sz >= threshold)
//for循环,rehash();
rehash();
}
复制代码
2.4ThreadLocalMap中的hash冲突
看到这的读者同学应该会发现,其实ThreadLocalMap的set方法与HashMap的put方法很像;但是同时也有一个问题:
ThreadLocalMap并没有链表结构,那么hash冲突又是怎么解决的呢?
- 根据key计算出当前元素存储的索引下标i。
- tab[i] != null
- tab[i] 存放的key与当前key不一致,则继续向下寻找为空的位置
- tab[i] 存放的key一致,但value不一致,则更新value
- tab[i] 存放的key为null,则ThreadLocal实例可能已被被回收。
- tab[i] == null,则直接将key、value存储即可。
其实说白了,就是线性for循环,遍历为空的位置解决hash冲突。
2.5替换并清理replaceStaleEntry方法
当key==null的时候,使用replaceStaleEntry方法将当前的key、value去覆盖空key和value
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
int slotToExpunge = staleSlot;
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;
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;
if (slotToExpunge == staleSlot)
slotToExpunge = i;
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
复制代码
三、其他问题
3.1 内存泄漏
内存泄漏:简单说就是:开辟的空间在使用完成后未释放,导致内存一直占据。
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
复制代码
Threadlocal把自己的弱引用实例对象当作key存在了ThreadLocalMap中,这样就导致在没有外部强引用的情况下,key会被回收,而如果创建ThreadLocal的线程一直持续运行,Entry对象中的value将可能产生内存泄漏(一直无法被回收)。
- 解决办法:
在使用Threadlocal的时候,在代码最后去使用remove()方法即可。
3.2弱引用
弱引用:即非必需内存,当JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在java中,用java.lang.ref.WeakReference类来表示
其实key设计成弱引用:也是为了防止内存泄漏的发生。