Skip to content

Commit 97286b9

Browse files
committed
[#2275] Switch usage counter to AtomicLong
A three-way benchmark of the counter primitive (TopicRawThroughputBenchmark, topic-per-producer, direct dispatch, 0 consumers, JDK 25, 11-core Apple M3 Pro, msgs/sec) showed the striped LongAdder buys nothing measurable over a plain AtomicLong at 1-22 producer threads: threads RW-lock (pre-#2275) AtomicLong LongAdder 1 5.12M +/- 0.10 5.85M +/- 0.11 5.79M +/- 0.10 4 1.48M +/- 0.12 7.03M +/- 0.94 6.93M +/- 1.04 8 1.28M +/- 0.11 5.01M +/- 0.41 5.92M +/- 0.27 22 1.17M +/- 0.09 4.98M +/- 1.53 4.85M +/- 0.59 8-thread rerun (5 forks/variant to rule out an outlier): 8 5.00M +/- 0.45 5.08M +/- 0.31 The 4-6x win over the old ReentrantReadWriteLock comes from removing blocking (park/unpark convoys), not from striping; at the broker's achieved message rates (~2 CAS per message on the shared parent) a single AtomicLong cache line is nowhere near saturation on this class of hardware. The apparent LongAdder edge at 8 threads did not reproduce - the rerun put the two within 1.6% (inside error), with AtomicLong repeating its original score exactly; the outlier was the first run's LongAdder reading. AtomicLong is preferable over LongAdder for simplicity. Validation: MemoryUsageConcurrencyTest (4, incl. the concurrent setUsage drift aggressive test and the 150-round untimed waitForSpace liveness soak) and MemoryUsageTest (5) pass; ProducerFlowControlTest, ProducerFlowControlSendFailTest and TopicProducerFlowControlTest (18) pass.
1 parent 3ebd8d3 commit 97286b9

1 file changed

Lines changed: 17 additions & 17 deletions

File tree

activemq-client/src/main/java/org/apache/activemq/usage/MemoryUsage.java

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
package org.apache.activemq.usage;
1818

1919
import java.util.concurrent.TimeUnit;
20-
import java.util.concurrent.atomic.LongAdder;
20+
import java.util.concurrent.atomic.AtomicLong;
2121

2222
/**
2323
* Used to keep track of how much of something is being used so that a
@@ -29,11 +29,14 @@
2929
*/
3030
public class MemoryUsage extends Usage<MemoryUsage> {
3131

32-
// PROTOTYPE: lock-free usage accounting. The counter is a striped LongAdder so
33-
// increase/decrease never take an exclusive lock; the usageLock is only taken when the
34-
// rounded percentUsage actually changes (at most ~100/percentUsageMinDelta times per
35-
// limit traversal), which preserves listener events and waitForSpace signalling.
36-
private final LongAdder usage = new LongAdder();
32+
// Lock-free usage accounting: the counter is an AtomicLong so increase/decrease never
33+
// take an exclusive lock; the usageLock is only taken when the rounded percentUsage
34+
// actually changes (at most ~100/percentUsageMinDelta times per limit traversal), which
35+
// preserves listener events and waitForSpace signalling. AtomicLong was chosen over a
36+
// striped LongAdder after benchmarking showed equal throughput at 1-22 producer threads
37+
// on an 11-core machine, while AtomicLong keeps get() exact, makes setUsage() a plain
38+
// atomic set, and avoids per-instance cell inflation.
39+
private final AtomicLong usage = new AtomicLong();
3740

3841
public MemoryUsage() {
3942
this(null, null);
@@ -166,7 +169,7 @@ public void increaseUsage(long value) {
166169
// recomputes the percent from a sum that includes all completed updates, so a lasting
167170
// 100% -> <100% transition always reaches the locked setPercentUsage() path, which
168171
// signals waitForSpaceCondition. Breaking this ordering can strand waiters forever.
169-
usage.add(value);
172+
usage.addAndGet(value);
170173
maybeUpdatePercent();
171174

172175
if (parent != null) {
@@ -186,7 +189,7 @@ public void decreaseUsage(long value) {
186189

187190
// INVARIANT: add() must be followed unconditionally by maybeUpdatePercent()
188191
// (see increaseUsage for the full liveness rationale).
189-
usage.add(-value);
192+
usage.addAndGet(-value);
190193
maybeUpdatePercent();
191194

192195
if (parent != null) {
@@ -214,24 +217,21 @@ private void maybeUpdatePercent() {
214217

215218
@Override
216219
protected long retrieveUsage() {
217-
return usage.sum();
220+
return usage.get();
218221
}
219222

220223
@Override
221224
public long getUsage() {
222-
return usage.sum();
225+
return usage.get();
223226
}
224227

225228
/**
226-
* Sets the usage to the given value. Implemented as a delta adjustment because
227-
* LongAdder.reset() is only safe when there are no concurrent updates - a racing
228-
* increase/decrease could be lost outright. With a delta add, a concurrent update is
229-
* always preserved, equivalent to it linearizing after the set. Note: as with the
230-
* historical field assignment, this does not propagate an adjustment to the parent
231-
* usage.
229+
* Sets the usage to the given value as a single atomic store; a concurrent
230+
* increase/decrease linearizes cleanly before or after it. Note: as with the historical
231+
* field assignment, this does not propagate an adjustment to the parent usage.
232232
*/
233233
public void setUsage(long value) {
234-
this.usage.add(value - this.usage.sum());
234+
this.usage.set(value);
235235
maybeUpdatePercent();
236236
}
237237

0 commit comments

Comments
 (0)