Skip to content

Commit 4538cb4

Browse files
committed
[#2187] Add lock-free optimized AtomicBitArrayBin and test
- Lock-free CAS-based ring buffer replacement for BitArrayBin using AtomicLongArray vs synchronization. - Includes unit tests for correctness, equivalence with BitArrayBin, and concurrent stress tests
1 parent af6d6de commit 4538cb4

2 files changed

Lines changed: 674 additions & 0 deletions

File tree

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.activemq.util;
18+
19+
import java.util.concurrent.atomic.AtomicLong;
20+
import java.util.concurrent.atomic.AtomicLongArray;
21+
22+
/**
23+
* A lock-free replacement for {@link BitArrayBin} that uses {@link AtomicLongArray}
24+
* as a ring buffer with CAS-based bit operations.
25+
*
26+
* <p>The upstream {@code BitArrayBin} stores bits in a {@code LinkedList<BitArray>}
27+
* and requires external synchronization for all access. This class replaces that
28+
* with a fixed-size {@code AtomicLongArray} ring buffer where each slot holds 64 bits.
29+
* All operations use CAS (compare-and-swap) instead of locks.
30+
*
31+
* <h3>Ring buffer design</h3>
32+
*
33+
* <p>Slots are addressed by absolute position: {@code ringPos = (index / 64) % capacity}.
34+
* This mapping is independent of the window origin, so when the window advances,
35+
* slots that remain in the window keep their data without copying. Only evicted
36+
* slots need to be reclaimed, which happens lazily on first access.
37+
*
38+
* <p>Each slot carries an epoch ({@code slotEpoch[ringPos]}) identifying which
39+
* absolute 64-bit block it holds. On access, if the slot's epoch doesn't match
40+
* the expected epoch, the slot is reclaimed via a brief CAS protocol: the epoch
41+
* is set to a {@code CLEARING} sentinel, bits are zeroed, and the epoch is
42+
* published to the new value. Concurrent readers that see {@code CLEARING}
43+
* retry until the transition completes.
44+
*
45+
* <h3>Concurrency guarantees</h3>
46+
* <ul>
47+
* <li><b>Common path</b> (bit set/get within current window): fully lock-free,
48+
* single CAS on the bits slot</li>
49+
* <li><b>Window advance</b> (rare, only on sequence jumps): single CAS on
50+
* the origin; slot reclamation uses a brief per-slot CAS protocol</li>
51+
* <li><b>No global lock</b>: threads operating on different bit indices within
52+
* the same bin only contend if they hash to the same 64-bit slot</li>
53+
* </ul>
54+
*/
55+
public class AtomicBitArrayBin {
56+
57+
static final int LONG_SIZE = 64;
58+
59+
private static final long UNINITIALIZED = -1L;
60+
private static final long CLEARING = Long.MAX_VALUE;
61+
62+
private final int capacity;
63+
private final AtomicLongArray bits;
64+
private final AtomicLongArray slotEpoch;
65+
private final AtomicLong origin;
66+
private final AtomicLong lastInOrderBit;
67+
68+
public AtomicBitArrayBin(int windowSize) {
69+
capacity = Math.max(1, ((windowSize + 1) / LONG_SIZE) + 1);
70+
bits = new AtomicLongArray(capacity);
71+
slotEpoch = new AtomicLongArray(capacity);
72+
for (var i = 0; i < capacity; i++) {
73+
slotEpoch.set(i, UNINITIALIZED);
74+
}
75+
origin = new AtomicLong(0);
76+
lastInOrderBit = new AtomicLong(UNINITIALIZED);
77+
}
78+
79+
/**
80+
* Set or clear a bit at the given index.
81+
*
82+
* @param index the absolute bit index (message sequence number)
83+
* @param value true to set, false to clear
84+
* @return the previous value of the bit (true if it was already set)
85+
*/
86+
public boolean setBit(long index, boolean value) {
87+
if (index < 0) return false;
88+
89+
while (true) {
90+
var orig = origin.get();
91+
var epoch = index / LONG_SIZE;
92+
var originEpoch = orig / LONG_SIZE;
93+
94+
if (epoch < originEpoch) {
95+
return true;
96+
}
97+
98+
if (epoch >= originEpoch + capacity) {
99+
advanceOrigin(orig, epoch);
100+
continue;
101+
}
102+
103+
var ringPos = (int)(epoch % capacity);
104+
var bitOffset = (int)(index % LONG_SIZE);
105+
long mask = 1L << bitOffset;
106+
107+
if (!ensureSlotEpoch(ringPos, epoch)) {
108+
continue;
109+
}
110+
111+
while (true) {
112+
if (slotEpoch.get(ringPos) != epoch) {
113+
break;
114+
}
115+
116+
var oldBits = bits.get(ringPos);
117+
var wasSet = (oldBits & mask) != 0;
118+
119+
if (value) {
120+
if (wasSet) return true;
121+
if (bits.compareAndSet(ringPos, oldBits, oldBits | mask)) return false;
122+
} else {
123+
if (!wasSet) return false;
124+
if (bits.compareAndSet(ringPos, oldBits, oldBits & ~mask)) return true;
125+
}
126+
}
127+
}
128+
}
129+
130+
/**
131+
* Get the boolean value at the index.
132+
*
133+
* @param index the absolute bit index
134+
* @return true if the bit is set, or if the index is behind the window
135+
*/
136+
public boolean getBit(long index) {
137+
if (index < 0) return false;
138+
139+
var orig = origin.get();
140+
var epoch = index / LONG_SIZE;
141+
var originEpoch = orig / LONG_SIZE;
142+
143+
if (epoch < originEpoch) return true;
144+
if (epoch >= originEpoch + capacity) return false;
145+
146+
var ringPos = (int)(epoch % capacity);
147+
var curEpoch = slotEpoch.get(ringPos);
148+
149+
if (curEpoch != epoch) {
150+
return curEpoch > epoch && curEpoch != UNINITIALIZED;
151+
}
152+
153+
var bitOffset = (int)(index % LONG_SIZE);
154+
return (bits.get(ringPos) & (1L << bitOffset)) != 0;
155+
}
156+
157+
/**
158+
* Test if the index is the next expected in-order sequence.
159+
*
160+
* @param index the absolute bit index
161+
* @return true if this is the next in-order message
162+
*/
163+
public boolean isInOrder(long index) {
164+
var prev = lastInOrderBit.getAndSet(index);
165+
return prev == UNINITIALIZED || prev + 1 == index;
166+
}
167+
168+
/**
169+
* Get the index of the highest set bit across all valid slots.
170+
*
171+
* @return the highest set bit index, or -1 if no bits are set
172+
*/
173+
public long getLastSetIndex() {
174+
var orig = origin.get();
175+
var originEpoch = orig / LONG_SIZE;
176+
177+
for (int offset = capacity - 1; offset >= 0; offset--) {
178+
var epoch = originEpoch + offset;
179+
var ringPos = (int)(epoch % capacity);
180+
181+
var curEpoch = slotEpoch.get(ringPos);
182+
if (curEpoch != epoch || curEpoch == CLEARING) continue;
183+
184+
var slotBits = bits.get(ringPos);
185+
if (slotBits != 0) {
186+
var highBit = LONG_SIZE - 1 - Long.numberOfLeadingZeros(slotBits);
187+
return epoch * LONG_SIZE + highBit;
188+
}
189+
}
190+
return -1;
191+
}
192+
193+
/**
194+
* @return the number of 64-bit slots in the ring buffer
195+
*/
196+
public int getCapacity() {
197+
return capacity;
198+
}
199+
200+
private void advanceOrigin(long currentOrigin, long targetEpoch) {
201+
var newOriginEpoch = targetEpoch - capacity + 1;
202+
var newOrigin = Math.max(0, newOriginEpoch * LONG_SIZE);
203+
if (newOrigin > currentOrigin) {
204+
origin.compareAndSet(currentOrigin, newOrigin);
205+
}
206+
}
207+
208+
private boolean ensureSlotEpoch(int ringPos, long expectedEpoch) {
209+
var curEpoch = slotEpoch.get(ringPos);
210+
211+
if (curEpoch == expectedEpoch) return true;
212+
213+
if (curEpoch == CLEARING) {
214+
Thread.yield();
215+
return false;
216+
}
217+
218+
if (curEpoch > expectedEpoch && curEpoch != UNINITIALIZED) {
219+
return false;
220+
}
221+
222+
if (slotEpoch.compareAndSet(ringPos, curEpoch, CLEARING)) {
223+
bits.set(ringPos, 0);
224+
slotEpoch.set(ringPos, expectedEpoch);
225+
return true;
226+
}
227+
228+
return false;
229+
}
230+
}

0 commit comments

Comments
 (0)