大家好,我是小黑,一个在互联网得过且过的农民工。
从前上一期 并发编程之:synchronized 咱们学到要保障在并发状况下对于共享资源的平安拜访,就须要用到锁。
然而,加锁通常状况下会让运行效率升高,那有什么方法能够彻底防止对共享资源的竞争,同时又能够不影响效率呢?答案就是小黑明天要和大家讲的 ThreadLocal。
ThreadLocal 是什么?
该类提供了线程部分 (thread-local) 变量。这些变量不同于它们的一般对应物,因为拜访某个变量(通过其 get 或 set 办法)的每个线程都有本人的局部变量,它独立于变量的初始化正本。ThreadLocal 实例通常是类中的 private static 字段,它们心愿将状态与某一个线程(例如,用户 ID 或事务 ID)相关联。
以上起源官网 API。大略能够总结为两点:
- ThreadLocal 提供 get/set 办法,能够拜访属于以后线程的变量,也就是能够保障每个线程的变量不一样。
- ThreadLocal 应用时通常定义为 private static 的。
从字面意思了解,可能会将 ThreadLocal 认为是本地线程,其实 ThreadLocal 并不是线程,而是线程 Thread 的局部变量。
如何应用
首先,定义一个 private static 的 ThreadLocal 对象。
private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
而后每个线程能够将以后线程须要寄存在局部变量中,并且能够从中获取。
public void setAndGet(String name) {threadLocal.set(name);
String s = threadLocal.get();}
最初在应用完之后,须要将 ThreadLocal 中的值移除。
public void remove() {threadLocal.remove();
}
原理
那么 ThreadLocal 是如何做到保障每个线程 get 进去的数据不一样的呢?咱们通过源码来看一下。
public void set(T value) {Thread t = Thread.currentThread();
// 通过以后线程获取进去一个 ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
void createMap(Thread t, T firstValue) {t.threadLocals = new ThreadLocalMap(this, firstValue);
}
咱们发现在 set 办法中,会创立一个 ThreadLocalMap,而后将要设置的值放在这个 Map 中,而以后这个 ThreadLocal 对象作为 key;
而后再将这个 ThreadLocalMap 赋值给 Thread 的 threadLocals 里。如果去看 Thread 类的代码会发现,在 Thread 类中存在两个变量 threadLocals 和 inheritableThreadLocals,它们的类型就是 ThreadLocal.ThreadLocalMap。通过下图能够看出 Thread,ThreadLocal,ThreadLocalMap 三者的关系。
这时候咱们再看看 get()和 remove()办法的代码。
public T get() {Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();}
ThreadLocalMap getMap(Thread t) {return t.threadLocals;}
public void remove() {ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
能够看进去,和咱们下面图中的构造一样,get()办法就是从 Thread 中取出来 ThreadLocalMap,而后通过 ThreadLocal 对象作为 Key 取出值;remove()办法则是取出 ThreadLocalMap 将 ThreadLocal 对应的数据移除。
那么 ThreadLocalMap 到底是什么呢?是 java.util.Map 接口的子类吗?咱们来看源码。
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;
}
}
private static final int INITIAL_CAPACITY = 16;
private Entry[] table;
private int size = 0;
private int threshold; // Default to 0
// 省略其余代码
}
通过源码发现,ThreadLocalMap 并没有实现 Map 接口,也没有集成其余任何的 Map 类。是定义在 ThreadLocal 类中的一个动态外部类。而它的构造和 HashMap 构造极其类似。有一个 Entry[]数组存放数据。而这个 Entry 类是继承自 WeakReference 类的子类,这一点和 HashMap 有所不同。
ThreadLocalMap 和 HashMap 构造有很多不同,然而还有一点和 HashMap 不一样,咱们来看一下 ThreadLocalMap 的 set 办法。
private void set(ThreadLocal<?> key, Object value) {Entry[] tab = table;
int len = tab.length;
// 计算 key 对应放在 tab 中的下标
int i = key.threadLocalHashCode & (len-1);
// 循环遍历 tab, 首先获取到下标 i 对应的对象,如果不为空, 则执行循环体
// 如果不是雷同的 threadLocal 或者地位生效,则要寻找下一个地位。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;
}
// 如果 k ==null 代表 threadLocal 的 key 生效了,将生效的 key 进行替换
if (k == null) {replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();}
代码中循环遍历 tab, 首先获取到下标 i 对应的对象,如果不为空, 则执行循环体,如果在循环体中的两个条件都不满足,则会执行 nextIndex()办法,这个办法的代码如下:
private static int nextIndex(int i, int len) {return ((i + 1 < len) ? i + 1 : 0);
}
这个办法的逻辑就是 凋谢寻址法 。而 HashMap 则是通过 拉链法 和rehash来做的。
哪些场景应用
通过下面的内容根本能够把握 ThreadLocal 的根本用法,那么 ThreadLocal 次要在什么场景中应用呢。
ThreadLocal 的作用通过以上理解咱们晓得次要是用来做线程间数据隔离。那么在什么场景下能用到线程隔离呢?
首先想到的就是 SimpleDateFormat 这个工具类,它不是线程平安的,能够通过 ThreadLocal 在每个线程中放一份,保障线程平安。
还有比如说用户登录的 session,或者 token 数据,只数据以后会话线程,也能够通过 ThreadLocal 存储。
再比方在某些场景下,上下文数据在不同办法之间调用,传递起来十分麻烦,能够通过 ThreadLocal 寄存,只须要在须要用到的中央获取就能够。
除了这些场景,在某些框架源码中也会应用到,比方 Spring 中的事务也次要是通过 ThreadLocal 和面向切面编程 AOP 实现的,感兴趣的同学能够查看源码理解。
防止踩坑
内存透露
ThreadLocalMap 中的 Entry 的 Key 是一个弱援用,因而如果在应用后不调用 remove 办法革除掉会导致对应的 value 内存透露。所以在应用完当前肯定要记得调用 remove 办法革除数据。
好的,明天的内容就到这里,咱们下期再见。