前言
多线程在访问同一个共享变量时很可能会出现并发问题,特别是在多线程对共享变量进行写入时,那么除了加锁还有其他方法避免并发问题吗?本文将详细讲解 ThreadLocal 的使用及其源码。
一、什么是 ThreadLocal?
ThreadLocal 是 JDK 包提供的,它提供了线程本地变量,也就是说,如果你创建了一个 ThreadLocal 变量,那么访问这个变量的每一个线程,都创建这个变量的一个本地副本。
这样可以解决什么问题呢?当多个线程操作这个变量时,实际操作的是自己线程本地内存里的数据,从而避免线程安全问题。
如下图,线程表中的每个线程,都有自己 ThreadLocal 变量,线程操作这个变量只是在自己的本地内存在,跟其他线程是隔离的。
二、如何使用 ThreadLocal
ThreadLocal 就是一个简单的容器,使用起来也没有难度,初始化后仅需通过 get/set 方法进行操作即可。
如下代码,开辟两个线程对 ThreadLocal 变量进行操作,获取的值是不同的。
public class FuXing {
/**
* 初始化ThreadLocal
*/
private static final ThreadLocal<String> myThreadLocal = new ThreadLocal<>();
public static void main (String[] args) {
// 线程1中操作 myThreadLocal
new Thread(()->{
myThreadLocal.set("thread 1"); //set方法设置值
System.out.println(myThreadLocal.get()); //get方法获取值"thread 1"
},"thread 1").start();
// 线程2中操作 myThreadLocal
new Thread(()->{
myThreadLocal.set("thread 2"); //set方法设置值
System.out.println(myThreadLocal.get()); //get方法获取值"thread 2"
},"thread 2").start();
}
}
复制代码
三、ThreadLocal 实现原理
ThreadLocal 是如何保证操作的对象只被当前线程进行访问呢,我们通过源码一起进行分析学习。
一般分析源码我们都先看它的构造方法是如何初始化的,接着通过对 ThreadLocal 的简单使用,我们知道了关键的两个方法 set/get,所以源码分析也按照这个顺序。
1. 构造方法
泛型类的空参构造,没有什么特别的
2. set 方法源码
源码如下,ThreadLocalMap 是什么呢?由于比较复杂,这里先不做解释,你暂时可以理解为是一个 HashMap,其中 key 为 ThreadLocal 当前对象,value 就是我们设置的值,后面会单独解释源码。
public void set(T value) {
//获取本地线程
Thread t = Thread.currentThread();
//获取当前线程下的threadLocals对象,对象类型是ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null)
//获取到则添加值
map.set(this, value);
else
//否则初始化ThreadLocalMap --第一次设置值
createMap(t, value);
}
复制代码
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
复制代码
3. get 方法源码
public T get() {
//获取本地线程
Thread t = Thread.currentThread();
//获取当前线程下的threadLocals对象,对象类型是ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
//通过当前的ThreadLocal作为key去获取对应value
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
//@SuppressWarnings忽略告警的注解
//"unchecked"表示未经检查的转换相关的警告,通常出现在泛型编程中
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//threadLocals为空或它的Entry为空时,需要对其进行初始化操作。
return setInitialValue();
}
复制代码
private T setInitialValue() {
//初始化为null
T value = initialValue();
//获取当前线程
Thread t = Thread.currentThread();
//获取当前线程下的threadLocals对象,对象类型是ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
//返回的其实就是个null
return value;
}
复制代码
protected T initialValue() {
return null;
}
复制代码
4. remove 方法源码
核心也是 ThreadLocalMap 中的 remove 方法,会删除 key 对应的 Entry,具体源码后面统一在 ThreadLocalMap 源码中分析。
public void remove() {
//获取当前线程下的threadLocals对象,对象类型是ThreadLocalMap
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
//通过当前的ThreadLocal作为key调用remove
m.remove(this);
}
复制代码
5. ThreadLocalMap 源码
ThreadLocalMap 是 ThreadLocal 的一个静态内部类,看了上面的几个源码解释,可以了解到 ThreadLocalMap 其实才是核心。
简单的说,ThreadLocalMap 与 HashMap 类似,如,初始容量 16,一定范围内扩容,Entry 数组存储等,那它与 HashMap 有什么不同呢,下面将对源码进行详解。
ThreadLocalMap 的底层数据结构:
5.1 常量
//初始容量,一定是2的幂等数。
private static final int INITIAL_CAPACITY = 16;
// Entry 数组
private Entry[] table;
//table的长度
private int size = 0;
//扩容阈值
private int threshold;
//设置扩容阈值,长度的 2 / 3
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
//计算下一个存储位置
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
// 计算前一个存储位置
private static int prevIndex(int i, int len) {
return ((i - 1 >= 0) ? i - 1 : len - 1);
}
复制代码
5.2 Entry 相关源码
由于 Entry 是底层核心源码,所有的操作几乎都是围绕着它来进行的,所以关于 Entry 的源码会比较多,我一一拆分进行分析讲解。
静态内部类 Entry
这个是 ThreadLocalMap 的底层数据结构,Entry 数组,每个 Entry 对象,这里的 Entry 继承了 WeakReference,关于弱引用不懂得,可以看我的另一篇文章《Java 引用》。
然后将 Entry 的 key 设置承了 弱引用,这有什么作用呢?作用是当 ThreadLocal 失去强引用后,在系统 GC 时,只要发现弱引用,不管系统堆空间使用是否充足,都会回收掉 key,进而 Entry 被内部清理。
//静态内部类Entry
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
// key为弱引用
super(k);
value = v;
}
}
复制代码
获取 Entry
拿到当前线程中对应的 ThreadLocal 所在的 Entry,找不到的话会重新寻找,因为当前的 Entry 可能已经扩容,扩容后会重新计算索引位置,详情见扩容机制源码。
源码中的计算索引位置的算法我没有解释,这个我会放在后面解释,涉及到了如何解决 Hash 冲突的问题,这个和我们熟知的 HashMap 是不同的。
//获取Entry
private Entry getEntry(ThreadLocal<?> key) {
//计算索引位置
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
//找到了就返回Entry
if (e != null && e.get() == key)
return e;
else
//没找到则重新寻找,因为可能发生扩容导致索引重新计算
return getEntryAfterMiss(key, i, e);
}
//重新获取Entry --从当前索引i的位置向后搜索
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
//循环遍历,获取对应的 ThreadLocal 所在的 Entry
while (e != null) {
//获取Entry对象的弱引用,WeakReference的方法
ThreadLocal<?> k = e.get();
if (k == key)
return e;
if (k == null)
//清除无效 Entry,详解见下方
expungeStaleEntry(i);
else
//计算下一个索引位置
i = nextIndex(i, len);
//可以理解为指针后移
e = tab[i];
}
return null;
}
复制代码
清除无效 Entry
expunge 删除,抹去,stale 陈旧的,没有用的
第 1 个方法:根据索引删除对应的桶位,并从给定索引开始,遍历清除无效的 Entry,何为无效?就是当 Entry 的 key 为 null 时,代表 key 已经被 GC 掉了,对应的 Entry 就无效了。
第 2 个方法:删除 Entry 数组中所有无效的 Entry,方法中的e.get() == null
,代表 key 被回收了。
第 3 个方法:清除一些失效桶位,它执行对数数量的扫描,向后遍历 logn 个位置,如 8,4,2,1。
方法 2、3 最后都通过方法 1 进行桶位的删除。
//根据索引删除对应的桶位
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
//删除该桶位的元素,并将数组长度减1
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
Entry e;
int i;
//从当前索引开始,直到当前 Entry为null才会停止遍历
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
//获取Entry对象的弱引用,WeakReference的方法
ThreadLocal<?> k = e.get();
if (k == null) {//说明key已失效
//删除该桶位的元素,并将数组长度减1
e.value = null;
tab[i] = null;
size--;
} else {//说明key有效,需要将其Rehash
//计算rehash后索引位置
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
//移动元素位置,若rehash后索引位置有其他元素,则继续向后移动,直至为空
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
//直到当前 Entry为null才会停止遍历,i为其索引
return i;
}
//删除Entry数组中所有无效的Entry,用于rehash时
private void expungeStaleEntries() {
Entry[] tab = table;
int len = tab.length;
for (int j = 0; j < len; j++) {
Entry e = tab[j];
//获取Entry对象的弱引用,Entry不为空而弱引用为空,代表被GC了
if (e != null && e.get() == null)
//根据索引删除对应的桶位
expungeStaleEntry(j);
}
}
//清楚一些清除桶位,它执行对数数量的扫描
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
//向后遍历logn个位置,如8,4,2,1
do {
i = nextIndex(i, len);
Entry e = tab[i];
//获取Entry对象的弱引用,Entry不为空而弱引用为空,代表被GC了
if (e != null && e.get() == null) {
n = len;
removed = true;
//根据索引删除对应的桶位
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0);//对数递减
return removed;
}