【问题标题】:Why Non blocking Concurrency is better than blocking concurrency为什么非阻塞并发优于阻塞并发
【发布时间】:2018-11-15 22:39:06
【问题描述】:

我只是想知道为什么非阻塞并发比阻塞并发更好。在阻塞并发中你的线程必须等到其他线程完成它的执行。所以在这种情况下线程不会消耗 CPU。

但是,如果我谈论非阻塞并发,线程不会等待获得锁,如果某些线程包含锁,它们会立即返回。

例如在ConcurrentHashMap 类中,在put() 方法中,tryLock() 在循环中。由于tryLock() 是非阻塞的,其他线程将处于活动状态并不断尝试检查锁是否已被释放。我假设在这种情况下,不需要使用 CPU。

那么暂停线程直到其他线程完成执行并在工作完成时唤醒线程不是很好吗?

【问题讨论】:

  • 这取决于你是否希望(例如)你的整个程序因为一件事还没有完成而被阻止。
  • ConcurrentHashMap 不会锁定整个集合,因此可以有 2+ 个线程使用这种类型的集合,与同步 hashmap 相比,它提高了性能,例如

标签: java multithreading concurrency locking spinlock


【解决方案1】:

阻塞或非阻塞并发是否更好取决于您期望等待多长时间才能获取您正在等待的资源。

通过阻塞等待(即,mutex 锁,用 C 语言来说),操作系统内核使等待线程进入睡眠状态。 CPU 调度程序不会为其分配任何时间,直到 所需的资源变得可用。这里的好处是,正如你所说,这个线程在睡眠时不会消耗任何 CPU 资源。

但是有一个缺点:将线程置于睡眠状态、确定何时唤醒以及再次唤醒它的过程既复杂又昂贵,并且可能会抵消通过不让线程消耗 CPU 而实现的节省等待。此外(可能正因为如此),操作系统可能会选择在资源可用时立即唤醒线程,因此锁的等待时间可能会超过必要的时间。

非阻塞等待(也称为自旋锁)在等待时确实会消耗 CPU 资源,但节省了将线程置于睡眠状态、确定何时唤醒并唤醒它的开销.一旦锁被释放,它也可能能够更快地响应,因为它在何时可以继续执行方面不太受操作系统的影响。

因此,作为一个非常普遍的规则,如果您希望只等待很短的时间(例如,另一个线程完成 ConcurrentHashMap 中的条目可能需要几个 CPU 周期),您应该更喜欢自旋锁。对于更长的等待时间(例如同步 I/O 或多个线程等待单个复杂计算),互斥体(阻塞等待)可能更可取。

【讨论】:

  • 短语“非阻塞等待”没有意义。 “等待”和“阻塞”都是同一个意思。等待自旋锁的线程被“阻塞”,因为在其他线程释放锁之前它无法取得任何进展。您不能将使用自旋锁的算法称为“非阻塞”算法,因为如果您挂起或终止已获取锁的线程,这将阻止其他需要获取锁的线程取得任何进展。 en.wikipedia.org/wiki/Non-blocking_algorithm
  • @jameslarge 我同意,我对使用这些术语感到紧张。但是,我决定继续使用 OP 使用的术语,因为我相信从他们所暗示的问题中可以清楚地看到。
  • 我没有时间理解原来的问题。我不知道 OP 询问的是什么 ConcurrentHashMap.java 源代码。 OpenJDK 版本似乎没有使用 tryLock()。
  • 自旋锁,如你所说,如果没有线程持有锁的时间超过很短的时间,那么它在多 CPU 系统上会很有效;但如果仍然有任何单 CPU 平台,那么您绝对不想在其上使用自旋锁定。我通常相信ReentrantLock 的平台实现会为大多数使用模式做一些相当有效的事情。如果实际性能测量证明我可以做得更好,我只会实施某种自定义锁定。
  • @user31601 所以我应该假设,我的问题一定是自旋锁相对于同步的优势,而不是为什么非阻塞并发优于阻塞并发 ?因为根据上面的讨论,很明显自旋锁使用的是非阻塞数据结构(tryLock()),但是因为它是在紧密循环中使用的,所以它表现为阻塞算法。并且我们只想在多核处理器上短时间阻塞,所以最好使用自旋锁而不是同步。
【解决方案2】:

如果您以 ConcurrentHashMap 为例,考虑到由于多个线程执行更新操作(如 put)而导致的开销,以及等待锁释放的阻塞(正如您提到的其他线程将处于活动状态并不断尝试检查是否锁已发布),不会一直如此。

与 HashTable 相比,ConcurrentHashMap 中的并发控制是分开的。所以多个线程可以获取锁(在表的段上)。

最初,ConcurrentHashMap 类支持硬连线预设并发级别 32。这允许最多 32 个 put 和/或 remove 操作同时进行(当超过 32 个线程同时尝试时,同步以外的因素往往会成为瓶颈更新。)

此外,使用 get(key) 和 containsKey(key) 成功检索(当密钥存在时)通常无需锁定即可运行。

因此,例如,一个线程可能正在添加一个元素,这种锁定策略无法完成的操作是仅在元素不存在时才添加元素(ConcurrentReaderHashMap 提供了这样的功能)。

此外,size() 和 isEmpty() 方法需要跨 32 个控制段进行累积,因此可能会稍微慢一些。

【讨论】:

    猜你喜欢
    • 2016-11-06
    • 1970-01-01
    • 2012-10-31
    • 2014-10-24
    • 2011-02-18
    • 1970-01-01
    • 1970-01-01
    • 2017-04-26
    • 2011-05-07
    相关资源
    最近更新 更多