四、ThreadMap详解

多线程访问同一个变量容易引发并发问题,但在每个线程定义同一个变量又不太现实,ThreadLocal的出现就解决了这个问题,Thread为每个线程创建了一个线程变量副本,每个线程操作的数据都是属于自己的,不存在安全问题


实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class ThreadLocalTest
{
static ThreadLocal<String>localVal=new ThreadLocal<>();
static ThreadLocal<Integer>localInt=new ThreadLocal<>();

static void print(String str){
// 读取本地变量
System.out.println(str+":"+localVal.get());
// 移除本地变量
localVal.remove();
}

public static void main(String[] args) throws InterruptedException
{
Thread threadOne=new Thread(()->{
// 设置本地变量值
localVal.set("threadOne local");
localInt.set(123);
print("threadOne");
// 读取本地变量
System.out.println("threadOne remove after:"+localVal.get()+localInt.get());
});
Thread threadTwo=new Thread(()->{
// 设置本地变量值
localVal.set("threadTwo local");
print("threadOne");
System.out.println("threadOne remove after:"+localVal.get());
});
threadOne.start();
threadTwo.start();
}
}

最终输出:

1
2
3
4
threadOne:threadTwo local
threadOne:threadOne local
threadOne remove after:null
threadOne remove after:null123

如上我们明明在set()方法上设置了值,并使用get()获取,按照以往,都是以最后一次set为准,那这里为什么读取的两个线程不一样呢?很自然我们就想到了,它的set()和get()方法具有特殊性。

原理分析

Thread和ThreadLocal关系图
Thread类中有一个threadLocals和inheritableThreadLocals,他们都是ThreadLocalMap类(类似HashMap),默认情况下都为null,什么时候改变,我们看下源代码:
set方法:

1
2
3
4
5
6
7
8
public void set(T value) {
Thread t = Thread.currentThread();// 获取当前变量
ThreadLocalMap map = getMap(t); // 将当前变量作为参数去找到线程变量
if (map != null) // 如果存在则获取,不存在则初始化一个map
map.set(this, value); // 讲当前ThreadLocal变量this作为key放入map
else
createMap(t, value);
}

进一步看 getMap(t)方法:

1
2
3
ThreadLocalMap getMap(Thread t) {
return t.threadLocals; 获取当前线程的threadLocals
}

从这段代码,我们就知道,我们真正取到的变量是线程中的变量,createMap也是如此:

1
2
3
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}

再看get方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
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();
}

发现get方法与set方法差不多,都是从线程变量中以ThreadLocal作为key获取value值。
到这基本就已经了解了ThreadLocal的基本用法了。
ThreadLocal原理图

细心的同学们肯定发现了一个问题,inheritableThreadLocals还没讲呢,这是什么?

inheritableThreadLocals

问题: ThreadLocal不支持继承性(注意这里的继承不是说父类子类的继承),而是说主线程和子线程中ThreadLocal对各自是不可见的。
还是上面的例子,不过在随后加上下面两行:

1
2
System.out.println("main Int:" +localVal.get());
System.out.println("main str:"+localInt.get());

程序无论怎么运行,都会打印

1
2
main Int:null
main str:null

也就是说,子线程的本地变量对父线程不可见,同理父线程变量对子线程不可见。

子线程的本地变量对父线程不可见,可以理解,类似父类子类的关系,但父线程的线程变量对子线程可见还是蛮常见的,InheritableThreadLocal就是为此设计的。
查看源码:InheritableThreadLocal继承ThreadLocal并重写了一下三个方法

1
2
3
4
5
6
7
8
9
protected T childValue(T parentValue) {
return parentValue;
}
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}

也就是说set(T value)和get()方法是与ThreadLocal一致的,不同的地方在于map的获取与设置,操作的不再是threadLocals这个值,而是inheritableThreadLocals;
然后,它是怎么共享的inheritableThreadLocals的呢?其实原理比较简单,就是在构造线程的时候将当前线程(也就是父线程)的变量赋值到新创建的线程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0); // 初始化
}
private void init(ThreadGroup g, Runnable target, String name,
long stackSize) {
init(g, target, name, stackSize, null, true); // 初始化
}
private void init(ThreadGroup g, Runnable target, String name,long stackSize, AccessControlContext acc, boolean inheritThreadLocals) {
...
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);// 核心代码,如果父线程inheritableThreadLocals不为空,则使用父线程的inheritableThreadLocals去初始化新创建线程的本地变量
...
}
// 下面这段代码就是将key、value值进行一个复制
private ThreadLocalMap(ThreadLocalMap parentMap) {
Entry[] parentTable = parentMap.table;
int len = parentTable.length;
setThreshold(len);
table = new Entry[len];

for (int j = 0; j < len; j++) {
Entry e = parentTable[j];
if (e != null) {
@SuppressWarnings("unchecked")
ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
if (key != null) {
Object value = key.childValue(e.value);
Entry c = new Entry(key, value);
int h = key.threadLocalHashCode & (len - 1);
while (table[h] != null)
h = nextIndex(h, len);
table[h] = c;
size++;
}
}
}
}

其实由上面的代码可以看出,父线程的本地变量到子线程可见,是一个深拷贝,也就是说他们的值是不同步的,如果一方修改,那就不一样了。