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

时间:2014-02-16 20:05:32

标签: java multithreading caching synchronization

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

执行方案:

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

这项工作适用于以下场景


以下是顺序接收

的请求

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

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

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

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


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

以下是并行收到请求的情况(同时

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

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

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

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


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

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

我如何避免这个问题,以便只有一个线程命中数据库而另一个线程从缓存中选择该数据(特别是在并行执行的情况下)?他们已经存在解决这些问题的任何模式吗?

我知道我将请求混合在线程中,但它们本质上是相同的。

如果看起来很糟糕,可以随意修改这个问题的标题。

6 个答案:

答案 0 :(得分:1)

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

后续线程会查看临时对象,但不会查询数据库,但会一直等待结果显示。

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

临时对象的类可以从头开始创建,也可以基于番石榴的SettableFuture,java8 CompletableFuture或java5 FutureTask创建。

附录

为了确保只有一个并发线程启动数据库提取,当线程测试缓存并插入临时对象时,应锁定整个缓存。结果,访问不同密钥的线程相互竞争。如果访问率很高并且这会导致性能下降,this solution可能会有所帮助。它解释了缓存可以并行测试。对于您的任务,应该更新解决方案,以便将获取的数据放入缓存中。

答案 1 :(得分:0)

您可以使用双重检查锁定(http://en.wikipedia.org/wiki/Double-checked_locking)来确保只有一个线程写入缓存,而其他线程仅使用缓存进行读取。

答案 2 :(得分:0)

这个问题让我想起了懒惰初始化的单例模式。如果没有完成同步,则第一个并行线程可以创建多个实例,从而打破了模式的目标。

你可以从解决单身人士问题的Lazy Initialization holder class idiom中获得灵感。

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

答案 3 :(得分:0)

我真的不确定您的问题与延迟初始化有任何相似之处。我认为你的问题可以通过非常简单的解决方案来解决。要使其平行,您可以使用ReadWriteLocks,如:

private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private final Lock cacheReadLock  = readWriteLock.readLock();
private final Lock dbWriteLock = readWriteLock.writeLock();

当您使用writelock(dbWriteLock)从DB更新缓存时,只需从缓存中读取时使用简单的readlock(cacheReadLock)。希望能帮助到你。 如果您希望我添加更多代码示例,请随意发表评论。

答案 4 :(得分:0)

使用原子整数 [比较计数器](初始化为零)并将其传递给所有工作者(线程)。 无论线程在缓存检查之后到达代码点,在进入db之前,都会调用类似这样的内容:

/* some code for read from cache */
/* if not found in cache , then  only */
boolean goAhead = counter.compareAndSet(0,1);
if(goAhead) {
          /* some code to read from db */
          /* some code to put into cache */
        synchronized(counter) {
              counter.notifyAll();
        }

 } else {
       synchronized(counter) {
              counter.wait();
        }
        /* read from cache */
  }

请确保仅在缓存为空时调用此(等待和通知)代码。

答案 5 :(得分:0)

另一个简单的解决方案是执行像双重检查锁定之类的操作,因为我们用它来创建单例设计模式。我知道您担心要避免同步,但使用double checked null lock之类的东西只允许一个线程进行同步,其他线程将等待任务完成。

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

Similar implementation is explained here