【问题标题】:Semaphore for pausing actions, not limiting concurrent actions用于暂停动作的信号量,不限制并发动作
【发布时间】:2024-01-22 02:59:01
【问题描述】:

我有一个被许多线程不断调用的方法,它在数据结构中查找数据。

当需要更新该数据结构时,我需要阻止所有尝试查找的线程,直到更新完成。

我可以使用带有 X 许可的信号量,并让 update 方法在执行更新之前获取所有 X 许可,然后再次释放它们 - 但现在 X 还用于将我的查找方法限制为 X 个同时调用的次要目的,我不想。

我还有哪些其他选项可以阻止对更新方法的调用而不限制它的同时执行?

【问题讨论】:

  • 这可能与您当前的思路相反,但是限制同时调用您的方法的数量可能不一定是坏事。由于可用内存以及生成新线程所需的大量时间,您的机器无论如何都可以运行多少线程有实际限制 - 也许您可以将 X 设置为您肯定永远无法达到的大数字(Integer.MAX_VALUE ),或者在达到之前可能会导致处理效率问题的数字 (10*Runtime.getRuntime().availableProcessors())。这完全取决于该方法调用在做什么。

标签: java multithreading concurrency semaphore


【解决方案1】:

考虑使用 ReadWriteLock,特别是 ReentrantReadWriteLock。每个读取线程都可以锁定readLock,当需要写入时,只需锁定writeLock

写锁在被持有时,将阻止所有读取(和后续写入)发生,直到写锁被释放。持有readLock 的线程可以同时执行。即多个线程可以同时持有readLock。

如果您使用的是 Java 8,我建议您使用 StampedLock

【讨论】:

  • 我们还在 7 上,但 ReentrantReadWriteLock 看起来 正是 我需要的,非常感谢
【解决方案2】:

John Vint 的回答 - 阅读器/写入器锁 - 听起来最适合您的问题,但您让我想到了一种古老且非常简单的设计模式,称为“旋转门”。

(警告!这是一个非答案:旋转门实际上并没有解决问题。它可能会阻止线程进入查找例程,但它对已经存在的线程没有任何作用更新线程激活时的查找例程。)

无论如何,旋转门可以追溯到信号量被认为是低级原语的时代,所有其他同步对象都将从中派生。 (比 Java 早得多)。可能有一种更有效的方法来做到这一点,但我一直很喜欢实现的简单性:

class Turnstile {
    private final Semaphore semaphore = new Semaphore(1);

    // To be called in threads that need to "pass through" the turnstile.
    public void passThrough() {
        semaphore.acquire();
        semaphore.release();
    }

    // To be called only in the single thread that owns the turnstile.
    public void lock() {
        semaphore.acquire();
    }

    // To be called only in the single thread that owns the turnstile.
    public void unlock() {
        semaphore.release();
    }
}

【讨论】:

  • 约翰的回答应该更高效;但是很高兴看到巧妙使用基本信号量的实现 - 谢谢:-)
  • 尽管停下来想一想,它实际上并不能解决问题。通过旋转门的线程“Y”现在正在执行查找方法。在它完成之前,没有什么可以阻止线程“X”调用“lock()”并开始对数据结构进行更改。它防止 new 线程在更新进行时开始不安全代码,但不考虑可能已经存在的线程。
  • @tdimmig,是的,这就是为什么我说约翰文特的回答是“更合适”。我会修改我的答案,说一些更强有力的东西。
最近更新 更多