Java并发:CAS与锁的区别

79

我正在阅读《Java并发编程实战》这本书。在第15章中,讨论了非阻塞算法和compare-and-swap(CAS)方法。

书中写道,CAS的性能比锁定方法要好得多。我想问已经使用过这两个概念的人,想听听你们更喜欢哪一个概念?它真的快那么多吗?

对于我来说,锁的使用更清晰、更易于理解,甚至可能更易于维护(如果我错了,请纠正我)。我们是否应该真的专注于创建与CAS相关的并发代码,以获得更好的性能提升,还是可持续性更重要?

我知道可能没有严格的规则来决定何时使用什么。但我只想听听一些关于CAS的意见和经验。


通常使用监视器对象进行锁定时,涉及CAS以及锁定内核调用。CAS减轻了锁定,因此在没有争用的情况下不会发生内核调用。当存在争用时,CAS可能会旋转一段时间,然后内核调用将发生,或者内核调用将立即跟随。 - Bonita Montero
5个回答

52
CAS通常比锁定快得多,但具体取决于争用的程度。因为如果读取和比较之间值发生变化,CAS可能会强制重试,所以如果涉及的变量受到许多其他线程的大量命中(或从旧值计算新值太昂贵(或两者都是),则理论上线程可能会陷入繁忙等待中。
使用CAS的主要问题是它比锁定更难正确编程。请注意,锁定反过来要比消息传递或STM正确使用困难得多,因此不要将此视为对锁定使用的强烈支持。

2
我同意,重点在于计算新值的成本。通常这个成本非常高,使得非锁定策略输给了基于锁的策略。 - Alex Salauyou

29

操作的相对速度在很大程度上不是问题。重要的是锁定算法和非阻塞算法之间可扩展性的差异。如果您正在运行一个1或2核心系统,请停止考虑这些事情。

非阻塞算法通常比基于锁的算法具有更好的可扩展性,因为它们具有比基于锁的算法更短的“临界区”。


24
您可以比较一下 ConcurrentLinkedQueueBlockingQueue 之间的数字。您会发现,在中度线程争用(在真实世界应用程序中更为常见)的情况下,CAS 显然更快。
非阻塞算法最具吸引力的特性是,如果一个线程失败(缓存未命中,或更糟的是,段错误),则其他线程将不会注意到此失败并可以继续执行。但是,在获取锁时,如果持有锁的线程遇到某种操作系统故障,则等待该锁被释放的所有其他线程也将受到影响。
回答您的问题,是的,非阻塞线程安全算法或集合(例如ConcurrentLinkedQueue, ConcurrentSkipListMap/Set)可能比它们的阻塞对应物要快得多。但正如 Marcelo 所指出的那样,编写正确的非阻塞算法非常困难,需要考虑很多因素。
您应该阅读一下 Michael and Scott Queue,这是 ConcurrentLinkedQueue 的队列实现,解释了如何使用单个CAS处理双向、线程安全、原子函数。

1
关于“Michael和Scott队列”的文章很有趣。谢谢! - Prine

14

有一本与无锁并发主题密切相关的好书,《多处理器编程的艺术》(Maurice Herlihy著)


此外,他的演示文稿部分1部分2也非常值得一看。 - Tamas Ionut

11

如果您正在寻找现实世界的比较,这里有一个例子。我们的应用程序有两个线程:1)用于网络数据包捕获的读取器线程;2)将数据包接收、计数并报告统计信息的消费者线程。

线程 #1 一次只与线程 #2 交换一个数据包。

结果 #1 - 使用自定义的基于 CAS 的交换方法,使用与 SynchronousQueue 相同的原理,我们的类名为 CASSynchronousQueue

30,766,538 packets in 59.999 seconds ::  500.763Kpps, 1.115Gbps 0 drops
libpcap statistics: recv=61,251,128, drop=0(0.0%), ifdrop=0

结果 #2 - 当我们使用标准的Java SynchronousQueue 替换我们的CAS实现时:

8,782,647 packets in 59.999 seconds ::  142.950Kpps, 324.957Mbps 0 drops 
libpcap statistics: recv=69,955,666, drop=52,369,516(74.9%), ifdrop=0

我认为性能差异再清晰不过了。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接