Java 8 到 Java 17 ThreadLocal 问题

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

我有在Java 8中运行良好的代码,但是当我将其迁移到Java 17时,它不起作用。它涉及到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();
         */
    }
}

threadLocalDemo()
中的
SessionHandlerMain
中,我首先为
DateRangeEntity
设置新的
SessionHandler
对象,然后在
runAsync()
方法中,调用
getDateRangeEntity()
来检查该对象是否不为空。这在 Java 8 中有效并打印“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
方法的逻辑,那么这在 Java 17 中是有效的。

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

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

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

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
的值,因此代码将可靠地工作。 (尽管我对缓存服务的线程安全问题仍然存在。)


-1
投票

这不是答案,但我有两条评论;

I) 线程是并发模型。我对“异步”没有太多经验,但在我看来,“异步”是一种“不同”的并发模型。不要混合模型。 ThreadLocal<T>属于“线程”模型。它保证为每个使用它的

T
提供不同的
Thread
参考。
CompletableFuture.runAsync(task)
最有可能在其实现中的某个地方使用
Thread
,*但我不会
假设
给定的task将在其整个过程中由
same
Thread绑定到/安装/执行寿命。在编写依赖于该事实的代码之前,我想找到一些有关该事实的官方文档。

II)

ThreadLocal 本身并不是一个坏主意;但只有当你将它与

static
结合起来时才有意义。
static
是可测试性和可重用性的敌人(也许还有其他一些“
ilities
。”)就我自己而言,我尽量不在新代码中使用 static。我唯一会使用
ThreadLocal
的情况是,如果我急于在新的多线程程序中重用一些旧的单线程代码,而旧代码深深依赖于
static
变量。

* 我在顶部所说的:我对
CompletableFuture

或“异步”任何东西都没有太多经验。

    

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