<span class="ne-text">LongAdder</span>
与直接使用 CAS 操作相比之所以效率较高,主要原因在于它减少了线程间对同一变量的竞争,从而减少了 CAS 操作的重试次数,提高了性能。接下来将详细解释,并尝试用文字描述来代替示例图。
CAS 操作的问题
CAS 操作是一种乐观锁,它包含三个操作数:内存位置、预期原值及新值。CAS 操作的基本步骤是这样的:
- 系统先检查内存位置的当前值是否与预期原值相同;
- 如果相同,系统会将该位置的数据更新为新值;
- 如果不同,操作失败,并且通常会再次尝试,直到成功为止。
在高并发环境下,若多个线程同时对同一个变量进行更新,它们会不停地执行 CAS 操作。因为这些操作都在争夺同一个变量,导致大多数线程的 CAS 操作失败,并且不得不重试,这会造成大量的 CPU 资源浪费
LongAdder 的工作原理
<span class="ne-text">LongAdder</span>
通过内部分散计数器来减小热点冲突。其基本原理如下:
<span class="ne-text">LongAdder</span>
内部维护了一个<span class="ne-text">Cell[]</span>
数组和一个基础值<span class="ne-text">base</span>
。- 当多个线程尝试更新同一个
<span class="ne-text">LongAdder</span>
时,它们会根据线程的 hash 或其他分配算法映射到不同的<span class="ne-text">Cell</span>
上。 - 各个线程在自己对应的
<span class="ne-text">Cell</span>
上进行 CAS 累加操作,因为它们操作的是不同的<span class="ne-text">Cell</span>
,所以减少了 CAS 重试的次数,从而降低了竞争。 - 当需要获取总和时,
<span class="ne-text">LongAdder</span>
会将所有<span class="ne-text">Cell</span>
中的值以及<span class="ne-text">base</span>
值累加起来。
示意图描述
假设我们有一个计数器,多个线程需要对其进行累加操作。
- 使用 CAS 操作:
- 所有线程都试图更新同一个变量。
- 如果两个线程同时到达,一个线程的更新会导致另一个线程的 CAS 操作失败。
- 失败的线程必须重试,这在高并发下会不断发生。
Thread 1 --------------X (CAS失败,重试)
|
Thread 2 ---- CAS ----> [Counter]
|
Thread 3 --------------X (CAS失败,重试)
- 使用 LongAdder:
- 计数器分散在一个
<span class="ne-text">Cell</span>
数组中。 - 线程 1、2、3 被分配到不同的
<span class="ne-text">Cell</span>
上,它们可以并行地、独立地更新。 - 更新操作几乎不会失败,因为 CAS 的竞争被分散了。
- 计数器分散在一个
Thread 1 ---- CAS ----> [Cell 1]
Thread 2 ---- CAS ----> [Cell 2]
Thread 3 ---- CAS ----> [Cell 3]
当更新操作在不同的 <span class="ne-text">Cell</span>
上执行,每个 <span class="ne-text">Cell</span>
都能独立地进行 CAS 操作而几乎不会导致竞争,因此 <span class="ne-text">LongAdder</span>
在高并发环境下的性能要优于直接使用 CAS 操作。在必要时,只有少数情况下线程会发生冲突,并且发生冲突时只需要对个别 <span class="ne-text">Cell</span>
进行重试,而不是所有尝试更新的线程。