背景
在并发编程的场景下,我们经常会遇到线程不安全的情况。针对这种线程不安全的场景,我们可以加锁、使用线程安全的容器或集合或者是ThreadLocal来解决。加锁或者使用线程安全的容器或集合都是为了保证同一时刻只有一个线程可以访问该资源,而ThreadLocal则是由于ThreadLocal是每个线程独享的内容,所以也不存在线程不安全的情况。
但是在使用ThreadLocal存储变量的时候,我们无法进行异步编程,因为异步是开启了一个新的线程,原有线程中的ThreadLocal数据不会自动同步到新建线程中,我们也可以手动同步ThreadLocal的数据到新线程中,但是这样的话过程太过繁琐。幸好Java本身支持了InheritedThreadLocal,InheritedThreadLocal继承自ThreadLocal,与ThreadLocal的区别是当子线程创建时,会从父线程继承InheritedThreadLocal的值,这样的话,我们在异步编程的时候就能通过ThreadLocal进行变量传递了。
问题
那我们就用InheritedThreadLocal试试变量传递。我们首先定义了一个基于InheritedThreadLocal的变量传递Utils,
public class InheritableThreadLocalUtil {
private static final InheritableThreadLocal<Map<String, Object>> threadLocal = new InheritableThreadLocal() {
@Override
protected Object initialValue() {
return new HashMap(4);
}
@Override
protected Object childValue(Object parentValue) {
return super.childValue(parentValue);
}
};
public static <T> T get(String key) {
Map map = (Map)threadLocal.get();
return (T)map.get(key);
}
public static void set(String key, Object value) {
Map map = (Map)threadLocal.get();
map.put(key, value);
}
public static <T> T remove(String key) {
Map map = (Map)threadLocal.get();
return (T)map.remove(key);
}
}
然后我们使用只有一个线程的线程池测试下效果。
public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(1);
InheritableThreadLocalUtil .set("fruit", "apple");
Runnable test1 = () -> {
System.out.println("当前线程的名称为:" + Thread.currentThread().getName() + "; 获取到的水果名称为: " + InheritableThreadLocalUtil .get("fruit"));
};
Runnable test2 = () -> {
System.out.println("当前线程的名称为:" + Thread.currentThread().getName() + "; 获取到的水果名称为: " + InheritableThreadLocalUtil .get("fruit"));
};
executorService.submit(test1);
Thread.sleep(10);
InheritableThreadLocalUtil .set("fruit", "banana");
executorService.submit(test2);
// 关闭 ExecutorService
executorService.shutdown();
}
输出内容如下:
当前线程的名称为:pool-1-thread-1; 获取到的水果名称为: apple
当前线程的名称为:pool-1-thread-1; 获取到的水果名称为: banana
实测过程中,我们发现InheritedThreadLocal确实可以实现跨线程的变量传递。但是,我们还发现一个很严重的问题,我们在第一个线程执行后,在主线程中修改了fruit对应的值为banana后,这个改动在第二个线程中的变量中生效了,理论上来说,InheritedThreadLocal这个只会在新建线程的时候才会继承数据啊,我们使用了线程池,理论上是线程复用的,我们在主线程中的修改在第二个线程中应该不生效啊。
排查
那究竟是怎么回事呢?我们看下InheritedThreadLocal这个类的具体实现就能发现根因。
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
/**
* Computes the child's initial value for this inheritable thread-local
* variable as a function of the parent's value at the time the child
* thread is created. This method is called from within the parent
* thread before the child is started.
* <p>
* This method merely returns its input argument, and should be overridden
* if a different behavior is desired.
*
* @param parentValue the parent thread's value
* @return the child thread's initial value
*/
protected T childValue(T parentValue) {
return parentValue;
}
/**
* Get the map associated with a ThreadLocal.
*
* @param t the current thread
*/
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
/**
* Create the map associated with a ThreadLocal.
*
* @param t the current thread
* @param firstValue value for the initial entry of the table.
*/
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
}
重点是childValue这个方法,看下默认实现,它是直接返回了父线程中的整个对象,也就是说,整个对象是父线程和子线程共享的。如果是可变对象,那么在父线程中的修改也会影响到子线程,反过来也是同理。因此,上述我们在父线程中的修改影响到了子线程。那么我们如何避免这个问题呢?
参考官方介绍,我们可以重写childValue方法,重写后的方法如下(注意,这里是浅拷贝,如果我们是修改Map对象内部对象的一些属性,不同线程之间还是会有干扰,如果想完全隔离,建议使用序列化等方式的深拷贝):
private static final InheritableThreadLocal<Map<String, Object>> threadLocal = new InheritableThreadLocal() {
@Override
protected Object initialValue() {
return new HashMap<>(4);
}
@Override
protected Object childValue(Object parentValue) {
if(parentValue instanceof Map) {
return new HashMap<>((Map) parentValue);
}
return super.childValue(parentValue);
}
};
然后我们再运行上面的测试代码,发现结果输出符合预期:
当前线程的名称为:pool-1-thread-1; 获取到的水果名称为: apple
当前线程的名称为:pool-1-thread-1; 获取到的水果名称为: apple
结论
我们在使用ThreadLocal类进行跨线程的变量传递时,一定要注意对象引用的问题,不只是InheritedThreadLocal这个类存在上述问题,阿里开源的TransmittableThreadLocal由于继承自InheritedThreadLocal,也会存在此类问题,因此我们在开发过程中一定要多加注意,深究源码,否则很容易出现错误。
参考资料:
- https://www.jianshu.com/p/2bc95bbf7595
- https://github.com/alibaba/transmittable-thread-local/issues/123
- https://luxinfeng.top/article/ThreadLocal%E9%87%8C%E7%9A%84%E5%8F%98%E9%87%8F%E4%B8%80%E5%AE%9A%E6%98%AF%E7%BA%BF%E7%A8%8B%E7%8B%AC%E4%BA%AB%E7%9A%84%E5%90%97%EF%BC%9F
有了InheritedThreadLocal,我们已经可以实现变量的跨线程传递了,那么阿里为什么还要开源TransmittableThreadLocal工具呢?那就是我们后续要讲述的内容了