无锁并不像人们所说的那么好

简介:

原文链接译文连接,译者:梁海舰,校对:李任

注意,我将在这篇文章中说一些关于“无锁”和“无等待”算法的坏话。在我们开始之前,让我来问你一个问题:

你之前有过需要无锁算法来解决问题吗?为什么必须使用无锁算法来解决?

这是一个意味深长的问题,在某种意义上,我们尝试区分哪些问题需要“无锁”或者受益于“无锁”提供的保证,哪些问题使用阻塞解决方案就已经足够了。那么,什么样的问题可以利用无锁算法解决呢?

你会说高竞争下的扩展性?是的,听起来无锁算法确实是合适的候选方案,但是,实际上它很大程度取决于算法的某一些特征。

例如:如果循环中有很多CAS操作,当你增加线程数量时候,那些CAS操作将增加大量的重试,记住CAS操作的代价是很高的,并且很有可能成为该算法的瓶颈。实际上,这意味着CAS操作上越多的线程竞争,你的代码运行的整体速度越慢,不管你添加多少线程。

如果你一开始没有很多线程,那为什么不使用阻塞的方式呢?在这个例子中使用阻塞的方式通常更简单、更快实现。

如果问题允许,你甚至可以使用读写锁,不同于大部分无锁算法,读写锁已经展示了读线程的数量可扩展。

还有人说用在解决高竞争下的低延迟,好吧,无锁仅仅保证了某些线程能够执行,但让我们假设你没有一个高优先级的线程或一个高于其他线程优先级的线程,未必当前线程就在执行它的任务(在这种情况下你需要无等待算法实现低延迟保证)。

对于这样的情况,无锁确实给了你比阻塞更好的保证,但是除此之外不要期望得太多。首先,你很可能丢失一些性能(但不是很多),其次,如果其他线程持续的执行,那么特定的任务可能耗费无限的时间直至完成并且导致当前操作重启。这意味着那些循环中包含很多CAS操作的无锁算法,执行起来慢得离谱,其原因在于重试的次数,并不是所谓的复杂性。

现在我们知道为什么使用无锁,值得注意的是,许多无锁算法在它们执行的一些步骤中会暗自分配和释放内存。

我所知道的,没有一个系统使用“无锁”算法进行内存管理,事实上,在大部分系统中,内存分配有时候执行得非常慢。想象一下你申请一个对象,系统需要为你获得一个新的内存页,更糟糕的是,内存不足,需要将它旧的内容从内存中交换到硬盘中。

如果你关注的是扩展能力,那么偶尔的阻塞,内存分配慢并不是太坏,但问题是你需要的是低延迟,使用无锁进行内存分配、释放没有使用到任何“无锁”算法自身提供的保证。不管使用有内存管理或无内存管理的系统都有这个棘手的问题,换而言之有没有使用内存回收也是一样。

我认为无锁算法的适用性影响非常大,大到我们需要有两个清楚的分类来为无锁算法命名:

无界无锁:这个算法是无锁的,但是它的方法中或多或少需要为新的对象分配内存或者释放一些对象占用的内存。

有界无锁:这个算法是无锁定,并且它的方法中没有涉及到对象分配内存或者是有限制分配并且一开始预先分配,然后使用一个池设计模式,池也是采用无锁方式实现的。

对于“无界无锁”,如果我们教条主义,我们甚至可以说“无界无锁”算法完全不是无锁,因为算法中会有操作被阻塞,并且会导致任务阻塞。

我不会那么教条主义,毕竟内存分配通常是很快的,并且垃圾回收器只是偶尔执行。

有一件事是可以肯定的,那就是给定一个需要解决低延迟的问题,它有两个解决方案可选,一个是“无界无锁”,另一个是“有界无锁”,我会选择有界的那个,即便它性能更低。

顺便说一下,把上面所有段落中的“无锁”字眼替换成“无等待”,它仍然适用。

话虽如此,我想再重申一下我偏爱的并发算法的顺序,下面的算法中最上面的是你可以实现的最好的算法:

  1. 有界集居数无关无等待
  2. 有界无等待
  3. 有界无锁
  4. 无界集居数无关无等待
  5. 无界无等待
  6. 无界无锁
  7. 阻塞
目录
相关文章
|
Java 编译器 开发者
【并发编程的艺术】Java内存模型的顺序一致性
首先明确一点,顺序一致性内存模型是一个被理想化了的理论参考模型,提供了很强的内存可见性保证。其两大特性如下: 1)一个线程中的所有操作,必须按照程序的顺序来执行(代码编写顺序) 2)无论程序是否同步,所有线程都只能看到一个单一的操作执行顺序。每个操作都必须原子执行且立即对所有线程可见。
157 0
|
2月前
|
安全 Java 调度
解锁Java并发编程高阶技能:深入剖析无锁CAS机制、揭秘魔法类Unsafe、精通原子包Atomic,打造高效并发应用
【8月更文挑战第4天】在Java并发编程中,无锁编程以高性能和低延迟应对高并发挑战。核心在于无锁CAS(Compare-And-Swap)机制,它基于硬件支持,确保原子性更新;Unsafe类提供底层内存操作,实现CAS;原子包java.util.concurrent.atomic封装了CAS操作,简化并发编程。通过`AtomicInteger`示例,展现了线程安全的自增操作,突显了这些技术在构建高效并发程序中的关键作用。
65 1
|
4月前
|
Python
Python多线程中递归锁如何解决死锁问题的详细阐述
Python多线程中递归锁如何解决死锁问题的详细阐述
|
5月前
|
Java
【专栏】Java多线程中,锁用于控制共享资源访问,确保数据一致性和正确性,锁是什么意思,有哪些分类?
【4月更文挑战第28天】Java多线程中,锁用于控制共享资源访问,确保数据一致性和正确性。本文探讨锁的概念、作用及分类:乐观锁与悲观锁、自旋锁与适应性自旋锁、公平锁与非公平锁、可重入锁和读写锁。使用锁需注意避免死锁、合理选择锁粒度及性能优化。理解锁有助于提升多线程编程的效率和稳定性。
99 0
|
5月前
|
存储 缓存 Linux
Linux驱动开发(锁和信号量的概念及实现原理)
Linux驱动开发(锁和信号量的概念及实现原理)
107 0
|
5月前
|
Java 开发者
你知道偏向锁已经被废弃了吗?谈谈你对此的看法。
你知道偏向锁已经被废弃了吗?谈谈你对此的看法。
172 0
|
存储 Java
【一文读懂】 Java并发 - 锁升级原理
Java对象头,锁升级的原因,重量级锁、轻量级锁、偏向锁的原理
263 0
【一文读懂】 Java并发 - 锁升级原理
死锁概念
本章讲解什么是死锁以及如何解决
61 0
|
缓存 Linux 容器
深入了解锁细节
深入了解锁细节
68 0
|
设计模式 算法 安全
并发设计模式 之 CAS算法
并发设计模式 之 CAS算法
181 0