【问题标题】:synchronized access of resource between two threads两个线程之间资源的同步访问
【发布时间】:2018-06-01 13:21:43
【问题描述】:

我有两个线程和一个缓存。我们将线程 1 称为 Tb,它是一个后台线程(即具有较低优先级),另一个线程称为 Tm(具有较高优先级的主线程)。有一个由两个线程更新的缓存。你可以说线程 Tb 是 Tm 的一个助手,用于在可能的情况下抢先填充缓存。

问题是当 Tm 想要访问缓存时,它必须立即访问它,因为某些 UI 更新显示依赖于它。

由于缓存是共享并发写入的,所以我对缓存的同步访问如下:

Element checkAndUpdateCache(int elementPositionToBeChecked){

  Element toBeReturned;

  synchronized(lock){

    // Check if the element is already present in the cache
    if(!cache.hasElement(elementPositionToBeChecked)){

      // If not, retrieve a new one and fill the cache
      toBeReturned = retrieveNewElement(elementPositionToBeChecked);
      cache.put(elementPositionToBeChecked, toBeReturned );
    }
    else{
      toBeReturned = cache.getElement(elementPositionToBeChecked);
    }

  }

  return toBeReturned;

}

问题在于,由于后台线程在循环中调用此方法,它需要非常快速地继续访问缓存并且几乎从不释放锁。目前,我在每个循环周期后调用 Thread.yield() 并另外调用 Thread.sleep(10) 以提供对主线程的一些访问权限。

拥有不同的优先级并没有真正的帮助,在每个循环周期调用 Thread.yield() 也无济于事。 Thread.sleep() 确实有所帮助,但我认为,我们都同意,这根本不是一个好策略。毕竟,我们想要最大的 CPU 利用率,对吧?

有什么方法可以确保无论何时主线程需要访问缓存,它都能在后台线程等待它并稍后恢复操作时轻松获取它?

编辑:实施细节

缓存是Map<Integer, Album>,其中键是Integer

public static Album getAlbum(Context context, int position, @NonNull Cursor cursor, @NonNull Map<Integer, Album> cache){

        // Do we have the Album in cache
        Album albumInfo = cache.get(position);

        if(albumInfo == null){

            cursor.moveToPosition(position);

            // Let's cache this Album
            albumInfo = Album.fromMediaStoreCursor(context, cursor);
            cache.put(position, albumInfo);
        }
EDIT 2
        return albumInfo;
    }

EDIT 2:正在节流的后台线程循环

// While we pre-emptively fetch the Albums to cache in the background :)
if (cursorImages != null) {

    for (int i = 0; i < cursorImages.getCount(); i++) {

        synchronized(SnapsboardApplication.getInstance()) {
            AlbumsListCursorAdapter.getAlbum(ListPhotoVideoAlbumsOnDeviceActivity.this,
                i, cursorImages, cache);
        }

        // Keep checking if we have been asked to cancel
        if (isCancelled()) {
            return null;
        }

        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        Thread.yield();
    }
}

【问题讨论】:

  • 缓存是用什么实现的?如果您使用java.util.concurrent 集合,则根本不需要同步。
  • Tm 是向缓存本身添加数据还是纯粹的读取访问?
  • @EJP :需要同步,因为如果一个线程发现 pos p 的缓存为空,并且在填充缓存时,主线程检查 pos p 的缓存并发现它是空的,但它是快满了
  • @Lothar Tm 也仅使用此方法将数据添加到缓存中。我的意思是,任何一个线程都必须第一次检索不存在于某个位置的数据。后台线程只是尽力提供帮助
  • 使用ConcurrentHashMap.computeIfAbsent() 可将其减少到几行,无需手动同步并提高吞吐量,因为整个缓存不再锁定。 Guava 的Cache 也不错。

标签: java multithreading


【解决方案1】:

Thread Tb 是在后台准备数据的助手。如果一定要保留线程Tb,则需要一种机制来通知线程Tb停止/恢复工作(更进一步,通知线程Tb准备哪些数据)。

丑,声明一个volatile boolean work

线程 Tm。输入checkAndUpdateCache

...
work = false;
synchronized(lock) {
    ...
    work = true;
    lock.notify();
}

线程 Tb。输入checkAndUpdateCache

...
synchronized(lock) {
    while (!work) {
        lock.wait();
    }
    ...
}

但是,我不认为这个问题是唯一的问题。例如:

  • Tb 如何决定是继续准备数据还是准备哪些数据。
  • 线程 Tb / Tm 是否需要不同的函数入口来做不同的事情。
  • 有必要在锁定中调用retrieveNewElement。也许release lock, retrieveNewElement, relock and put 会更好。

【讨论】:

    【解决方案2】:

    在不了解完整实现的情况下,很难给出真正好的建议,但我想到的一个可能的解决方案是将地图的定义更改为 Map&lt;Integer, Future&lt;Album&gt;&gt; 并使用 ExecuterService 检索要添加到地图。这样,后台线程不应该在方法 retrieveNewElement(elementPositionToBeChecked) 中停留太久(我认为这是您的主线程阻塞的原因)并且即使检索未完成,您的主线程也已经得到结果,但是.调用future.get(timeout, TimeUnit.SECONDS) 将返回所需的值或将阻塞直到检索完成(或达到超时)。

    【讨论】:

    • 好的。添加了实现细节。如果您需要任何矿石详细信息,请告诉我。
    • @pulp_fiction 提到的后台线程循环会很有帮助,因为它似乎是导致您的问题的原因。
    • 好的。添加循环
    • @pulp_fiction 您可能只是尝试我的解决方案(应该是对现有代码更改最少的解决方案)。如果这不起作用,您可以将您的地图更改为 ConcurrentMap 并尝试 Kayaman 的(但至少需要 Java 8)。
    • 同意会减少花费的时间,但是如果出现一个位置没有 Future 并且主线程需要立即访问它的情况怎么办。后台线程仍在限制数据检索的处理,现在以 Future 的形式。主线程需要立即访问这些数据。因此,任何后台处理都必须等待。我的意思是,等待问题基本上仍然存在,不是吗?
    猜你喜欢
    • 2011-12-13
    • 1970-01-01
    • 2011-03-10
    • 1970-01-01
    • 1970-01-01
    • 2021-06-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多