How to Achieve Thread‑Safe Random Selection from a Dynamically Changing List in Java
This article explains how to implement a thread‑safe random element picker for a list whose size changes during a performance test, covering the pitfalls of using list.size(), the use of a cached size with AtomicInteger, and a complete asynchronous example.
Problem Statement
When a list of API endpoints is modified dynamically during a performance test (e.g., a QPS model that periodically changes scenario weights), the usual static‑ratio random selection becomes unsafe. The typical implementation directly calls list.size() to bound the random index, which can produce an IndexOutOfBoundsException or return null if the list is altered concurrently.
public static <F> F random(List<F> list) {
if (list == null || list.isEmpty())
ParamException.fail("Array cannot be empty!");
return list.get(getRandomIntZero(list.size()));
}Thread‑Safe Solution
1. Cache the list size in a thread‑safe container
Use java.util.concurrent.atomic.AtomicInteger (or a plain int with proper synchronization) to store the current size of the list. The cache is updated atomically whenever elements are added or removed.
static AtomicInteger size = new AtomicInteger();2. Modify the list asynchronously
Run a background thread that periodically adds or removes elements. After each modification the cached size is adjusted with size.getAndAdd(delta). The order of operations matters:
When increasing the list, add elements to the list first, then increase the cached size.
When decreasing the list, decrease the cached size first, then remove elements from the list.
boolean upKey = false;
while (FunQpsConcurrent.key) {
if (upKey) {
// increase phase – repeat 10 times
10.times {
sleep(10.0);
// remove "reduce" elements
size.getAndAdd(-reduce);
reduce.times { PriapiWriteApiQpsConfig.apiList.remove(13 as Integer) }
// add "add" elements
add.times { PriapiWriteApiQpsConfig.apiList.add(10) }
size.getAndAdd(add);
}
} else {
// decrease phase – repeat 10 times
10.times {
sleep(10.0);
// add "reduce" elements first
reduce.times { PriapiWriteApiQpsConfig.apiList.add(13 as Integer) }
size.getAndAdd(reduce);
// then remove "add" elements
size.getAndAdd(-add);
add.times { PriapiWriteApiQpsConfig.apiList.remove(10 as Integer) }
}
}
upKey = !upKey; // flip direction after each interval
}3. Random selection using the cached size
All threads that need a random endpoint call the method below. Because the size is read from the atomic cache, the index is always within the current bounds of the list.
PriapiWriteApiQpsConfig.apiList.get(getRandomIntZero(size.get()))Key Observations
The cached size must be updated atomically to avoid race conditions.
Using AtomicInteger simplifies the implementation, but a synchronized int would also work.
The approach works under very high request rates; in tests up to 100 000 calls per second no null values or out‑of‑range indices were observed.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
