【问题标题】:ThreadLocal get() returning null sometimes after spring boot upgrade from 2.1.4 to 3.4.3从 2.1.4 升级到 3.4.3 的 Spring Boot 升级后,ThreadLocal get() 有时会返回 null
【发布时间】:2021-05-05 15:58:34
【问题描述】:

这是代码:

public class ContextManagerImpl implements ContextManager {

    private static final ThreadLocal<Context> ctx = new ThreadLocal<Context>();
    @Override
    public Context getContext() {
        if(ctx.get() == null) {
            ctx.set(new Context("", ""));   // Dummy context. This should never happen
        }
        return ctx.get();
    }

    @Override
    public void begin(Context context){
        ctx.set(context);                            // Verified context passed is never null or blank 
    }

    @Override
    public void end() {
        if(ctx!= null) {
            ctx.remove();
        }
    }
}

public final class Context implements Serializable {
    private String s1;
    private String s2;
}

有 2 个线程正在使用具有不同线程本地上下文的此类。它在大多数情况下都能正常工作,但有时即使 begin() 正确设置了 ctx 中的值,getContext() 也会返回虚拟上下文。 我怀疑某处存在导致这种情况的竞争条件,但鉴于 threadlocal set() 和 get() 是线程安全的,并且 ctx 的初始化是在声明期间完成的,这永远不会发生。 注意:我已经将 spring boot 升级到 3.4.3 但仍然使用 JDK 8。

【问题讨论】:

    标签: java multithreading concurrency thread-safety thread-local


    【解决方案1】:

    在 Spring Boot 从 2.1.4 升级到 3.4.3 后,ThreadLocal get() 有时会返回 null

    如果 Spring 保证在 getContext() 之前调用 begin(...),那么在我看来,一个线程正在调用 begin(...) 方法,而另一个线程正在调用 getContext()。如果是这种情况,getContext() 将返回 null。

    ThreadLocal 旨在为每个线程提供一个不同的上下文。那是你要的吗?我不完全理解你的接线,但我怀疑你使用ThreadLocal。围绕线程的 Spring Boot 保证是什么?

    我怀疑您应该改为将其设为 volatile 字段:

     private static volatile Context ctx; // maybe set to new Context("", "");
     // this should be called by spring boot
     @Override
     public Context begin(Context ctx) {
        this.ctx = ctx;
     }
     public Context getContext() {
        return ctx;
     }
    

    如果您担心ContextManagerImpl 的多个实例使用相同的上下文初始化时会出现一些竞争条件,那么您可以改用AtomicReference

     // may want to initialize with a dummy context
     private static AtomicReference <Context> ctxRef = new AtomicReference();
     // this should be called by spring boot
     @Override
     public Context begin(Context ctx) {
        ctxRef.compareAndSet(null, ctx);
     }
     public Context getContext() {
        return ctxRef.get();
     }
    

    【讨论】:

    • 是的,可以保证在 getContext() 之前调用 begin()。是的,我希望每个线程都能获得不同的上下文。如果我使变量 volatile 或 AtomicReference 则两个线程将不会有不同的副本。问题是对于一个线程,即使调用 begin() 来设置上下文,getContext() 有时也会返回 null。鉴于线程旨在为每个线程提供不同的上下文,因此两个线程不应相互干扰。不知道我错过了什么。想知道是不是因为 spring boot 3.4.3 和 jdk 8 的兼容性?
    • 如果线程 1 调用开始设置上下文,然后线程 2 调用 getContext() 它将得到 null 对 @pankajsachan?我真的不认为那是你想要的。
    • 对不起@Gray 如果我之前不清楚,但两个线程都会在调用 getContext() 之前调用 begin(),因此不应该为 null。
    • 你确定@pankajsachan 每个调用getContext() 的线程都会先调用begin(...) 吗?我怀疑情况并非如此,因为您得到的是空值。如果你得到一个空值,那么你的代码不会像你怀疑的那样被调用。简单明了。
    • 提供更多详细信息,当 Grpc 请求到来时,在拦截器中我调用 begin(...),然后在 grpc 服务器实现类中处理请求期间,我调用 getContext(...) 以获取诸如标头之类的详细信息等我在begin(...) 中设置。我做了更多阅读,发现不能保证 grpc 请求和响应将在同一个线程中完成,因此当 grpc 请求作为线程 X 的一部分出现时,begin(...) 可能在该线程中被调用并处理/使用getContext(...) 的响应是在线程Y 中完成的?不过只是一个理论。
    猜你喜欢
    • 1970-01-01
    • 2021-10-15
    • 2015-08-04
    • 2015-12-16
    • 1970-01-01
    • 2022-12-22
    • 2022-01-18
    • 1970-01-01
    • 2020-09-15
    相关资源
    最近更新 更多