Java8 到 Java17 ThreadLocal 问题

问题描述 投票:0回答:1

我的代码在 Java8 中运行良好,但当我将其迁移到 Java17 时,它就不起作用了。涉及到ThreadLocal和CompletableFuture.runAsync。

以下是课程:

public class UriParameterHandler {
}

public class DateRangeEntity {
    public String getCurrentDate(){
        DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss");
        LocalDateTime now = LocalDateTime.now();
        return dtf.format(now);
    }
}

public class SessionHandler {

    private static ThreadLocal<SessionHandler> instance = new InheritableThreadLocal<>();
    private UriParameterHandler uriParameterHandler;
    private DateRangeEntity dateRangeEntity;

    private SessionHandler() {
        instance.set(this);
    }

    public static void initialize() {
        SessionHandler handler = new SessionHandler();
        handler.uriParameterHandler = new UriParameterHandler();
    }

    public static UriParameterHandler getUriParameterHandler() {
        return instance.get().uriParameterHandler;
    }

    public static void setUriParameterHandler(UriParameterHandler uriParameterHandler) {
        instance.get().uriParameterHandler = uriParameterHandler;
    }

    public static DateRangeEntity getDateRangeEntity() {
        return instance.get().dateRangeEntity;
    }

    public static void setDateRangeEntity(DateRangeEntity dateRangeEntity) {
        instance.get().dateRangeEntity = dateRangeEntity;
    }
}

public class LocalThread implements Runnable{
    @Override
    public void run() {
        if(SessionHandler.getDateRangeEntity()!=null){
            System.out.println("not null");
        }else{
            System.out.println("is null");
        }
    }
}

public class SessionHandlerMain {

    public static void main(String[] args) {
        threadLocalDemo();
    }

    private static void threadLocalDemo(){
        SessionHandler.initialize();
        SessionHandler.setDateRangeEntity(new DateRangeEntity());

        //works in java8 but not in java17
        CompletableFuture.runAsync(()->{
            if(SessionHandler.getDateRangeEntity()!=null){
                System.out.println("not null");
            }else{
                System.out.println("is null");
            }

        }).exceptionally(e->{
            e.printStackTrace();
            return null;
        });

        /*
        LocalThread localThread = new LocalThread();
        localThread.run();
         */
    }
}

在 SessionHandlerMain 的 threadLocalDemo() 中,我首先为 SessionHandler 设置新的 DateRangeEntity 对象,然后在 runAsync() 方法中调用 getDateRangeEntity() 来检查该对象是否不为 null。这在 java8 中有效并打印“not null”,但是当我迁移到 Java 17 时,这引发了此异常:

java.util.concurrent.CompletionException: java.lang.NullPointerException: Cannot read field dateRangeEntity because the return value of java.lang.ThreadLocal.get() is null
        at java.base/java.util.concurrent.CompletableFuture.encodeThrowable(CompletableFuture.java:315)
        at java.base/java.util.concurrent.CompletableFuture.completeThrowable(CompletableFuture.java:320)
        at java.base/java.util.concurrent.CompletableFuture$AsyncRun.run(CompletableFuture.java:1807)
        at java.base/java.util.concurrent.CompletableFuture$AsyncRun.exec(CompletableFuture.java:1796)
        at java.base/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:373)
        at java.base/java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1182)
        at java.base/java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1655)
        at java.base/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1622)
        at java.base/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:165)
Caused by: java.lang.NullPointerException: Cannot read field dateRangeEntity because the return value of java.lang.ThreadLocal.get() is null

但是,如果我将 runAsync() 方法的逻辑移动到扩展 Runnable 的类中,那么这在 java17 中可以工作。

有人可以向我提供一些关于为什么这种行为在 java17 中不同以及是否有其他解决方法的见解吗?

非常感谢:-)

java multithreading completable-future java-17 thread-local
1个回答
1
投票

您的代码已损坏,它在大多数情况下都可以工作只是偶然。

A

ThreadLocal
提供每个线程唯一的存储位置。不同的线程,不同的存储位置。这意味着如果您在不同的线程上,您应该期望得到不同的东西
ThreadLocal

您运行代码,使用单个参数查询

ThreadLocal
内的
CompletableFuture.runAsync
。这意味着代码将在
ForkJoinPool.commonPool()
执行器上运行,即在线程池中的某个线程上运行。

您绝对不应该依赖此类代码中

ThreadLocal
内的任何值。


那么为什么代码大部分时间都能工作呢?这是因为您将

ThreadLocal
制作为
InheritableThreadLocal
(这对于“存储服务实例”来说是一个坏主意 - 要么您的服务是线程安全的,您应该全局使用一个,要么不是(就像您的服务一样) )并且在线程之间共享它被破坏)。这个类的特殊之处在于,当创建一个新线程时,
InheritableThreadLocal
会被初始化为与父线程(即创建新线程的线程)相同的值,而普通的
ThreadLocal
只是初始化为
null 
.

未指定

ForkJoinPool.commonPool()
何时以及如何初始化。最可能的策略是延迟创建,即首次调用方法时创建池。因此,通常,池是在您调用
CompletableFuture.runAsync()
时创建的,因此池中的线程是主线程的子线程,继承您已存储在
ThreadLocal
中的服务。

但是,在 Java 17 中,似乎在您初始化

ThreadLocal
之前就初始化了池,因此它不会继承您设置的值。

然而,即使其他 Java 版本可能没有相同的行为,我仍然认为将

ForkJoinPool
ThreadLocal
结合起来从根本上来说是错误的。使用
ThreadLocal
来实现真正的线程本地化并且本来就是如此。


至于

LocalThread
类 - 您确实意识到这只是一个带有
run()
方法的类,您在正常执行时在主线程上调用该方法?如果你想在另一个线程上运行它,你需要做
new Thread(new LocalThread()).start()
。但由于您显式启动了该线程,因此可以保证继承此处的
ThreadLocal
的值,因此代码将可靠地工作。 (尽管我对缓存服务的线程安全问题仍然存在。)

© www.soinside.com 2019 - 2024. All rights reserved.