我有在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 中不同以及是否有其他解决方法的见解吗?
您的代码已损坏,它在大多数情况下都可以工作只是偶然。
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
的值,因此代码将可靠地工作。 (尽管我对缓存服务的线程安全问题仍然存在。)
这不是答案,但我有两条评论;
I) 线程是并发模型。我对“异步”没有太多经验,但在我看来,“异步”是一种“不同”的并发模型。不要混合模型。 ThreadLocal<T>
属于“线程”模型。它保证为每个使用它的
T
提供不同的 Thread
参考。 CompletableFuture.runAsync(task)
最有可能在其实现中的某个地方使用Thread
,*但我不会假设给定的
task
将在其整个过程中由same
Thread
绑定到/安装/执行寿命。在编写依赖于该事实的代码之前,我想找到一些有关该事实的官方文档。II) ThreadLocal
本身并不是一个坏主意;但只有当你将它与
static
结合起来时才有意义。 static
是可测试性和可重用性的敌人(也许还有其他一些“ilities。”)就我自己而言,我尽量不在新代码中使用
static
。我唯一会使用 ThreadLocal
的情况是,如果我急于在新的多线程程序中重用一些旧的单线程代码,而旧代码深深依赖于 static
变量。
* 我在顶部所说的:我对 CompletableFuture
或“异步”任何东西都没有太多经验。