在前面的博客《线程安全的代码及ThreadLocal的使用》中说到了ThreadLocal可以在同一个线程中实现参数传递,如果在某些需要异步处理的情况下,就需要在当前线程的执行逻辑中新启动一个子线程,那这个子线程如何共享父线程的参数呢?
除了通过方法形参传入之外,还可以通过InheritableThreadLocal
在子线程创建的时候从父线程中同步数据。
在一个线程中执行new Thread()
来创建线程,那么当前线程就是这个新创建线程的父线程。我们任意打开一个Thread
的构造方法,可以看到里面调用了一个init()
方法。
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
进入这个方法中,会找到一行代码Thread parent = currentThread();
,在新的线程对象的初始化过程中,就会依赖这个parent
从父线程中“继承”一部分数据,我们现在要聊的InheritableThreadLocal
也是通过这里的parent
来实现父子线程数据共享的。
下面使用一个简单Demo来演示父子线程的数据共享:
public class InheritableThreadLocalDemo {
static InheritableThreadLocal<String> local = new InheritableThreadLocal<>();
public static void testParent() {
local.set("I'm parent");
Thread child = new Thread(InheritableThreadLocalDemo::testChild, "子线程");
child.start();
try {
child.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 查看父线程的值是否对子线程有影响
print();
}
public static void testChild() {
print();
// 在子线程中修改值,后续查看父线程是否受影响
local.set("I'm child");
// 查看子线程是否修改成功
print();
}
public static void print() {
System.out.println("当前线程为:" + Thread.currentThread().getName() + "; " +
"当前ThreadLocal中的值为:" + local.get());
}
public static void main(String[] args) {
// 命名为父线程,方便后面打印结果
Thread parent = new Thread(InheritableThreadLocalDemo::testParent, "父线程");
parent.start();
}
}
当前线程为:子线程; 当前ThreadLocal中的值为:I’m parent
当前线程为:子线程; 当前ThreadLocal中的值为:I’m child
当前线程为:父线程; 当前ThreadLocal中的值为:I’m parent
打印的结果显示,子线程在没有set
值的情况下的,可以直接使用到父线程在InheritableThreadLocal
设置的值,这里是实现了父线程向子线程共享数据。
接着使用子线程重新设置了一个新的值,这个值的作为范围只在子线程上,不会影响到父线程的数据。
InheritableThreadLocal
是ThreadLocal
的一个子类,大部分的实现原理是一样的,可以看一下上一篇《线程本地变量的实现——ThreadLocal原理详解》,然后看看有差异的地方,里面重写了里面的三个方法,childValue()
后面在聊,另外两个重写的方法,其实就是把对Thread
对象中的threadLocals
变量的操作替换成了对inheritableThreadLocals
变量的操作,就这么一点区别。
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
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);
}
}
补充一下:Thread
类中与ThreadLocalMap
相关的两个变量。
既然InheritableThreadLocal
与ThreadLocal
并没有多大区别,那它是怎么实现的父线程向子线程传递数据呢?
在上面1.1中说到了一个parent
变量,在Thread
的init()
方法中有这么几行代码:
if (parent.inheritableThreadLocals != null) {
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
}
上面的this
指的是调用new Thread()
方法时新增的Thread
对象。后面调用createInheritedMap()
将parent
中的Map对象传了进去,可以猜想一下,数据传递就发生在这个方法中。现在点进去验证一下,是不是在做数据传递:
private ThreadLocalMap(ThreadLocalMap parentMap) {
// 通过parent的Map参数来创建新的Map
Entry[] parentTable = parentMap.table;
int len = parentTable.length;
setThreshold(len);
table = new Entry[len];
for (int j = 0; j < len; j++) {
// 将parent的Map中的数据,传递到新的Map中
Entry e = parentTable[j];
if (e != null) {
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++;
}
}
}
}
通过上面这种方式,就将父线程的Map中的数据传递到了子线程的Map中,并且由于子线程中的inheritableThreadLocals
是一个新的对象,与父线程之间互不影响。
上面提到的InheritableThreadLocal
类中,还有一个childValue()
方法,里面的实现就是传入什么就返回什么。我们可以看到,在父子线程做数据传递时使用了这个方法。
Object value = key.childValue(e.value);
为什么在这里不直接使用e.value
,要多此一举的去调用一次呢?
在这个方法的注释上面写了:
This method merely returns its input argument, and should be overridden if a different behavior is desired.
意思就是说,这里就是传入什么就返回什么,如果想要这个方法有不同的行为的话,应该去重写这个方法。
childValue()
这个方法是protected
修饰的,也就是说我们可以自定义一个类去继承InheritableThreadLocal
类,然后重写这个方法,在收到父线程传递过来的数据的时候,去做些特殊的操作。
比如对传入的数据做一点修改再返回,或者打印个日志什么的。
public class InheritableThreadLocalExtendDemo<T> extends InheritableThreadLocal<T> {
static InheritableThreadLocalExtendDemo<String> local = new InheritableThreadLocalExtendDemo<>();
@Override
protected T childValue(T parentValue) {
System.out.println("进入父子线程传递数据方法");
if (parentValue instanceof String) {
parentValue = (T) ("姓名:" + parentValue);
}
return parentValue;
}
public static void main(String[] args) {
local.set("张三");
new Thread(() -> System.out.println(local.get())).start();
}
}
姓名:张三
通过上面的代码分析可以知道,父子线程的数据传递是在父线程中创建子线程时发生的,但是线程池中的线程是可复用的,这种情况下由于没有新建的动作,数据自然就不会发生传递。
但是如果线程池中的线程数量没有到达最大线程数,那么还是会存在新建线程的动作,这时候传递又是生效的,所以使用线程池的时候,父子线程间的数据传递并不稳定,尽可能的不在线程池中使用InheritableThreadLocal
。