1:什么是“非阻塞”并发,它与使用线程的普通并发有何不同?为什么我们不在所有需要并发的场景中都使用非阻塞并发呢?使用非阻塞并发有开销吗?
非阻塞算法不使用特定的对象锁定方案来控制对内存的并发访问(同步和标准对象锁是使用对象/函数级别锁来减少 Java 中的并发访问问题的示例。相反,这些使用某种形式的低级别在内存位置执行(在某种程度上)同时比较和交换的指令;如果失败,它只会返回 false 并且不会出错,如果它有效,那么它是成功的,你继续前进。通常,这是在一个循环直到它工作,因为只有一小段时间(希望)它会失败,它只是额外循环几次,直到它可以设置它需要的内存。
这并不总是被使用,因为从代码的角度来看它比标准的 Java 同步要复杂得多,即使对于相对琐碎的用例也是如此。此外,对于大多数用途而言,与系统中的其他来源相比,锁定对性能的影响是微不足道的。在大多数情况下,性能要求还不够高,甚至不足以保证看到这一点。
最后,随着 JDK/JRE 的发展,核心设计人员正在改进内部语言实现,以尝试将实现这些目标的最有效方法整合到核心结构中。当您远离核心构造时,您将失去这些改进的自动实现,因为您使用的标准实现较少(例如 jaxb/jibx;jaxb 过去的性能严重低于 jibx,但现在在大多数情况下如果不是更快的话,我从 java 7 开始测试),当你升级你的 java 版本时。
如果您查看下面的代码示例,您可以看到“开销”位置。它本身并不是真正的开销,但代码必须非常高效才能工作非锁定并且由于循环而实际上比标准同步版本执行得更好。即使是轻微的修改也可能导致代码从比标准性能好几倍到差几倍的代码(例如不需要在那里的对象实例化,甚至是快速的条件检查;你说的是节省周期在这里,所以成功和失败之间的差别很小)。
2:我听说在 Java 中可以使用非阻塞并发。是否有我们应该使用此功能的特定场景?
在我看来,只有在以下情况下,您才应该使用它 A) 在您的生产运行系统中,在其生产硬件上存在已证明的性能问题; B)如果你能证明关键部分剩下的唯一低效率是锁定相关的; C)你从你的利益相关者那里得到了坚定的支持,他们愿意使用非标准的不易维护的代码来换取你必须的性能改进 D)在你的生产硬件上进行数字证明,以确保它甚至会有所帮助。
3:将这些方法之一与集合一起使用是否有区别或优势?有哪些取舍?
优势在于性能,首先要权衡的是它是更专业的代码(因此许多开发人员不知道该怎么做;让新团队或新员工更难跟上进度,请记住软件的大部分成本是人工成本,因此您必须注意通过设计决策强加的总拥有成本),并且应该再次测试任何修改以确保构造实际上仍然更快。通常,在需要此功能的系统中,任何更改都需要进行一些性能或负载和吞吐量测试。如果您不进行这些测试,那么我认为您几乎可以肯定甚至不需要考虑这些方法,并且几乎肯定不会看到增加的复杂性有任何价值(如果您一切正常)。
再一次,我只需要重申所有针对优化的标准警告,因为其中许多论点与我在设计中使用的相同。这样做的许多缺点与任何优化相同,例如,每当您更改代码时,您必须确保您的“修复”不会在某些仅用于提高性能的构造中引入低效率,并处理如果修复很关键并且会降低性能,那么(意味着重构整个部分以可能删除优化)。
以非常难以调试的方式将其搞砸真的非常容易,所以如果你不必这样做(我只发现了一些你曾经做过的场景;对我来说那些非常值得怀疑,我宁愿不这样做)不要这样做。使用标准的东西,每个人都会更快乐!
讨论/代码
非阻塞或无锁并发避免使用特定对象锁来控制共享内存访问(如同步块或特定锁)。代码段非锁定时有性能优势;但是,CAS 循环中的代码(如果您采用这种方式,Java 中还有其他方法)必须非常、非常高效,否则最终会花费您更多的性能而不是获得的收益。
与所有性能优化一样,对于大多数用例而言,额外的复杂性并不值得。如果不比大多数优化更好,使用标准结构编写干净的 Java 也能正常工作(实际上,一旦您离开,您的组织可以更轻松地维护软件)。在我看来,这仅在具有已证明性能问题的高性能部分才有意义,其中锁定是低效率的唯一来源。如果您确实没有已知和量化的性能问题,我会避免使用任何类似的技术,直到您证明问题实际上是由于锁定而存在的,而不是对代码效率的其他问题。一旦你发现了一个基于锁定的性能问题,我会确保你有某种类型的指标,以确保这种类型的设置实际上比仅使用标准 Java 并发运行得更快。
我为此完成的实现使用 CAS 操作和 Atomic 系列变量。在这个用例中(用于从高吞吐量翻译系统进行离线测试的随机采样输入和输出),这个基本代码从未锁定或引发任何错误。它基本上是这样工作的:
您有一些在线程之间共享的对象,它被声明为 AtomicXXX 或 AtomicReference(对于大多数重要的用例,您将使用 AtomicReference 版本运行)。
当引用给定的值/对象时,您从 Atomic 包装器中检索它,这将为您提供一个本地副本,您可以在该副本上执行一些修改。从这里你使用 compareAndSwap 作为 while 循环的条件来尝试从你的线程中设置这个 Atomic,如果失败它返回 false 而不是锁定。这将迭代直到它工作(这个循环中的代码必须非常高效和简单)。
您可以查看 CAS 操作以了解它们是如何工作的,它基本上旨在实现为单个指令集,并在末尾进行比较以查看该值是否是您尝试设置的值。
如果 compareAndSwap 失败,您从 Atomic 包装器中再次获取您的对象,再次执行任何修改,然后再次尝试比较和交换,直到它工作。没有特定的锁,您只是尝试将对象设置回内存,如果失败,您只需在线程再次获得控制权时重试。
下面是一个带有列表的简单案例的代码:
/* field declaration*/
//Note that I have an initialization block which ensures that the object in this
//reference is never null, this was required to remove null checks and ensure the CAS
//loop was efficient enough to improve performance in my use case
private AtomicReference<List<SampleRuleMessage>> specialSamplingRulesAtomic = new AtomicReference<List<SampleRuleMessage>>();
/*start of interesting code section*/
List<SampleRuleMessage> list = specialSamplingRulesAtomic.get();
list.add(message);
while(!specialSamplingRulesAtomic.compareAndSet(specialSamplingRulesAtomic.get(), list)){
list = specialSamplingRulesAtomic.get();
list.add(message);
};
/* end of interesting code section*/