Java 等待多个并行线程中的一个线程完成

Java 等待多个并行线程中的一个线程完成,java,multithreading,caching,synchronization,Java,Multithreading,Caching,Synchronization,以下是正在发生的事情的简要描述- 执行场景: 搜索请求-->检查此请求的缓存中是否存在数据-->如果缓存中存在数据,则从缓存中获取数据,否则从数据库中获取数据并将数据放入缓存中 对于以下场景,这项工作很好 下面是一个按顺序接收请求的场景 第一个请求请求-->检查缓存(不存在,因为这是第一个请求)-->从数据库获取并放入缓存 第二个请求请求-->检查缓存(数据存在,因为前一个请求已使缓存中的数据可用) 第三个请求请求-->检查缓存(数据仍然存在) 第四个请求请求-->检查缓存(数据仍然存在)

以下是正在发生的事情的简要描述-

执行场景:

搜索请求
-->
检查此请求的缓存中是否存在数据
-->
如果缓存中存在数据,则从缓存中获取数据,否则从数据库中获取数据并将数据放入缓存中

对于以下场景,这项工作很好


下面是一个按顺序接收请求的场景

第一个请求请求-->检查缓存(不存在,因为这是第一个请求)-->从数据库获取并放入缓存

第二个请求请求-->检查缓存(数据存在,因为前一个请求已使缓存中的数据可用)

第三个请求请求
-->
检查缓存(数据仍然存在)

第四个请求请求
-->
检查缓存(数据仍然存在)


但是,如果多个线程同时请求数据,则会失败

这里是一个并行接收请求的场景(同时)

第一个请求请求-->检查缓存(不存在,因为这是第一个请求)-->从数据库获取并放入缓存

第二个请求请求
-->
检查缓存(不存在,因为这是第一个请求)-->从数据库获取并放入缓存

第三个请求请求
-->
检查缓存(不存在,因为这是第一个请求)-->从数据库获取并放入缓存

第四个请求请求
-->
检查缓存(不存在,因为这是第一个请求)-->从数据库获取并放入缓存


你发现问题了吗?每个线程都在访问数据库

我没有使用任何同步块,因为这将使它成为顺序执行,对吗

如何避免这个问题,使只有一个线程命中数据库,而另一个线程从缓存中拾取数据(特别是在并行执行的情况下)?他们是否已经存在解决这些问题的模式

我知道我把线程和请求混合在一起,但它们本质上是一样的


如果这个问题看起来不好,请随意修改它的标题。

您可以使用双重检查锁定()来确保只有一个线程写入缓存,而其他线程仅使用缓存进行读取。

这个问题提醒我关于延迟初始化的单例模式。如果没有进行同步,则第一个并行线程可以创建多个实例,从而破坏模式的目标

你可以从解决单身问题的方法中获得灵感


由于缓存可能很棘手并且充满陷阱,我建议使用第三方缓存实现。例如,已经处理并发问题,应该对您很方便。

第一个看到对象在缓存中不存在的线程创建特殊类(一种未来)的临时对象并将其放置在哈希表中。然后启动数据库查询

后续线程看到临时对象,不查询数据库,而是排队等待结果出现

第一个线程从DB获取结果并通知其他线程

临时对象类可以从头创建,也可以基于guava的SettableFuture、java8 CompletableFuture或java5 FutureTask

附录


为了确保只有一个并发线程开始DB抓取,当线程测试缓存并插入临时对象时,应该锁定整个缓存。因此,访问不同密钥的线程相互竞争。如果访问率高,导致性能下降,则可能会有所帮助。它解释了缓存可以以并行方式进行测试。对于您的任务,应该更新解决方案,以便将获取的数据放入缓存。

我真的不确定您的问题与延迟初始化有什么相似之处。我认为你的问题可以用非常简单的方法解决。要使其并行,可以使用ReadWriteLocks,如:

private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private final Lock cacheReadLock  = readWriteLock.readLock();
private final Lock dbWriteLock = readWriteLock.writeLock();
当您从DB更新缓存时,请使用writelock(dbWriteLock),当您刚从缓存读取时,请使用simple readlock(cacheReadLock)。希望能有帮助。
如果您希望我添加更多代码示例,请随意评论。

使用原子整数(say counter)(初始化为零)并将其传递给所有工作线程。 任何线程在缓存检查之后和进入数据库之前到达代码点的,都将被调用如下所示:


请确保仅当缓存为空时才会调用此(等待并通知)代码

另一个简单的解决方案是执行类似双重检查锁定的操作,我们使用它来创建单例设计模式。我知道您担心要避免同步,但是使用双重检查空锁之类的方法只允许一个线程进入同步,而另一个线程将等待任务完成

if (fetchFromCache() == null) {
    synchronized(this) { // or any monitor lock of choice
          if(fetchFromCache == null) {
                  // code to call db and fill cache data
         }
     }
}
return fetchFromCache(); // this call will never be null

synchronized
是一个关键字,用于标记一个特定的代码块,该代码块一次只能由一个线程使用-其思想不是使所有的
同步化,而是使关键部分同步化。你说的是一次一个线程,这正是我想要避免的。“第一个看到对象的线程在缓存中不存在”否,如果所有线程同时启动,则它们都是第一个线程。这是解决方案的良好开端,但需要进行一些微调。缓存未命中后,必须在缓存上执行原子
putIfAbsent(promise)
,并且只有获胜的线程继续进行DB抓取。@Marko Topolnik是的,我扩展了我的答案。虽然此链接可以回答问题,但最好在此处包含答案的基本部分,并提供链接供参考。如果链接页面发生更改,仅链接的答案可能无效。-谢谢你的意见。我相应地修改了答案
if (fetchFromCache() == null) {
    synchronized(this) { // or any monitor lock of choice
          if(fetchFromCache == null) {
                  // code to call db and fill cache data
         }
     }
}
return fetchFromCache(); // this call will never be null