在系统中,我有一个对象-我们称之为TaskProcessor
。它保存任务队列,这些任务由一些线程池(ExecutorService
+ PriorityBlockingQueue
)执行
每个任务的结果都以唯一标识符的形式保存在数据库中。
知道此唯一标识符的用户可以检查此任务的结果。结果可能在数据库中,但是任务仍然可以在队列中等待执行。在这种情况下,UserThread
应该等待任务完成。
此外,以下假设是有效的:
其他人可以将任务排队到TaskProcessor
,如果他知道唯一标识符,则随机的UserThread
可以访问结果。
UserThread
和TaskProcess
在同一应用中。 TaskProcessor
包含一个线程池,而UserThread
就是servlet线程。
UserThread
,并且结果尚未完成。 UserThread
应在TaskProcessor
完成由唯一标识符分组的一个或多个任务后立即解除阻止
我的第一次尝试(天真)是在循环中检查结果并休眠一段时间:
// UserThread
while(!checkResultIsInDatabase(uniqueIdentifier))
sleep(someTime)
但是我不喜欢它。首先,我在浪费数据库连接。此外,如果任务在睡眠后立即完成,那么即使结果刚刚出现,用户也将等待。
下次尝试基于等待/通知:
//UserThread
while (!checkResultIsInDatabase())
taskProcessor.wait()
//TaskProcessor
... some complicated calculations
this.notifyAll()
但是我也不喜欢。如果更多的UserThreads
将使用TaskProcessor
,那么每次完成某些任务时,它们都会被不必要地唤醒,而且它们还会进行不必要的数据库调用。
最后一次尝试是基于我称为waitingRoom
的东西:
//UserThread
Object mutex = new Object();
taskProcessor.addToWaitingRoom(uniqueIdentifier, mutex)
while (!checkResultIsInDatabase())
mutex.wait()
//TaskProcessor
... Some complicated calculations
if (uniqueIdentifierExistInWaitingRoom(taskUniqueIdentifier))
getMutexFromWaitingRoom(taskUniqueIdentifier).notify()
但这似乎并不安全。在数据库检查和wait()
之间,任务可以完成(notify()
尚未生效,因为UserThread
尚未调用wait()
),可能会导致死锁
看来,我应该在某个地方同步它。但是,恐怕它不会有效。 有没有办法纠正我的任何尝试,使它们安全有效?也许还有其他更好的方法可以做到这一点?
答案 0 :(得分:10)
您似乎正在寻找某种未来 / 承诺抽象。看一下CompletableFuture,从Java 8开始可用。
CompletableFuture<Void> future = CompletableFuture.runAsync(db::yourExpensiveOperation, executor);
// best approach: attach some callback to run when the future is complete, and handle any errors
future.thenRun(this::onSuccess)
.exceptionally(ex -> logger.error("err", ex));
// if you really need the current thread to block, waiting for the async result:
future.join(); // blocking! returns the result when complete or throws a CompletionException on error
您还可以从异步操作中返回一个(有意义的)值,并将结果传递给回调。要使用此功能,请查看supplyAsync()
,thenAccept()
,thenApply()
,whenComplete()
等。
您还可以将多个期货组合成一个或更多。
答案 1 :(得分:4)
我相信在mutex
方法中将CountDownLatch
替换为waitingRoom
可以防止死锁。
CountDownLatch latch = new CountDownLatch(1)
taskProcessor.addToWaitingRoom(uniqueIdentifier, latch)
while (!checkResultIsInDatabase())
// consider timed version
latch.await()
//TaskProcessor
... Some complicated calculations
if (uniqueIdentifierExistInWaitingRoom(taskUniqueIdentifier))
getLatchFromWaitingRoom(taskUniqueIdentifier).countDown()
答案 2 :(得分:2)
使用CompletableFuture
和ConcurrentHashMap
,您可以实现它:
/* Server class, i.e. your TaskProcessor */
// Map of queued tasks (either pending or ongoing)
private static final ConcurrentHashMap<String, CompletableFuture<YourTaskResult>> tasks = new ConcurrentHashMap<>();
// Launch method. By default, CompletableFuture uses ForkJoinPool which implicitly enqueues tasks.
private CompletableFuture<YourTaskResult> launchTask(final String taskId) {
return tasks.computeIfAbsent(taskId, v -> CompletableFuture // return ongoing task if any, or launch a new one
.supplyAsync(() ->
doYourThing(taskId)) // get from DB or calculate or whatever
.whenCompleteAsync((integer, throwable) -> {
if (throwable != null) {
log.error("Failed task: {}", taskId, throwable);
}
tasks.remove(taskId);
})
);
/* Client class, i.e. your UserThread */
// Usage
YourTaskResult taskResult = taskProcessor.launchTask(taskId).get(); // block until we get a result
每当用户要求taskId
的结果时,他们要么:
taskId
的人,则请他们排队;或taskId
的正在进行的任务的结果。 这是当前数百个用户同时使用的生产代码。
在我们的应用程序中,用户通过REST端点(每个用户在其自己的线程上)请求任何给定的文件。我们的taskId
是文件名,而我们的doYourThing(taskId)
是从本地文件系统中检索文件,或者从不存在的S3存储桶中下载文件。
显然,我们不想一次下载同一文件。使用我实现的此解决方案,任何数量的用户都可以在相同或不同的时间请求相同的文件,并且该文件将被下载一次。所有在下载时要求它的用户都将在下载完成的同时获得它。所有稍后要求它的用户,都将立即从本地文件系统中获取它。
像魅力一样。
答案 3 :(得分:0)
我从问题详细信息中了解到的是-
当UserThread请求结果时,有3种可能性:
对于第1点和第3点,它很简单,不会对UserThread造成任何阻塞,只需从DB查询结果即可。
对于第2点-我编写了TaskProcessor
的简单实现。在这里,我使用ConcurrentHashMap
来保留当前尚未完成的任务。该映射包含UniqueIdentifier和相应任务之间的映射。我使用了computeIfPresent()
的{{1}}(在JAVA-1.8中引入)方法,该方法保证对于相同的键,该方法的调用是线程安全的。以下是java doc所说的内容:
Link
如果存在指定键的值,则尝试计算 给定键及其当前映射值的新映射。整个 方法调用是原子执行的。一些尝试更新 其他线程对此地图进行的操作可能会在 计算正在进行中,因此计算应简短且 简单,并且不得尝试更新此地图的任何其他映射。
因此,使用此方法,只要有用户线程请求任务T1,并且如果任务T1在队列中或正在执行但尚未完成,则用户线程将等待该任务。 当任务T1完成时,将通知所有正在等待任务T1的用户请求线程,然后我们将从上图中删除任务T1。
此link中提供了以下代码中使用的其他类引用。
TaskProcessor.java:
ConcurrentHashMap
如果您有任何疑问,请在评论中告诉我。 谢谢。